"""HTTP routes for auth: register, login, logout, me, refresh.""" from fastapi import APIRouter, Cookie, HTTPException, Response, status from starlette.requests import Request from app.config import settings from app.dependencies import AuthServiceDep, CurrentUserDep from app.main import limiter from app.models.auth import LoginRequest, RegisterRequest, TokenResponse, UserMeResponse from app.utils.errors import AuthError, ConflictError, UnauthorizedError router = APIRouter(prefix="/api/auth", tags=["auth"]) _REFRESH_COOKIE = "refresh_token" _ACCESS_EXPIRES_IN = settings.jwt_access_token_expire_minutes * 60 @router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED) @limiter.limit("10/minute") async def register( request: Request, payload: RegisterRequest, response: Response, service: AuthServiceDep ) -> TokenResponse: try: user, _pd = await service.register( email=str(payload.email), password=payload.password, display_name=payload.display_name, ) except ConflictError as exc: raise HTTPException(status.HTTP_409_CONFLICT, str(exc)) from exc access = service.create_access_token(user.id) refresh = service.create_refresh_token(user.id) _set_refresh_cookie(response, refresh) return TokenResponse(access_token=access, expires_in=_ACCESS_EXPIRES_IN) @router.post("/login", response_model=TokenResponse) @limiter.limit("10/minute") async def login( request: Request, payload: LoginRequest, response: Response, service: AuthServiceDep ) -> TokenResponse: try: user = await service.login(str(payload.email), payload.password) except AuthError as exc: raise HTTPException(status.HTTP_401_UNAUTHORIZED, str(exc)) from exc access = service.create_access_token(user.id) refresh = service.create_refresh_token(user.id) _set_refresh_cookie(response, refresh) return TokenResponse(access_token=access, expires_in=_ACCESS_EXPIRES_IN) @router.post("/logout", status_code=status.HTTP_204_NO_CONTENT) async def logout(response: Response) -> None: response.delete_cookie(_REFRESH_COOKIE) @router.post("/refresh", response_model=TokenResponse) async def refresh( response: Response, service: AuthServiceDep, refresh_token: str | None = Cookie(default=None, alias=_REFRESH_COOKIE), ) -> TokenResponse: if not refresh_token: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No refresh token") try: access = await service.refresh_access_token(refresh_token) except UnauthorizedError as exc: raise HTTPException(status.HTTP_401_UNAUTHORIZED, str(exc)) from exc return TokenResponse(access_token=access, expires_in=_ACCESS_EXPIRES_IN) @router.get("/me", response_model=UserMeResponse) async def me(current_user: CurrentUserDep) -> UserMeResponse: # PersonalData is fetched separately when workspace context is needed. # Here we return just the user-level data. return UserMeResponse( id=str(current_user.id), email=current_user.email, display_name="", # enriched by the frontend via workspace API workspace_id=None, # client fetches via /api/workspace after login ) def _set_refresh_cookie(response: Response, token: str) -> None: response.set_cookie( key=_REFRESH_COOKIE, value=token, httponly=True, secure=True, samesite="lax", max_age=settings.jwt_refresh_token_expire_days * 86400, )