from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from app.core.config import settings from app.core.database import get_db from app.core.security import create_access_token, create_token_payload from app.core.redis import get_redis from app.core.rate_limiter import limiter from app.models.user import User from app.models.audit_log import AuditAction from app.schemas.auth import LoginRequest, LoginResponse, UserInfo from app.services.auth_client import ( verify_credentials, AuthAPIError, AuthAPIConnectionError, ) from app.services.audit_service import AuditService from app.middleware.auth import get_current_user router = APIRouter() @router.post("/login", response_model=LoginResponse) @limiter.limit("5/minute") async def login( request: Request, login_request: LoginRequest, db: Session = Depends(get_db), redis_client=Depends(get_redis), ): """ Authenticate user via external API and return JWT token. """ # Prepare metadata for audit logging client_ip = request.client.host if request.client else "unknown" user_agent = request.headers.get("user-agent", "unknown") try: # Verify credentials with external API auth_result = await verify_credentials(login_request.email, login_request.password) except AuthAPIConnectionError: # Log failed login attempt due to service unavailable AuditService.log_event( db=db, event_type="user.login_failed", resource_type="user", action=AuditAction.LOGIN, user_id=None, resource_id=None, changes={"email": login_request.email, "reason": "auth_service_unavailable"}, request_metadata={"ip_address": client_ip, "user_agent": user_agent}, ) db.commit() raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Authentication service temporarily unavailable", ) except AuthAPIError as e: # Log failed login attempt due to invalid credentials AuditService.log_event( db=db, event_type="user.login_failed", resource_type="user", action=AuditAction.LOGIN, user_id=None, resource_id=None, changes={"email": login_request.email, "reason": "invalid_credentials"}, request_metadata={"ip_address": client_ip, "user_agent": user_agent}, ) db.commit() raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", ) # Find or create user in local database user = db.query(User).filter(User.email == login_request.email).first() # Get name from auth API response (nested in data.userInfo.name) user_info = auth_result.get("data", {}).get("userInfo", {}) auth_name = user_info.get("name", login_request.email.split("@")[0]) if not user: # Create new user based on auth API response user = User( email=login_request.email, name=auth_name, is_active=True, ) db.add(user) db.commit() db.refresh(user) else: # Sync user name from external auth system on each login if user.name != auth_name: user.name = auth_name db.commit() db.refresh(user) if not user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled", ) # Get role name role_name = user.role.name if user.role else None # Create token payload token_data = create_token_payload( user_id=user.id, email=user.email, role=role_name, department_id=user.department_id, is_system_admin=user.is_system_admin, ) # Create access token access_token = create_access_token(token_data) # Store session in Redis (sync with JWT expiry) redis_client.setex( f"session:{user.id}", settings.JWT_EXPIRE_MINUTES * 60, # Convert to seconds access_token, ) # Log successful login AuditService.log_event( db=db, event_type="user.login", resource_type="user", action=AuditAction.LOGIN, user_id=user.id, resource_id=user.id, changes=None, request_metadata={"ip_address": client_ip, "user_agent": user_agent}, ) db.commit() return LoginResponse( access_token=access_token, user=UserInfo( id=user.id, email=user.email, name=user.name, role=role_name, department_id=user.department_id, is_system_admin=user.is_system_admin, ), ) @router.post("/logout") async def logout( current_user: User = Depends(get_current_user), redis_client=Depends(get_redis), ): """ Logout user and invalidate session. """ # Remove session from Redis redis_client.delete(f"session:{current_user.id}") return {"detail": "Successfully logged out"} @router.get("/me", response_model=UserInfo) async def get_current_user_info( current_user: User = Depends(get_current_user), ): """ Get current authenticated user information. """ role_name = current_user.role.name if current_user.role else None return UserInfo( id=current_user.id, email=current_user.email, name=current_user.name, role=role_name, department_id=current_user.department_id, is_system_admin=current_user.is_system_admin, )