from collections.abc import Callable from typing import TYPE_CHECKING, Annotated from fastapi import Depends, Header from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.orm import Session from app.core.database import get_db from app.core.errors import ForbiddenError, UnauthorizedError from app.core.security import decode_access_token, verify_device_key if TYPE_CHECKING: from app.modules.auth.models import User bearer = HTTPBearer(auto_error=False) INGEST_WILDCARD_SCOPE = "ingest:*" def get_current_user( credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)], db: Annotated[Session, Depends(get_db)], ) -> "User": """JWT Bearer -> User. Raises UnauthorizedError (401) otherwise.""" from app.modules.auth.models import User if credentials is None: raise UnauthorizedError("Authentification requise.") user_id = decode_access_token(credentials.credentials) user = db.get(User, user_id) if user is None or not user.is_active: raise UnauthorizedError("Session invalide ou expirée.") return user def authenticate_ingest( db: Session, credentials: HTTPAuthorizationCredentials | None, api_key: str | None, required_scope: str, ) -> "User": """Shared ingest authentication: JWT Bearer OR X-API-Key device key. A device key must hold `required_scope` (ex: "ingest:health") or the wildcard "ingest:*". Raises 401/403 accordingly. """ from app.modules.auth.models import User if api_key: key = verify_device_key(db, api_key) scopes = key.scopes or [] if required_scope not in scopes and INGEST_WILDCARD_SCOPE not in scopes: raise ForbiddenError( "Cette clé d'appareil ne dispose pas du droit requis.", details={"required_scope": required_scope}, ) user = db.get(User, key.user_id) if user is None or not user.is_active: raise UnauthorizedError("Clé d'appareil invalide.") return user if credentials is not None: return get_current_user(credentials, db) raise UnauthorizedError("Authentification requise.") def get_ingest_identity(required_scope: str) -> Callable[..., "User"]: """Factory dependency for ingest endpoints. Accepts EITHER: - Authorization: Bearer (interactive user), OR - X-API-Key: ltk_... (device key) """ def dependency( credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)], db: Annotated[Session, Depends(get_db)], x_api_key: Annotated[str | None, Header(alias="X-API-Key")] = None, ) -> "User": return authenticate_ingest(db, credentials, x_api_key, required_scope) return dependency