Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions GitReadMe.md
Original file line number Diff line number Diff line change
@@ -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)
57 changes: 47 additions & 10 deletions ReadMe.md
Original file line number Diff line number Diff line change
@@ -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)

---
30 changes: 30 additions & 0 deletions backend/auth.py
Original file line number Diff line number Diff line change
@@ -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"},
)
27 changes: 27 additions & 0 deletions backend/database-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
187 changes: 179 additions & 8 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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=["*"],
Expand All @@ -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(
Expand Down Expand Up @@ -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
}
}
}

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)
Loading