from datetime import datetime, timedelta from typing import Optional import uuid from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel, EmailStr from sqlalchemy.orm import Session from app.config import settings from app.models.db import get_db, User router = APIRouter(prefix="/api/auth", tags=["auth"]) pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False) # ── Schemas ──────────────────────────────────────────────────────────────── class RegisterRequest(BaseModel): email: EmailStr password: str class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" class UserOut(BaseModel): id: str email: str created_at: datetime class Config: from_attributes = True # ── Helpers ──────────────────────────────────────────────────────────────── def hash_password(password: str) -> str: return pwd_context.hash(password) def verify_password(plain: str, hashed: str) -> bool: return pwd_context.verify(plain, hashed) def create_token(user_id) -> str: expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) return jwt.encode( {"sub": str(user_id), "exp": expire}, settings.SECRET_KEY, algorithm=settings.ALGORITHM, ) def get_current_user( token: Optional[str] = Depends(oauth2_scheme), db: Session = Depends(get_db), ) -> Optional[User]: if not token: return None try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) user_id_str: str = payload.get("sub") if not user_id_str: return None import uuid as _uuid user_id = _uuid.UUID(user_id_str) except (JWTError, ValueError): return None return db.query(User).filter(User.id == user_id).first() def require_user(current_user: Optional[User] = Depends(get_current_user)) -> User: if not current_user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return current_user # ── Endpoints ────────────────────────────────────────────────────────────── @router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED) def register(body: RegisterRequest, db: Session = Depends(get_db)): if db.query(User).filter(User.email == body.email).first(): raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email already registered") user = User( email=body.email, hashed_password=hash_password(body.password), ) db.add(user) db.commit() db.refresh(user) return user @router.post("/login", response_model=TokenResponse) def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = db.query(User).filter(User.email == form.username).first() if not user or not verify_password(form.password, user.hashed_password): raise HTTPException(status_code=400, detail="Incorrect email or password") return {"access_token": create_token(user.id)} @router.get("/me", response_model=UserOut) def me(current_user: User = Depends(require_user)): return current_user