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
3 changes: 3 additions & 0 deletions app/db/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from sqlalchemy.orm import declarative_base

Base = declarative_base()
3 changes: 1 addition & 2 deletions app/db/models/orders.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Numeric, func
from sqlalchemy.orm import relationship, Session
from sqlalchemy.orm import declarative_base

Base = declarative_base()
from app.db.base import Base

class Order(Base):
__tablename__ = "orders"
Expand Down
68 changes: 57 additions & 11 deletions app/db/models/users.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from sqlalchemy import Column, Integer, String, DateTime, func
from sqlalchemy.orm import relationship, Session
from sqlalchemy.orm import declarative_base

Base = declarative_base()
from app.db.session import engine
from app.db.models.orders import Order
from app.db.base import Base
from fastapi import HTTPException, status
from app.validators.users import UserUpdate

class User(Base):
__tablename__ = "users"
Expand All @@ -11,20 +13,64 @@ class User(Base):
username = Column(String(255), unique=True, nullable=False)
email = Column(String(255), unique=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
role = Column(String(50), default="customer")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
role = Column(String(50), nullable=False, default="customer")
created_at = Column(DateTime, server_default=func.now(), nullable=False)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)

orders = relationship(Order, back_populates="user", cascade="all, delete-orphan")


def create_tables():
Base.metadata.create_all(bind=engine)

orders = relationship("Order", back_populates="user", cascade="all, delete-orphan")

# --- DB interaction functions ---

def get_user_by_id(db: Session, user_id: int):
def get_user_by_id(db: Session, user_id: int) -> User | None:
return db.query(User).filter(User.id == user_id).first()

def create_user(db: Session, username: str, email: str, hashed_password: str, role: str = "customer"):
user = User(username=username, email=email, hashed_password=hashed_password, role=role)
def get_user_by_email(db: Session, email: str):
return db.query(User).filter(User.email == email).first()

def get_user_by_username(db: Session, username: str):
return db.query(User).filter(User.username == username).first()

def create_user(db: Session, username: str, email: str, hashed_password: str, role: str = "customer") -> User:
user = User(
username=username,
email=email,
hashed_password=hashed_password,
role=role
)
db.add(user)
db.commit()
db.refresh(user)
return user
return user

def get_all_users(db: Session):
return db.query(User).all()

def delete_user_by_id(db: Session, user_id: int):
user = db.query(User).filter(User.id == user_id).first()
if user:
db.delete(user)
db.commit()

def update_user_db(db: Session, user_id: int, user_update: UserUpdate):
user = db.query(User).filter(User.id == user_id).first()

if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {user_id} not found"
)

if user_update.username is not None:
user.username = user_update.username
if user_update.email is not None:
user.email = user_update.email

db.commit()
db.refresh(user)
return user

28 changes: 28 additions & 0 deletions app/db/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# app/database/session.py

import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from typing import Generator

# Load environment variables from .env file
load_dotenv()

DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_USER = os.getenv("POSTGRES_USER", "postgres")
DB_PASSWORD = os.getenv("POSTGRES_PASSWORD", "password")
DB_NAME = os.getenv("POSTGRES_DB", "db_name")

DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"

engine = create_engine(DATABASE_URL, echo=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()
22 changes: 22 additions & 0 deletions app/routers/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.services.auth import authenticate_user, create_token_for_user

router = APIRouter()

@router.post("/token")
def login_for_access_token(
db: Session = Depends(get_db),
form_data: OAuth2PasswordRequestForm = Depends()
):
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
token = create_token_for_user(user)
return {"access_token": token, "token_type": "bearer"}
184 changes: 151 additions & 33 deletions app/routers/users.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,164 @@
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List, Optional
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.validators.users import UserCreate, UserUpdate, UserOut, TokenData
from app.services.users import (
create_user_service,
list_users_service,
get_user_service,
delete_user_service,
update_user_service,
)
from typing import List
from app.utils.jwt import get_current_user, admin_required

router = APIRouter()

# --- User Endpoints ---
@router.post("/", summary="Create a new user (Admin only)", response_model=UserOut)
def create_user(
user_in: UserCreate,
db: Session = Depends(get_db),
current_user: TokenData = Depends(admin_required),
):
try:
user = create_user_service(db, user_in)
return user
except ValueError as ve:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve))
except Exception as e:
print(f"Unexpected error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unexpected error occurred.",
)

@router.post("/", summary="Create a new user (Admin only)")
def create_user(user_in: dict):
# Only Admins allowed
pass

@router.get("/", summary="List all users (Admin only)")
def list_users():
# Only Admins allowed
pass
@router.get("/", summary="List all users (Admin only)", response_model=List[UserOut])
def list_users(
db: Session = Depends(get_db), current_user: TokenData = Depends(admin_required)
):
try:
users = list_users_service(db)
return users
except Exception as e:
print(f"Unexpected error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unexpected error occurred.",
)

@router.get("/me", summary="Get current user's profile")
def get_me():
# Customer only
pass

@router.put("/me", summary="Update current user's profile")
def update_me(user_update: dict):
# Customer only
pass
@router.get("/me", summary="Get current user's profile", response_model=UserOut)
def get_me(
db: Session = Depends(get_db), current_user: TokenData = Depends(get_current_user)
):
try:
user = get_user_service(db, current_user.user_id)
return user
except HTTPException as e:
raise e
except Exception as e:
print(f"Unexpected error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unexpected error occurred.",
)

@router.get("/{user_id}", summary="Get user by ID")
def get_user(user_id: int):
# Admin or Customer (own)
pass

@router.put("/{user_id}", summary="Update user by ID")
def update_user(user_id: int, user_update: dict):
# Admin or Customer (own)
pass
@router.put("/me", summary="Update current user's profile", response_model=UserOut)
def update_me(
user_update: UserUpdate,
db: Session = Depends(get_db),
current_user: TokenData = Depends(get_current_user),
):
try:
user = update_user_service(db, current_user.user_id, user_update)
return user
except HTTPException as e:
raise e
except Exception as e:
print(f"Unexpected error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unexpected error occurred.",
)

@router.delete("/{user_id}", summary="Delete user by ID")
def delete_user(user_id: int):
# Admin only
pass

@router.get("/{user_id}", summary="Get user by ID", response_model=UserOut)
def get_user(
user_id: int,
db: Session = Depends(get_db),
current_user: TokenData = Depends(get_current_user),
):
# Admin or user themself allowed
if current_user.role != "admin" and current_user.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied.",
)
try:
user = get_user_service(db, user_id)
return user
except HTTPException as e:
raise e
except Exception as e:
print(f"Unexpected error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unexpected error occurred.",
)


@router.put("/{user_id}", summary="Update user by ID", response_model=UserOut)
def update_user(
user_id: int,
user_update: UserUpdate,
db: Session = Depends(get_db),
current_user: TokenData = Depends(get_current_user),
):
# Admin or user themself allowed
if current_user.role != "admin" and current_user.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied.",
)
try:
user = update_user_service(db, user_id, user_update)
return user
except HTTPException as e:
raise e
except Exception as e:
print(f"Unexpected error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unexpected error occurred.",
)


@router.delete("/{user_id}", summary="Delete user by ID", status_code=200)
def delete_user(
user_id: int,
db: Session = Depends(get_db),
current_user: TokenData = Depends(admin_required),
):
try:
delete_user_service(db, user_id)
return JSONResponse(
content={"message": f"User with ID {user_id} deleted successfully."}
)
except HTTPException as e:
raise e
except Exception as e:
print(f"Unexpected error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unexpected error occurred.",
)

@router.get("/{user_id}/orders", summary="List orders by user (Admin only)")
def list_user_orders(user_id: int):
# Admin only
pass
def list_user_orders(
user_id: int, current_user: TokenData = Depends(admin_required)
):
# Implement orders fetching logic here, admin only
pass
26 changes: 26 additions & 0 deletions app/services/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from passlib.context import CryptContext
from datetime import timedelta
from app.utils.jwt import create_access_token
from app.services.users import get_user_by_username

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)

def authenticate_user(db, username: str, password: str):
user = get_user_by_username(db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user

def create_token_for_user(user):
token_data = {"user_id": user.id, "role": user.role}
access_token_expires = timedelta(minutes=60)
token = create_access_token(token_data, expires_delta=access_token_expires)
return token
Loading