Spaces:
Running
Running
| """ | |
| Main FastAPI application for AUTH Microservice. | |
| """ | |
| import time | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, Request, status | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse | |
| from fastapi.exceptions import RequestValidationError | |
| from pydantic import ValidationError, BaseModel | |
| from typing import Optional, List, Dict, Any | |
| from jose import JWTError | |
| from pymongo.errors import PyMongoError, ConnectionFailure, OperationFailure | |
| from app.core.config import settings | |
| from app.core.logging import setup_logging, get_logger | |
| from app.nosql import connect_to_mongo, close_mongo_connection | |
| from app.system_users.controllers.router import router as system_user_router | |
| from app.auth.controllers.router import router as auth_router | |
| from app.auth.controllers.staff_router import router as staff_router | |
| from app.auth.controllers.customer_router import router as customer_router | |
| from app.auth.controllers.service_professional_router import router as service_professional_router | |
| from app.internal.router import router as internal_router | |
| # Setup logging | |
| setup_logging( | |
| log_level=settings.LOG_LEVEL if hasattr(settings, 'LOG_LEVEL') else "INFO", | |
| log_dir=settings.LOG_DIR if hasattr(settings, 'LOG_DIR') else "logs", | |
| ) | |
| logger = get_logger(__name__) | |
| # Standard error response models | |
| class ErrorDetail(BaseModel): | |
| """Detailed error information""" | |
| field: Optional[str] = None | |
| message: str | |
| type: Optional[str] = None | |
| class ErrorResponse(BaseModel): | |
| """Standard error response format""" | |
| success: bool = False | |
| error: str | |
| detail: str | |
| errors: Optional[List[ErrorDetail]] = None | |
| request_id: Optional[str] = None | |
| async def lifespan(app: FastAPI): | |
| """Manage application lifespan events""" | |
| # Startup | |
| logger.info("Starting AUTH Microservice") | |
| await connect_to_mongo() | |
| # Initialize customer authentication collections | |
| try: | |
| from app.auth.db_init_customer import init_customer_auth_collections | |
| await init_customer_auth_collections() | |
| logger.info("Customer authentication collections initialized") | |
| except Exception as e: | |
| logger.error(f"Failed to initialize customer auth collections: {e}") | |
| logger.info("AUTH Microservice started successfully") | |
| yield | |
| # Shutdown | |
| logger.info("Shutting down AUTH Microservice") | |
| await close_mongo_connection() | |
| logger.info("AUTH Microservice shut down successfully") | |
| import os | |
| # Create FastAPI app | |
| app = FastAPI( | |
| title="AUTH Microservice", | |
| description="Authentication & Authorization System - User Management, Login, JWT Tokens & Security", | |
| version="1.0.0", | |
| docs_url="/docs", | |
| redoc_url="/redoc", | |
| lifespan=lifespan, | |
| root_path=os.getenv("ROOT_PATH", "") | |
| ) | |
| # Request logging middleware | |
| async def log_requests(request: Request, call_next): | |
| """Log all incoming requests and responses with timing.""" | |
| request_id = str(id(request)) | |
| start_time = time.time() | |
| # Log request | |
| logger.info( | |
| f"Request started: {request.method} {request.url.path}", | |
| extra={ | |
| "request_id": request_id, | |
| "method": request.method, | |
| "path": request.url.path, | |
| "client": request.client.host if request.client else None, | |
| "user_agent": request.headers.get("User-Agent", "")[:100] | |
| } | |
| ) | |
| # Process request | |
| try: | |
| response = await call_next(request) | |
| # Calculate processing time | |
| process_time = time.time() - start_time | |
| # Log response | |
| logger.info( | |
| f"Request completed: {request.method} {request.url.path} - Status: {response.status_code}", | |
| extra={ | |
| "request_id": request_id, | |
| "method": request.method, | |
| "path": request.url.path, | |
| "status_code": response.status_code, | |
| "process_time": f"{process_time:.3f}s" | |
| } | |
| ) | |
| # Add custom headers | |
| response.headers["X-Process-Time"] = f"{process_time:.3f}" | |
| response.headers["X-Request-ID"] = request_id | |
| return response | |
| except Exception as e: | |
| process_time = time.time() - start_time | |
| logger.error( | |
| f"Request failed: {request.method} {request.url.path} - Error: {str(e)}", | |
| exc_info=True, | |
| extra={ | |
| "request_id": request_id, | |
| "method": request.method, | |
| "path": request.url.path, | |
| "process_time": f"{process_time:.3f}s" | |
| } | |
| ) | |
| raise | |
| # CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=settings.CORS_ORIGINS, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Global exception handlers | |
| async def validation_exception_handler(request: Request, exc: RequestValidationError): | |
| """Handle request validation errors with detailed field information.""" | |
| errors = [] | |
| for error in exc.errors(): | |
| field = " -> ".join(str(loc) for loc in error["loc"]) | |
| errors.append({ | |
| "field": field, | |
| "message": error["msg"], | |
| "type": error["type"] | |
| }) | |
| logger.warning(f"Validation error on {request.url.path}: {errors}") | |
| return JSONResponse( | |
| status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, | |
| content={ | |
| "success": False, | |
| "error": "Validation Error", | |
| "detail": "The request contains invalid data", | |
| "errors": errors | |
| } | |
| ) | |
| async def pydantic_validation_exception_handler(request: Request, exc: ValidationError): | |
| """Handle Pydantic validation errors.""" | |
| logger.warning(f"Pydantic validation error on {request.url.path}: {exc.errors()}") | |
| return JSONResponse( | |
| status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, | |
| content={ | |
| "success": False, | |
| "error": "Data Validation Error", | |
| "detail": str(exc), | |
| "errors": exc.errors() | |
| } | |
| ) | |
| async def jwt_exception_handler(request: Request, exc: JWTError): | |
| """Handle JWT token errors.""" | |
| logger.warning(f"JWT error on {request.url.path}: {str(exc)}") | |
| return JSONResponse( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| content={ | |
| "success": False, | |
| "error": "Authentication Error", | |
| "detail": "Invalid or expired token", | |
| "headers": {"WWW-Authenticate": "Bearer"} | |
| } | |
| ) | |
| async def mongodb_exception_handler(request: Request, exc: PyMongoError): | |
| """Handle MongoDB errors.""" | |
| logger.error(f"MongoDB error on {request.url.path}: {str(exc)}", exc_info=True) | |
| if isinstance(exc, ConnectionFailure): | |
| return JSONResponse( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| content={ | |
| "success": False, | |
| "error": "Database Connection Error", | |
| "detail": "Unable to connect to the database. Please try again later." | |
| } | |
| ) | |
| elif isinstance(exc, OperationFailure): | |
| return JSONResponse( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| content={ | |
| "success": False, | |
| "error": "Database Operation Error", | |
| "detail": "A database operation failed. Please contact support if the issue persists." | |
| } | |
| ) | |
| else: | |
| return JSONResponse( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| content={ | |
| "success": False, | |
| "error": "Database Error", | |
| "detail": "An unexpected database error occurred." | |
| } | |
| ) | |
| async def general_exception_handler(request: Request, exc: Exception): | |
| """Handle all uncaught exceptions.""" | |
| logger.error( | |
| f"Unhandled exception on {request.method} {request.url.path}: {str(exc)}", | |
| exc_info=True, | |
| extra={ | |
| "method": request.method, | |
| "path": request.url.path, | |
| "client": request.client.host if request.client else None | |
| } | |
| ) | |
| return JSONResponse( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| content={ | |
| "success": False, | |
| "error": "Internal Server Error", | |
| "detail": "An unexpected error occurred. Please try again later.", | |
| "request_id": id(request) # Include request ID for tracking | |
| } | |
| ) | |
| # Health check endpoint | |
| async def health_check(): | |
| """ | |
| Health check endpoint. | |
| Returns the service status and version. | |
| """ | |
| try: | |
| return { | |
| "status": "healthy", | |
| "service": "auth-microservice", | |
| "version": "1.0.0" | |
| } | |
| except Exception as e: | |
| logger.error(f"Health check failed: {e}") | |
| return JSONResponse( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| content={ | |
| "status": "unhealthy", | |
| "service": "auth-microservice", | |
| "error": str(e) | |
| } | |
| ) | |
| # Debug endpoint to check database status | |
| async def check_db_status(): | |
| """ | |
| Check database connection and user count. | |
| Returns information about database collections and sample data. | |
| Raises: | |
| HTTPException: 500 - Database connection error | |
| """ | |
| try: | |
| from app.nosql import get_database | |
| from app.constants.collections import AUTH_SYSTEM_USERS_COLLECTION, AUTH_ACCESS_ROLES_COLLECTION, SCM_ACCESS_ROLES_COLLECTION | |
| db = get_database() | |
| if db is None: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Database connection not available" | |
| ) | |
| users_count = await db[AUTH_SYSTEM_USERS_COLLECTION].count_documents({}) | |
| roles_count = await db[AUTH_ACCESS_ROLES_COLLECTION].count_documents({}) | |
| scm_roles_count = await db[SCM_ACCESS_ROLES_COLLECTION].count_documents({}) | |
| # Get sample user to verify | |
| sample_user = await db[AUTH_SYSTEM_USERS_COLLECTION].find_one( | |
| {"email": "superadmin@cuatrolabs.com"}, | |
| {"email": 1, "username": 1, "role": 1, "status": 1, "_id": 0} | |
| ) | |
| return { | |
| "status": "connected", | |
| "database": settings.MONGODB_DB_NAME, | |
| "collections": { | |
| "users": users_count, | |
| "auth_roles": roles_count, | |
| "scm_roles": scm_roles_count | |
| }, | |
| "superadmin_exists": sample_user is not None, | |
| "sample_user": sample_user if sample_user else None | |
| } | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Database status check failed: {str(e)}", exc_info=True) | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"Failed to check database status: {str(e)}" | |
| ) | |
| # Include routers with new organization | |
| app.include_router(auth_router) # /auth/* - Core authentication endpoints | |
| app.include_router(staff_router) # /staff/* - Staff authentication (mobile OTP) | |
| app.include_router(customer_router) # /customer/* - Customer authentication (OTP) | |
| app.include_router(service_professional_router) # /service-professional/* - Service professional authentication (OTP) | |
| app.include_router(system_user_router) # /users/* - User management endpoints | |
| app.include_router(internal_router) # /internal/* - Internal API endpoints | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run( | |
| "app.main:app", | |
| host="0.0.0.0", | |
| port=8002, | |
| reload=True, | |
| log_level="info" | |
| ) | |