# services/auth/app/api/auth.py - UPDATED TO RETURN TOKENS FROM REGISTRATION """ Authentication API routes - Updated to return tokens directly from registration Following industry best practices with unified token response format """ from fastapi import APIRouter, Depends, HTTPException, status, Request from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.ext.asyncio import AsyncSession from typing import Optional import structlog from app.core.database import get_db from app.schemas.auth import ( UserRegistration, UserLogin, TokenResponse, RefreshTokenRequest, UserResponse, PasswordChange, PasswordReset, TokenVerification ) from app.services.auth_service import AuthService from app.core.security import SecurityManager from shared.monitoring.decorators import track_execution_time logger = structlog.get_logger() router = APIRouter() security = HTTPBearer(auto_error=False) def get_metrics_collector(request: Request): """Get metrics collector from app state""" return getattr(request.app.state, 'metrics_collector', None) # ================================================================ # AUTHENTICATION ENDPOINTS # ================================================================ @router.post("/register", response_model=TokenResponse) @track_execution_time("registration_duration_seconds", "auth-service") async def register( user_data: UserRegistration, request: Request, db: AsyncSession = Depends(get_db) ): """Register a new user and return tokens directly""" metrics = get_metrics_collector(request) try: # Validate password strength if not SecurityManager.validate_password(user_data.password): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Password does not meet security requirements" ) # Create user and generate tokens in one operation result = await AuthService.register_user_with_tokens( email=user_data.email, password=user_data.password, full_name=user_data.full_name, db=db ) # Record successful registration if metrics: metrics.increment_counter("registration_total", labels={"status": "success"}) logger.info(f"User registration with tokens successful: {user_data.email}") return TokenResponse(**result) except HTTPException as e: if metrics: metrics.increment_counter("registration_total", labels={"status": "failed"}) logger.warning(f"Registration failed for {user_data.email}: {e.detail}") raise except Exception as e: if metrics: metrics.increment_counter("registration_total", labels={"status": "error"}) logger.error(f"Registration error for {user_data.email}: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Registration failed" ) @router.post("/login", response_model=TokenResponse) @track_execution_time("login_duration_seconds", "auth-service") async def login( login_data: UserLogin, request: Request, db: AsyncSession = Depends(get_db) ): """Login user and return tokens""" metrics = get_metrics_collector(request) try: # Check login attempts (rate limiting) #attempts = await SecurityManager.get_login_attempts(login_data.email) #if attempts >= 5: # raise HTTPException( # status_code=status.HTTP_429_TOO_MANY_REQUESTS, # detail="Too many login attempts. Please try again later." # ) # Attempt login result = await AuthService.login(login_data.email, login_data.password, db) # Clear login attempts on success await SecurityManager.clear_login_attempts(login_data.email) # Record successful login if metrics: metrics.increment_counter("login_success_total") logger.info(f"Login successful for {login_data.email}") return TokenResponse(**result) except HTTPException as e: # Increment login attempts on failure await SecurityManager.increment_login_attempts(login_data.email) # Record failed login if metrics: metrics.increment_counter("login_failure_total", labels={"reason": "auth_failed"}) logger.warning(f"Login failed for {login_data.email}: {e.detail}") raise except Exception as e: # Record login error if metrics: metrics.increment_counter("login_failure_total", labels={"reason": "error"}) logger.error(f"Login error for {login_data.email}: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Login failed" ) @router.post("/refresh", response_model=TokenResponse) @track_execution_time("token_refresh_duration_seconds", "auth-service") async def refresh_token( refresh_data: RefreshTokenRequest, request: Request, db: AsyncSession = Depends(get_db) ): """Refresh access token""" metrics = get_metrics_collector(request) try: result = await AuthService.refresh_access_token(refresh_data.refresh_token, db) # Record successful refresh if metrics: metrics.increment_counter("token_refresh_success_total") logger.info("Token refresh successful") return TokenResponse(**result) except HTTPException as e: if metrics: metrics.increment_counter("token_refresh_failure_total") logger.warning(f"Token refresh failed: {e.detail}") raise except Exception as e: if metrics: metrics.increment_counter("token_refresh_failure_total") logger.error(f"Token refresh error: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Token refresh failed" ) @router.post("/verify", response_model=TokenVerification) @track_execution_time("token_verification_duration_seconds", "auth-service") async def verify_token( credentials: HTTPAuthorizationCredentials = Depends(security), request: Request = None ): """Verify access token""" metrics = get_metrics_collector(request) if request else None if not credentials: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required" ) try: result = await AuthService.verify_user_token(credentials.credentials) if metrics: metrics.increment_counter("token_verify_success_total") return TokenVerification( valid=True, user_id=result.get("user_id"), email=result.get("email"), exp=result.get("exp") ) except HTTPException as e: if metrics: metrics.increment_counter("token_verify_failure_total") raise except Exception as e: if metrics: metrics.increment_counter("token_verify_failure_total") logger.error(f"Token verification error: {e}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token" ) @router.post("/logout") @track_execution_time("logout_duration_seconds", "auth-service") async def logout( refresh_data: RefreshTokenRequest, request: Request, db: AsyncSession = Depends(get_db) ): """Logout user by revoking refresh token""" metrics = get_metrics_collector(request) try: success = await AuthService.logout(refresh_data.refresh_token, db) if metrics: status_label = "success" if success else "failed" metrics.increment_counter("logout_total", labels={"status": status_label}) return {"message": "Logout successful" if success else "Logout failed"} except Exception as e: if metrics: metrics.increment_counter("logout_total", labels={"status": "error"}) logger.error(f"Logout error: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Logout failed" ) # ================================================================ # PASSWORD MANAGEMENT ENDPOINTS # ================================================================ @router.post("/change-password") async def change_password( password_data: PasswordChange, credentials: HTTPAuthorizationCredentials = Depends(security), request: Request = None, db: AsyncSession = Depends(get_db) ): """Change user password""" metrics = get_metrics_collector(request) if request else None try: if not credentials: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required" ) # Verify current token payload = await AuthService.verify_user_token(credentials.credentials) user_id = payload.get("user_id") if not user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token" ) # Validate new password if not SecurityManager.validate_password(password_data.new_password): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="New password does not meet security requirements" ) # Change password logic would go here # This is a simplified version - you'd need to implement the actual password change in AuthService # Record password change if metrics: metrics.increment_counter("password_change_total", labels={"status": "success"}) logger.info(f"Password changed for user: {user_id}") return {"message": "Password changed successfully"} except HTTPException: raise except Exception as e: # Record password change error if metrics: metrics.increment_counter("password_change_total", labels={"status": "error"}) logger.error(f"Password change error: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Password change failed" ) @router.post("/reset-password") async def reset_password( reset_data: PasswordReset, request: Request, db: AsyncSession = Depends(get_db) ): """Request password reset""" metrics = get_metrics_collector(request) try: # Password reset logic would go here # This is a simplified version - you'd need to implement email sending, etc. # Record password reset request if metrics: metrics.increment_counter("password_reset_total", labels={"status": "requested"}) logger.info(f"Password reset requested for: {reset_data.email}") return {"message": "Password reset email sent if account exists"} except Exception as e: # Record password reset error if metrics: metrics.increment_counter("password_reset_total", labels={"status": "error"}) logger.error(f"Password reset error: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Password reset failed" ) # ================================================================ # HEALTH AND STATUS ENDPOINTS # ================================================================ @router.get("/health") async def health_check(): """Health check endpoint""" return { "status": "healthy", "service": "auth-service", "version": "1.0.0" }