Files
ENI-projet-piscine/apps/backend
Johan LEROY 1f6210698d feat(backend): fait tourner les jetons de rafraîchissement et détecte leur réutilisation
Le jeton de rafraîchissement est une chaîne opaque de 256 bits, jamais un
JWT. Il doit être révocable, donc sa ligne en base existe de toute façon,
et le JWT n'ajouterait qu'un second chemin de signature. Surtout, la
séparation d'avec le jeton d'accès devient structurelle : un JWT ne
figure dans aucune ligne, une chaîne opaque échoue au décodage. La
confusion refresh-vers-accès, qui transforme une fenêtre de 15 minutes en
fenêtre de 7 jours, est impossible même si quelqu'un oublie le test.

Seule l'empreinte SHA-256 est stockée. Pas d'Argon2 : l'entrée fait
256 bits de CSPRNG, aucun dictionnaire ne l'atteint, et une KDF coûterait
17 ms à chaque rafraîchissement.

La rotation ne protège de rien par elle-même : elle rend la réutilisation
détectable, et c'est la détection qui termine le vol. Un jeton déjà
tourné révoque donc toute sa famille et laisse une trace dans
`audit_log` ; un jeton expiré, lui, ne révoque rien, ce n'est pas une
preuve de compromission. Les deux cas ont leur test.

La revendication est une seule instruction SQL avec RETURNING. Un SELECT
puis un UPDATE laisseraient une fenêtre où deux onglets réussissent la
même rotation ; le test d'intégration le prouve, ce qui est
indémontrable sur un double.

`expires_at` est absolu et hérité du prédécesseur : s'il glissait, la
promesse de sept jours serait fictive.

Corrige au passage un défaut trouvé par un test : une `HTTPException`
construit sa propre réponse, donc l'effacement du cookie posé sur la
`Response` injectée était perdu. Un navigateur gardait un cookie mort
après une détection de réutilisation.
2026-09-15 14:49:30 +02:00
..

Backend EnerVision

API FastAPI exposant les series temporelles energetiques.

Element Choix
Python 3.14
Gestionnaire uv (uv.lock fait foi)
Framework FastAPI + Uvicorn
Persistance SQLAlchemy 2 async + asyncpg + Alembic
Lint/format ruff
Typage mypy en mode strict
Tests pytest + pytest-asyncio + httpx

Installation

cp .env.example .env
uv sync --all-groups

APP_SECRET_KEY et DATABASE_URL n'ont pas de valeur par defaut : l'application refuse de demarrer sans elles.

DATABASE_URL pointe sur localhost:5433, le port publie par le service db du docker-compose.yml racine. Demarrer la base depuis la racine avec make db-up.

Commandes

Depuis la racine du monorepo, via le Makefile : make install, make dev, make lint, make format, make typecheck, make test, make check, make docker-build.

Directement depuis ce dossier :

uv run uvicorn app.main:create_app --factory --reload --port 8000
uv run ruff check .          # lint
uv run ruff format .         # format
uv run mypy app              # typage strict
uv run pytest                # tests + couverture
uv run pytest -m integration # tests exigeant une base joignable

Les conventions de tests, les gabarits et le detail des marqueurs sont dans TESTING.md.

pytest ecarte par defaut les tests marques integration, pour que make check reste jouable sans Docker. Ces tests visent la base enervision_test, creee par db/init/110-test-database.sql au premier demarrage du conteneur.

L'application est exposee par une factory (create_app) et non par un objet module : aucune configuration n'est lue a l'import, ce qui rend les tests et les migrations independants de l'environnement.

Structure

app/
├── api/
│   ├── deps.py          Dependances FastAPI partagees (session, settings)
│   └── v1/
│       ├── router.py    Agregation des routes de la version 1
│       └── endpoints/   Un module par ressource exposee
├── core/
│   ├── config.py        Settings Pydantic, source unique de configuration
│   └── logging.py       Journalisation console en local, JSON en production
├── db/
│   ├── base.py          Base declarative SQLAlchemy
│   └── session.py       Engine et sessions asynchrones
├── models/              Modeles SQLAlchemy
├── schemas/             Modeles Pydantic d'entree et de sortie
├── repositories/        Acces aux donnees, une classe par agregat
├── services/            Regles metier, orchestrent les repositories
└── main.py              Factory applicative
tests/                   Miroir de app/
alembic/                 Migrations du schema applicatif

Le sens de dependance est unique : endpoints vers services vers repositories vers models. Un endpoint ne touche jamais une session directement.

Routes

Route Role
/api/v1/health/live Sonde de vivacite, aucune dependance externe
/api/v1/health/ready Sonde de disponibilite, verifie la base et TimescaleDB
/metrics Metriques au format Prometheus
/docs, /openapi.json Documentation, desactivee quand APP_ENV=prod

Migrations

uv run alembic revision --autogenerate -m "libelle"
uv run alembic upgrade head

L'URL de connexion vient de DATABASE_URL, pas de alembic.ini.

La premiere revision ne cree aucune table : elle refuse de s'appliquer si l'extension TimescaleDB manque, ce qui arrive quand db/init n'a pas ete joue. Le DDL propre a TimescaleDB qui ne depend pas du schema applicatif vit dans db/, pas ici.

Image Docker

Build multi-stage, dependances resolues par uv depuis uv.lock, execution sous un utilisateur non root, sonde de sante integree.

docker build -t enervision-backend:local .
docker run --rm -p 8000:8000 --env-file .env enervision-backend:local