"""Authentication router - login, register, token refresh.""" from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, EmailStr from jose import JWTError, jwt from passlib.context import CryptContext from app.config import settings from app.database import get_db from app.models.user import User from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") router = APIRouter() # --- Pydantic schemas --- class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" class RegisterRequest(BaseModel): username: str email: EmailStr password: str class LoginRequest(BaseModel): username: str password: str # --- Helpers --- def _create_access_token(data: dict) -> str: to_encode = data.copy() expire = datetime.now(timezone.utc) + timedelta( minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES ) to_encode.update({"exp": expire}) return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) def verify_password(plain_password: str, hashed_password: str) -> bool: """Verify a plain password against a hash.""" return pwd_context.verify(plain_password, hashed_password) def hash_password(password: str) -> str: """Hash a password.""" return pwd_context.hash(password) # --- Endpoints --- @router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED) async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)): """Register a new user and return an access token.""" # Check if username already exists result = await db.execute(select(User).where(User.username == req.username)) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Username already registered", ) # Check if email already exists result = await db.execute(select(User).where(User.email == req.email)) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Email already registered", ) user = User( username=req.username, email=req.email, hashed_password=hash_password(req.password), ) db.add(user) await db.commit() await db.refresh(user) access_token = _create_access_token({"sub": str(user.id)}) return TokenResponse(access_token=access_token) @router.post("/login", response_model=TokenResponse) async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)): """Authenticate user and return an access token.""" result = await db.execute(select(User).where(User.username == req.username)) user = result.scalar_one_or_none() if not user or not verify_password(req.password, user.hashed_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", ) if not user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="User account is inactive", ) access_token = _create_access_token({"sub": str(user.id)}) return TokenResponse(access_token=access_token) @router.get("/me") async def get_current_user( user_id: int, db: AsyncSession = Depends(get_db), ): """Get current user profile.""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") return { "id": user.id, "username": user.username, "email": user.email, "is_active": user.is_active, "created_at": user.created_at, }