""" Authentication router for login, logout, and token management """ from fastapi import APIRouter, Depends, status, Request from fastapi.responses import JSONResponse from sqlalchemy.ext.asyncio import AsyncSession import structlog from models import get_db from schemas.auth import ( LoginRequest, TokenResponse, LogoutResponse, RefreshTokenRequest, UserInfo ) from services.auth_service import AuthService from middleware.auth_middleware import ( get_current_user, get_client_ip, get_user_agent, require_viewer ) from models.user import User logger = structlog.get_logger() router = APIRouter( prefix="/api/v1/auth", tags=["authentication"] ) @router.post( "/login", response_model=TokenResponse, status_code=status.HTTP_200_OK, summary="User login", description="Authenticate with username and password to receive JWT tokens" ) async def login( request: Request, credentials: LoginRequest, db: AsyncSession = Depends(get_db) ): """ Authenticate user and return access and refresh tokens **Request Body:** - `username`: User's username - `password`: User's password **Response:** - `access_token`: JWT access token (short-lived) - `refresh_token`: JWT refresh token (long-lived) - `token_type`: Token type (always "bearer") - `expires_in`: Access token expiration in seconds - `user`: Authenticated user information **Audit Log:** - Creates audit log entry for login attempt (success or failure) """ auth_service = AuthService(db) # Get client IP and user agent for audit logging ip_address = get_client_ip(request) user_agent = get_user_agent(request) # Attempt login result = await auth_service.login( username=credentials.username, password=credentials.password, ip_address=ip_address, user_agent=user_agent ) if not result: logger.warning("login_endpoint_failed", username=credentials.username, ip=ip_address) return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={ "error": "Unauthorized", "message": "Invalid username or password" } ) logger.info("login_endpoint_success", username=credentials.username, user_id=result["user"]["id"], ip=ip_address) return result @router.post( "/logout", response_model=LogoutResponse, status_code=status.HTTP_200_OK, summary="User logout", description="Logout by blacklisting the current access token", dependencies=[Depends(require_viewer)] # Requires authentication ) async def logout( request: Request, db: AsyncSession = Depends(get_db) ): """ Logout user by blacklisting their access token **Authentication Required:** - Must include valid JWT access token in Authorization header **Response:** - `message`: Logout confirmation message **Audit Log:** - Creates audit log entry for logout """ # Extract token from Authorization header auth_header = request.headers.get("Authorization") if not auth_header: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={ "error": "Unauthorized", "message": "Authentication required" } ) # Extract token (remove "Bearer " prefix) token = auth_header.split()[1] if len(auth_header.split()) == 2 else None if not token: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={ "error": "Unauthorized", "message": "Invalid authorization header" } ) auth_service = AuthService(db) # Get client IP and user agent for audit logging ip_address = get_client_ip(request) user_agent = get_user_agent(request) # Perform logout success = await auth_service.logout( token=token, ip_address=ip_address, user_agent=user_agent ) if not success: logger.warning("logout_endpoint_failed", ip=ip_address) return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={ "error": "Unauthorized", "message": "Invalid or expired token" } ) user = get_current_user(request) logger.info("logout_endpoint_success", user_id=str(user.id) if user else None, username=user.username if user else None, ip=ip_address) return {"message": "Successfully logged out"} @router.post( "/refresh", status_code=status.HTTP_200_OK, summary="Refresh access token", description="Generate new access token using refresh token" ) async def refresh_token( request: Request, refresh_request: RefreshTokenRequest, db: AsyncSession = Depends(get_db) ): """ Generate new access token from refresh token **Request Body:** - `refresh_token`: Valid JWT refresh token **Response:** - `access_token`: New JWT access token - `token_type`: Token type (always "bearer") - `expires_in`: Access token expiration in seconds **Note:** - Refresh token is NOT rotated (same refresh token can be reused) - For security, consider implementing refresh token rotation in production """ auth_service = AuthService(db) # Get client IP for logging ip_address = get_client_ip(request) # Refresh token result = await auth_service.refresh_access_token( refresh_token=refresh_request.refresh_token, ip_address=ip_address ) if not result: logger.warning("refresh_endpoint_failed", ip=ip_address) return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={ "error": "Unauthorized", "message": "Invalid or expired refresh token" } ) logger.info("refresh_endpoint_success", ip=ip_address) return result @router.get( "/me", response_model=UserInfo, status_code=status.HTTP_200_OK, summary="Get current user", description="Get information about the currently authenticated user", dependencies=[Depends(require_viewer)] # Requires authentication ) async def get_me(request: Request): """ Get current authenticated user information **Authentication Required:** - Must include valid JWT access token in Authorization header **Response:** - User information (id, username, role, created_at, updated_at) **Note:** - Password hash is NEVER included in response """ user = get_current_user(request) if not user: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={ "error": "Unauthorized", "message": "Authentication required" } ) logger.info("get_me_endpoint", user_id=str(user.id), username=user.username) return { "id": str(user.id), "username": user.username, "role": user.role.value, "created_at": user.created_at, "updated_at": user.updated_at }