diff --git a/GitReadMe.md b/GitReadMe.md
new file mode 100644
index 0000000..9caf620
--- /dev/null
+++ b/GitReadMe.md
@@ -0,0 +1,15 @@
+1. initial commit with its feature name
+
+2. git reset --soft #hash: undoing a commit (like to see changes that youve made)
+
+3. git commit -m "message" to add the commit (to add this new change)
+
+4. git commit --amend --no-edit (changes the last commit)
+
+"Feature homepage"
+initial commit - "feature homepage"
+commit your new changes "feature homepage"
+
+git log
+git status
+git push --force (anytime you have different commits between local and remote)
diff --git a/ReadMe.md b/ReadMe.md
index 6f4b599..5892c18 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -1,15 +1,52 @@
-1. initial commit with its feature name
+# Blog App – Full Stack Project
-2. git reset --soft: undoing a commit (like to see changes that youve made)
+## Overview
-3. git commit -m "message" to add the commit (to add this new change)
+This is a full stack blog application where users can sign up, log in, create posts (with optional images), comment on posts, and like/unlike posts. The project demonstrates a modern web application architecture using a **React** frontend and a **FastAPI** backend, with a **PostgreSQL** database for persistent storage.
-4. git commit --amend (changes the last commit)
+---
-"Feature homepage"
-initial commit - "feature homepage"
-commit your new changes "feature homepage"
+## Features
-git log
-git status
-git push --force (anytime you have different commits between local and remote)
+- **User Authentication:** Sign up and log in with JWT-based authentication.
+- **Create Posts:** Users can create text posts and optionally upload images.
+- **View Posts:** All posts are displayed in reverse chronological order.
+- **Commenting System:** Users can comment on any post.
+- **Like System:** Users can like or unlike posts.
+- **Responsive UI:** Built with React and Tailwind CSS for a modern look.
+- **Image Uploads:** Uploaded images are stored and served from the backend.
+- **Admin/Feature Roadmap:** (Planned) Admin dashboard, notifications, user profiles, and post categorization.
+
+---
+
+## Technologies Used
+
+### **Frontend**
+
+- **React** (with Vite for fast development)
+- **React Router** (for navigation)
+- **Axios** (for HTTP requests)
+- **Tailwind CSS** (for styling)
+- **React Icons** (for UI icons)
+
+### **Backend**
+
+- **FastAPI** (Python web framework)
+- **SQLAlchemy** (ORM for database models)
+- **Pydantic** (for data validation)
+- **bcrypt** (for password hashing)
+- **python-jose** (for JWT authentication)
+- **python-dotenv** (for environment variable management)
+- **Uvicorn** (ASGI server for FastAPI)
+
+### **Database**
+
+- **PostgreSQL** (relational database)
+
+### **DevOps/Deployment**
+
+- **Docker & Docker Compose** (for containerization and easy local development)
+- **Nginx** (serving the frontend in production)
+- **Render** (for cloud deployment, if used)
+
+---
diff --git a/backend/auth.py b/backend/auth.py
new file mode 100644
index 0000000..7aa2e34
--- /dev/null
+++ b/backend/auth.py
@@ -0,0 +1,30 @@
+import os
+# Load environment variables from .env file
+from dotenv import load_dotenv
+load_dotenv()
+
+from fastapi import HTTPException, status
+from datetime import datetime, timezone, timedelta
+from jose import jwt, JWTError
+
+
+def create_access_token(data: dict, expires_delta: int = 3600):
+ to_encode = data.copy()
+ if expires_delta:
+ expire = datetime.now(timezone.utc) + timedelta(seconds=expires_delta)
+ else:
+ expire = datetime.now(timezone.utc) + timedelta(minutes=15)
+ to_encode.update({"exp": expire})
+ encoded_jwt = jwt.encode(to_encode, os.environ.get("JWT_SECRET_KEY") , algorithm=os.environ.get("JWT_ALGORITHM")) # Use a secure secret key
+ return encoded_jwt
+
+def decode_access_token(token: str):
+ try:
+ payload = jwt.decode(token, os.environ.get("JWT_SECRET_KEY"), algorithms=[os.environ.get("JWT_ALGORITHM")])
+ return payload
+ except JWTError:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid token",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
diff --git a/backend/database-schema.sql b/backend/database-schema.sql
index 36589a4..34323ce 100644
--- a/backend/database-schema.sql
+++ b/backend/database-schema.sql
@@ -8,4 +8,31 @@ CREATE TABLE users (
hashed_password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE posts (
+ post_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(user_id) ON DELETE CASCADE,
+ username TEXT NOT NULL,
+ text TEXT NOT NULL,
+ image_filename TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE comments (
+ comment_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ post_id UUID REFERENCES posts(post_id) ON DELETE CASCADE,
+ user_id UUID REFERENCES users(user_id) ON DELETE CASCADE,
+ username TEXT NOT NULL,
+ text TEXT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE likes (
+ like_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ post_id UUID REFERENCES posts(post_id) ON DELETE CASCADE,
+ user_id UUID REFERENCES users(user_id) ON DELETE CASCADE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
\ No newline at end of file
diff --git a/backend/main.py b/backend/main.py
index 0745d11..29ac2f6 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1,8 +1,13 @@
-from fastapi import FastAPI, HTTPException, status, Depends
+import uuid
+from fastapi import FastAPI, HTTPException, status, Depends, APIRouter, Form, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
+from fastapi.security import OAuth2PasswordBearer
+from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from database import SessionLocal
-from model import UserModel, SignUpRequest, LoginRequest
+from auth import decode_access_token, create_access_token
+from model import UserModel, SignUpRequest, LoginRequest, PostModel, CommentModel, LikeModel
+import os
import bcrypt
@@ -14,15 +19,20 @@
app = FastAPI()
+# FASTAPI security for OAuth2
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl = "/login") # Placeholder for OAuth2 scheme if needed
+
+
+
origins = [
- "https://blog-app-fe-be.onrender.com", #render production
+ "https://blog-app-fe-be.onrender.com", #frontend production server
+ "https://blog-app-y08k.onrender.com", #backend production server
"http://localhost:5173", #local development
"http://localhost" #docker compose
-
]
app.add_middleware(
CORSMiddleware,
- allow_origins=origins,
+ allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -35,10 +45,12 @@ def get_db():
finally:
db.close()
+
@app.get("/")
-def read_root():
- return {"message": "Hello shmoe mama 123"}
+def root():
+ return {"message": "Backend is running"}
+#public endpoints for signup and login
@app.post("/signup")
def signup(request: SignUpRequest, db: Session = Depends(get_db)):
existing_user = db.query(UserModel).filter(
@@ -84,11 +96,170 @@ def login(request: LoginRequest, db: Session = Depends(get_db)):
detail="Incorrect password"
)
+ #creating access token jwt
+ access_token = create_access_token(data={"sub": str(user.user_id), "username": user.username})
return {
+ "access_token": access_token,
+ "token_type": "bearer",
"message": "Login successful",
"user": {
"user_id": str(user.user_id),
"username": user.username,
"email": user.email
}
- }
\ No newline at end of file
+ }
+
+def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
+ payload = decode_access_token(token)
+ if not payload:
+ raise HTTPException(status_code=401, detail="Invalid token")
+ user_id = payload.get("sub")
+ user = db.query(UserModel).filter(UserModel.user_id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+ return user
+
+
+# Protected endpoints that require authentication
+auth_router = APIRouter(prefix="/auth") # allows for routing of auth related endpoints
+@auth_router.get("/home")
+def read_users_me(current_user: UserModel = Depends(get_current_user)):
+ return {
+ "message": f"Welcome {current_user.username}!",
+ "user_id": str(current_user.user_id),
+ "username": current_user.username,
+ "email": current_user.email
+ }
+
+UPLOAD_DIR = "static/images"
+os.makedirs(UPLOAD_DIR, exist_ok=True)
+@auth_router.post("/posts")
+def create_post(
+ text:str = Form(...),
+ image: UploadFile = File(None),
+ db: Session = Depends(get_db),
+ current_user: UserModel = Depends(get_current_user)
+):
+ image_filename = None
+ if image:
+ image_filename = f"{uuid.uuid4()}_{image.filename}"
+ with open(os.path.join(UPLOAD_DIR, image_filename), "wb") as buffer:
+ buffer.write(image.file.read())
+ post = PostModel(
+ user_id=current_user.user_id,
+ username=current_user.username,
+ text=text,
+ image_filename=image_filename,
+ )
+ db.add(post)
+ db.commit()
+ db.refresh(post)
+ return {
+ "message": "Post created successfully",
+ "post": {
+ "post_id": str(post.post_id),
+ "username": post.username,
+ "text": post.text,
+ "image_filename": post.image_filename,
+ "created_at": post.created_at.isoformat() if post.created_at else None,
+ }
+ }
+
+
+@auth_router.get("/posts")
+def get_posts(
+ db: Session = Depends(get_db),
+ current_user: UserModel = Depends(get_current_user)
+):
+ posts = db.query(PostModel).order_by(PostModel.created_at.desc()).all()
+ result = []
+ for post in posts:
+ likes_count = db.query(LikeModel).filter(LikeModel.post_id == post.post_id).count()
+ comments_count = db.query(CommentModel).filter(CommentModel.post_id == post.post_id).count()
+ result.append({
+ "post_id": str(post.post_id),
+ "user_id": str(post.user_id),
+ "username": post.username,
+ "text": post.text,
+ "image_filename": post.image_filename,
+ "created_at": post.created_at.isoformat() if post.created_at else None,
+ "updated_at": post.updated_at.isoformat() if post.updated_at else None,
+ "likes_count": likes_count,
+ "comments_count": comments_count
+ })
+ return result
+app.mount("/images", StaticFiles(directory=UPLOAD_DIR), name="images")
+
+# Creating and Getting Comments
+@auth_router.post("/posts/{post_id}/comments")
+def create_comment(
+ post_id: str,
+ text: str = Form(...),
+ db: Session = Depends(get_db),
+ current_user: UserModel = Depends(get_current_user)
+):
+ post = db.query(PostModel).filter(PostModel.post_id == post_id).first()
+ if not post:
+ raise HTTPException(status_code = 404, detail = "Post not found")
+ comment = CommentModel(
+ post_id=post.post_id,
+ user_id=current_user.user_id,
+ username=current_user.username,
+ text=text
+ )
+ db.add(comment)
+ db.commit()
+ db.refresh(comment)
+ return {
+ "message": "Comment created successfully",
+ "comment": {
+ "comment_id": str(comment.comment_id),
+ "post_id": str(comment.post_id),
+ "user_id": str(comment.user_id),
+ "username": comment.username,
+ "text": comment.text,
+ "created_at": comment.created_at.isoformat() if comment.created_at else None,
+ }
+ }
+@auth_router.get("/posts/{post_id}/comments")
+def get_comments(
+ post_id: str,
+ db: Session = Depends(get_db),
+ current_user: UserModel = Depends(get_current_user)
+):
+ post = db.query(PostModel).filter(PostModel.post_id == post_id).first()
+ if not post:
+ raise HTTPException(status_code=404, detail="Post not found")
+
+ comments = db.query(CommentModel).filter(CommentModel.post_id == post_id).order_by(CommentModel.created_at.desc()).all()
+ return [
+ {
+ "comment_id": str(comment.comment_id),
+ "post_id": str(comment.post_id),
+ "user_id": str(comment.user_id),
+ "username": comment.username,
+ "text": comment.text,
+ "created_at": comment.created_at.isoformat() if comment.created_at else None,
+ }
+ for comment in comments
+ ]
+
+# Creating and Getting Likes
+@auth_router.post("/posts/{post_id}/like")
+def like_post(post_id: str, db: Session = Depends(get_db), current_user: UserModel = Depends(get_current_user)):
+ existing_like = db.query(LikeModel).filter_by(post_id=post_id, user_id=current_user.user_id).first()
+ if existing_like:
+ db.delete(existing_like)
+ db.commit()
+ return {"message": "Post unliked"}
+ like = LikeModel(post_id=post_id, user_id=current_user.user_id)
+ db.add(like)
+ db.commit()
+ return {"message": "Post liked"}
+
+@auth_router.get("/posts/{post_id}/likes")
+def get_likes(post_id: str, db: Session = Depends(get_db), current_user: UserModel = Depends(get_current_user)):
+ count = db.query(LikeModel).filter_by(post_id=post_id).count()
+ return {"likes_count": count}
+
+app.include_router(auth_router)
diff --git a/backend/model.py b/backend/model.py
index 3fe3b9c..b33def7 100644
--- a/backend/model.py
+++ b/backend/model.py
@@ -1,5 +1,5 @@
# database models
-from sqlalchemy import Column, String, DateTime
+from sqlalchemy import Column, String, DateTime, ForeignKey, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
from database import Base
@@ -16,6 +16,33 @@ class UserModel(Base):
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
+class PostModel(Base):
+ __tablename__ = "posts"
+ post_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True, nullable=False)
+ user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
+ username = Column(String, nullable=False)
+ text = Column(Text, nullable=False)
+ image_filename = Column(String, nullable=True)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
+
+class CommentModel(Base):
+ __tablename__ = "comments"
+ comment_id = Column(UUID(as_uuid=True), primary_key = True, default = uuid.uuid4, unique=True, nullable=False)
+ post_id = Column(UUID(as_uuid=True), ForeignKey("posts.post_id"), nullable=False)
+ user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
+ username = Column(String, nullable=False)
+ text = Column(Text, nullable=False)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
+
+class LikeModel(Base):
+ __tablename__ = "likes"
+ like_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True, nullable=False)
+ post_id = Column(UUID(as_uuid=True), ForeignKey("posts.post_id"), nullable=False)
+ user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable = False)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+
#difference between BaseModel and Base
#Base = sql specific Model
#BaseModel = pydantic specific Model
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index bd59caf..94911bb 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -7,7 +7,11 @@ requires-python = ">=3.13"
dependencies = [
"bcrypt>=4.3.0",
"fastapi>=0.115.12",
+ "passlib[bcrypt]>=1.7.4",
"psycopg2-binary>=2.9.10",
"pydantic>=2.11.5",
+ "python-dotenv>=1.1.0",
+ "python-jose[cryptography]>=3.5.0",
+ "python-multipart>=0.0.20",
"sqlalchemy>=2.0.41",
]
diff --git a/backend/uv.lock b/backend/uv.lock
index 6610700..2bb5b29 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -31,8 +31,12 @@ source = { virtual = "." }
dependencies = [
{ name = "bcrypt" },
{ name = "fastapi" },
+ { name = "passlib", extra = ["bcrypt"] },
{ name = "psycopg2-binary" },
{ name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "python-jose", extra = ["cryptography"] },
+ { name = "python-multipart" },
{ name = "sqlalchemy" },
]
@@ -40,8 +44,12 @@ dependencies = [
requires-dist = [
{ name = "bcrypt", specifier = ">=4.3.0" },
{ name = "fastapi", specifier = ">=0.115.12" },
+ { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
{ name = "pydantic", specifier = ">=2.11.5" },
+ { name = "python-dotenv", specifier = ">=1.1.0" },
+ { name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" },
+ { name = "python-multipart", specifier = ">=0.0.20" },
{ name = "sqlalchemy", specifier = ">=2.0.41" },
]
@@ -95,6 +103,75 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" },
]
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "45.0.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712, upload-time = "2025-06-10T00:02:38.826Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" },
+ { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" },
+ { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" },
+ { url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769, upload-time = "2025-06-10T00:02:58.467Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441, upload-time = "2025-06-10T00:03:00.14Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836, upload-time = "2025-06-10T00:03:01.726Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" },
+ { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" },
+ { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" },
+ { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557, upload-time = "2025-06-10T00:03:22.563Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508, upload-time = "2025-06-10T00:03:24.586Z" },
+]
+
+[[package]]
+name = "ecdsa"
+version = "0.19.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" },
+]
+
[[package]]
name = "fastapi"
version = "0.115.12"
@@ -142,6 +219,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
+[[package]]
+name = "passlib"
+version = "1.7.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
+]
+
+[package.optional-dependencies]
+bcrypt = [
+ { name = "bcrypt" },
+]
+
[[package]]
name = "psycopg2-binary"
version = "2.9.10"
@@ -161,6 +252,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" },
]
+[[package]]
+name = "pyasn1"
+version = "0.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
+]
+
[[package]]
name = "pydantic"
version = "2.11.5"
@@ -204,6 +313,64 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
]
+[[package]]
+name = "python-dotenv"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
+]
+
+[[package]]
+name = "python-jose"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ecdsa" },
+ { name = "pyasn1" },
+ { name = "rsa" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" },
+]
+
+[package.optional-dependencies]
+cryptography = [
+ { name = "cryptography" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
+]
+
+[[package]]
+name = "rsa"
+version = "4.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
[[package]]
name = "sniffio"
version = "1.3.1"
diff --git a/docker-compose.yml b/docker-compose.yml
index f07aa5b..90421e3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,7 +10,7 @@ services:
build:
context: ./frontend
args:
- VITE_API_URL: http://localhost:8000
+ VITE_API_URL: http:s//localhost:8000
ports:
- "80:80"
database:
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index c2f2a67..8a5766c 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -3,6 +3,7 @@ import LoginPage from "./pages/loginPage";
import SignUpPage from "./pages/signUpPage";
import HomePage from "./pages/homePage";
import PostsPage from "./pages/postsPage";
+import FeaturesPage from "./pages/featuresPage";
import {
Route,
createBrowserRouter,
@@ -26,8 +27,9 @@ const router = createBrowserRouter(
Backend says: {message || "Loading..."}
)} +{new Date(post.created_at).toLocaleString()}
+
+
+