Files
lifetrack/apps/api/app/core/dependencies.py
T
MeeJayandClaude Opus 5 93f0689c1e Initial import: LifeTrack v1 (santé, vape, finances)
Tracker de vie auto-hébergé : suivi poids/calories/sport avec planning de
pesées, sevrage tabac (vape) avec modèle de coût DIY et économies, et
finances personnelles avec import de relevés bancaires.

Architecture : FastAPI + SQLAlchemy 2.0 + PostgreSQL 16, React 18 + TS +
Vite + Tailwind + ECharts, déploiement Docker Compose. Modules
auto-découverts des deux côtés (pkgutil / import.meta.glob) et framework
de connecteurs à deux voies (importeurs de fichiers + ingestion JSON)
pour brancher de nouvelles sources sans toucher au noyau.

Validé : 292 tests pytest, tsc + vite build, contrat API/web vérifié
contre le schéma OpenAPI, et déploiement Docker réel sur PostgreSQL 16
(28 tables, SPA servie par nginx, wizard de premier démarrage).

Documentation : README.md, docs/GUIDE.md, CONVENTIONS.md, et les
documents de conception et de recherche dans docs/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 10:48:57 +02:00

82 lines
2.8 KiB
Python

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 <jwt> (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