from fastapi import APIRouter, Depends, HTTPException, Response, Cookie, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models.user import User from app.schemas.auth import UserCreate, UserLogin, UserResponse, TokenResponse from app.services.auth import ( hash_password, verify_password, create_access_token, create_refresh_token, decode_token, get_current_user, ) from app.config import get_settings router = APIRouter(prefix="/api/auth", tags=["auth"]) settings = get_settings() @router.post("/register", response_model=UserResponse, status_code=201) async def register(data: UserCreate, db: AsyncSession = Depends(get_db)): # Check existing existing = await db.execute( select(User).where((User.email == data.email) | (User.username == data.username)) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=409, detail="Email or username already registered") user = User( email=data.email, username=data.username, password_hash=hash_password(data.password), ) db.add(user) await db.commit() await db.refresh(user) return user @router.post("/login", response_model=TokenResponse) async def login(data: UserLogin, response: Response, db: AsyncSession = Depends(get_db)): result = await db.execute(select(User).where(User.email == data.email)) user = result.scalar_one_or_none() if not user or not verify_password(data.password, user.password_hash): raise HTTPException(status_code=401, detail="Invalid credentials") access_token = create_access_token(user.id) refresh_token = create_refresh_token(user.id) response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=False, # Set True in production with HTTPS samesite="lax", max_age=settings.refresh_token_expire_days * 86400, path="/api/auth", ) return TokenResponse(access_token=access_token) @router.post("/refresh", response_model=TokenResponse) async def refresh( response: Response, refresh_token: str | None = Cookie(default=None), db: AsyncSession = Depends(get_db), ): if not refresh_token: raise HTTPException(status_code=401, detail="No refresh token") payload = decode_token(refresh_token) if payload.get("type") != "refresh": raise HTTPException(status_code=401, detail="Invalid token type") user_id = int(payload["sub"]) result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=401, detail="User not found") new_access = create_access_token(user.id) new_refresh = create_refresh_token(user.id) response.set_cookie( key="refresh_token", value=new_refresh, httponly=True, secure=False, samesite="lax", max_age=settings.refresh_token_expire_days * 86400, path="/api/auth", ) return TokenResponse(access_token=new_access) @router.post("/logout") async def logout(response: Response): response.delete_cookie("refresh_token", path="/api/auth") return {"message": "Logged out"} @router.get("/me", response_model=UserResponse) async def me(user: User = Depends(get_current_user)): return user