"""Auth router: register, login, refresh, logout.""" import uuid from fastapi import APIRouter, HTTPException, status from jose import JWTError from ..models.auth import LoginRequest, RefreshRequest, RegisterRequest, TokenResponse from ..services.auth import ( authenticate_user, create_access_token, create_refresh_token, decode_token, register_user, revoke_refresh_token, store_refresh_token, validate_refresh_token_jti, ) router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/register", status_code=status.HTTP_201_CREATED) async def register(body: RegisterRequest) -> dict: try: user = await register_user(body.email, body.password) except ValueError as exc: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) return {"id": user["id"], "email": user["email"], "role": user["role"]} @router.post("/login", response_model=TokenResponse) async def login(body: LoginRequest) -> TokenResponse: user = await authenticate_user(body.email, body.password) if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials.", headers={"WWW-Authenticate": "Bearer"}, ) jti = str(uuid.uuid4()) await store_refresh_token(user["id"], jti) return TokenResponse( access_token=create_access_token(user["id"], user["email"], user["role"]), refresh_token=create_refresh_token(user["id"], jti), ) @router.post("/refresh", response_model=TokenResponse) async def refresh(body: RefreshRequest) -> TokenResponse: credentials_error = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired refresh token.", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = decode_token(body.refresh_token) except JWTError: raise credentials_error if payload.get("type") != "refresh": raise credentials_error user_id: str = payload.get("sub", "") jti: str = payload.get("jti", "") if not await validate_refresh_token_jti(jti, user_id): raise credentials_error # Rotate: revoke old JTI, issue new pair await revoke_refresh_token(jti) new_jti = str(uuid.uuid4()) await store_refresh_token(user_id, new_jti) from ..db import get_conn conn = get_conn() row = conn.execute( "SELECT email, role FROM users WHERE id = ?", [user_id] ).fetchone() if row is None: raise credentials_error return TokenResponse( access_token=create_access_token(user_id, row[0], row[1]), refresh_token=create_refresh_token(user_id, new_jti), ) @router.post("/logout", status_code=status.HTTP_204_NO_CONTENT) async def logout(body: RefreshRequest) -> None: try: payload = decode_token(body.refresh_token) except JWTError: return # Already invalid — treat as success jti = payload.get("jti", "") if jti: await revoke_refresh_token(jti)