from fastapi import FastAPI, HTTPException, Request from fastapi.responses import JSONResponse from starlette.middleware.sessions import SessionMiddleware from app.core.settings import get_settings from app.core.middleware.context import ContextResolveMiddleware from app.core.middleware.domain_resolver import DomainResolverMiddleware from app.core.middleware.security_headers import SecurityHeadersMiddleware from app.core.middleware.csrf_guard import CsrfPostGuardMiddleware from app.core.startup import on_startup from app.core.api import api_router from app.core.http_responses import auth_required_response, forbidden_response, not_found_response, wants_json from app.ui.app import mount_ui def _register_error_handlers(app: FastAPI) -> None: @app.exception_handler(PermissionError) async def permission_error_handler(request: Request, exc: PermissionError): message = str(exc) or "Access denied" if "csrf" in message.lower(): return JSONResponse({"detail": "CSRF validation failed"}, status_code=403) if wants_json(request) else forbidden_response(request, "CSRF validation failed") if "not authenticated" in message.lower(): return auth_required_response(request) return forbidden_response(request, message) @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): detail = exc.detail if isinstance(exc.detail, str) else "Error" if request.url.path.startswith("/api"): return JSONResponse({"detail": exc.detail}, status_code=exc.status_code, headers=exc.headers) if exc.status_code == 401: return auth_required_response(request) if exc.status_code == 403: return forbidden_response(request, detail or "Access denied") if exc.status_code == 404: return not_found_response(request, detail or "Not found") return JSONResponse({"detail": exc.detail}, status_code=exc.status_code, headers=exc.headers) def create_app() -> FastAPI: s = get_settings() app = FastAPI(title=s.APP_NAME, debug=s.DEBUG) app.add_middleware(SecurityHeadersMiddleware) app.add_middleware(ContextResolveMiddleware) # Phase 7T.2: added after context so it resolves the request host before # context-aware middleware/routes need tenant/branch/domain state. app.add_middleware(DomainResolverMiddleware) # Runs after SessionMiddleware has populated request.session and before routes. # It only guards selected UI unsafe POST endpoints and does not change valid forms. app.add_middleware(CsrfPostGuardMiddleware) # SessionMiddleware is added last so it is available to downstream # middleware/routes in Starlette's middleware execution order. app.add_middleware( SessionMiddleware, secret_key=s.SECRET_KEY, session_cookie=s.COOKIE_SESSION_NAME, same_site=s.COOKIE_SAMESITE, https_only=s.COOKIE_SECURE, ) app.add_event_handler("startup", lambda: on_startup(app)) _register_error_handlers(app) app.include_router(api_router, prefix="/api") mount_ui(app) return app app = create_app()