from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app import __version__ from app.api.v1 import api_router, health from app.core.config import settings from app.core.logging import configure_logging, get_logger from shared.exceptions import AppError from shared.responses import ErrorDetail, ErrorResponse configure_logging() logger = get_logger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): logger.info("service_starting", extra={"service": settings.service_name}) yield def create_app() -> FastAPI: app = FastAPI( title="Identity & Access Service", version=__version__, description="سرویس احراز هویت و دسترسی (فاز ۲) — SSO با Keycloak", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origin_list, allow_origin_regex=r"https?://([a-z0-9-]+\.)*torbatyar\.ir", allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) def _cors_headers(request: Request) -> dict[str, str]: """هدرهای CORS برای پاسخ‌های خطا. هندلر خطای عمومی (۵۰۰) بیرون از CORSMiddleware اجرا می‌شود، بنابراین هدر CORS را دستی اضافه می‌کنیم تا خطاهای سرور در مرورگر به‌اشتباه «خطای CORS» نمایش داده نشوند و پیام واقعی به کاربر برسد. """ import re origin = request.headers.get("origin") if not origin: return {} if origin in settings.cors_origin_list or re.fullmatch( r"https?://([a-z0-9-]+\.)*torbatyar\.ir", origin ): return { "Access-Control-Allow-Origin": origin, "Vary": "Origin", } return {} @app.exception_handler(AppError) async def app_error_handler(request: Request, exc: AppError) -> JSONResponse: return JSONResponse( status_code=exc.status_code, content=ErrorResponse( error=ErrorDetail(code=exc.error_code, message=exc.message, details=exc.details) ).model_dump(), headers=_cors_headers(request), ) @app.exception_handler(Exception) async def unhandled_handler(request: Request, exc: Exception) -> JSONResponse: logger.error("unhandled_exception", extra={"error": str(exc)}, exc_info=exc) return JSONResponse( status_code=500, content=ErrorResponse( error=ErrorDetail(code="internal_error", message="خطای داخلی سرور") ).model_dump(), headers=_cors_headers(request), ) app.include_router(health.router) app.include_router(api_router, prefix=settings.api_v1_prefix) return app app = create_app()