from collections.abc import Awaitable, Callable import logging from fastapi import FastAPI, HTTPException, Request from sqlalchemy import text try: # For module mode: `uvicorn app.main:app` from app.db import SessionLocal from app.security import auth_error_response, decode_access_token except ModuleNotFoundError: # For script mode: `python app/main.py` or VS Code "Run Python File" from db import SessionLocal from security import auth_error_response, decode_access_token EXCLUDED_PATH_PREFIXES = ( "/health", "/docs", "/redoc", "/openapi.json", "/auth/login", ) logger = logging.getLogger("app.auth") def _is_excluded_path(path: str) -> bool: for prefix in EXCLUDED_PATH_PREFIXES: if path == prefix or path.startswith(f"{prefix}/"): return True return False def install_auth_middleware(app: FastAPI) -> None: @app.middleware("http") async def auth_middleware( request: Request, call_next: Callable[[Request], Awaitable], ): path = request.url.path if request.method == "OPTIONS" or _is_excluded_path(path): return await call_next(request) auth_header = request.headers.get("Authorization") if not auth_header: logger.warning( "auth failed: missing header", extra={ "event": "auth_check", "request_id": getattr(request.state, "request_id", None), "path": path, "reason": "missing_authorization_header", }, ) return auth_error_response("missing authorization header") parts = auth_header.split(" ", 1) if len(parts) != 2 or parts[0].lower() != "bearer": logger.warning( "auth failed: invalid header format", extra={ "event": "auth_check", "request_id": getattr(request.state, "request_id", None), "path": path, "reason": "invalid_authorization_format", }, ) return auth_error_response("invalid authorization format") try: user_id = decode_access_token(parts[1].strip()) except HTTPException: logger.warning( "auth failed: invalid token", extra={ "event": "auth_check", "request_id": getattr(request.state, "request_id", None), "path": path, "reason": "invalid_or_expired_token", }, ) return auth_error_response("invalid or expired access token") with SessionLocal() as db: user_row = ( db.execute( text( """ SELECT id, status FROM chat_user WHERE id = :user_id LIMIT 1 """ ), {"user_id": user_id}, ) .mappings() .first() ) if not user_row or int(user_row["status"]) != 1: logger.warning( "auth failed: user unavailable", extra={ "event": "auth_check", "request_id": getattr(request.state, "request_id", None), "path": path, "user_id": user_id, "reason": "user_not_found_or_disabled", }, ) return auth_error_response("user not found or disabled") request.state.user_id = user_id return await call_next(request)