docs(architecture): acte le contrat OpenAPI dans la vue backend

20-backend.md gagne une section qui dit où vit le schéma, comment on le
régénère, pourquoi il est versionné en plus d'être servi, et pourquoi servers,
license_info et contact restent absents. La table des routes gagne la colonne
des codes d'erreur déclarés.

31-contrat-authentification.md renvoyait le frontend vers /docs, donc vers une
API qui tourne. Il renvoie maintenant vers le fichier, lisible sans rien
lancer.
This commit is contained in:
Johan LEROY
2026-09-16 10:14:19 +02:00
parent da481d7485
commit 3347fa5bdb
4 changed files with 68 additions and 18 deletions
+5 -1
View File
@@ -28,7 +28,7 @@ de demarrer sans elles.
## 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`.
`make format`, `make typecheck`, `make test`, `make check`, `make openapi`, `make docker-build`.
Directement depuis ce dossier :
@@ -39,8 +39,12 @@ 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
uv run python -m app.cli export-openapi # régénère openapi.json
```
`openapi.json` est versionné : `tests/api/test_openapi.py` échoue si le fichier ne correspond
plus aux routes déclarées. Toute PR qui change une route le régénère dans le même commit.
Les conventions de tests, les gabarits et le detail des marqueurs sont dans
[`TESTING.md`](TESTING.md).
+59 -15
View File
@@ -126,22 +126,25 @@ Deux fichiers d'environnement, deux usages : `.env` à la racine alimente `docke
## Routes exposées
| Méthode | Chemin | Dans l'OpenAPI | Rôle |
| Méthode | Chemin | Rôle | Erreurs déclarées |
|---|---|---|---|
| GET | `/api/v1/health/live` | oui | Le processus répond. Ne touche pas la base |
| GET | `/api/v1/health/ready` | oui | La base répond **et** l'extension TimescaleDB est chargée |
| POST | `/api/v1/auth/login` | oui | Ouvre une session. Publique |
| POST | `/api/v1/auth/refresh` | oui | Fait tourner la session. Cookie seulement |
| POST | `/api/v1/auth/logout` | oui | Ferme la session courante. Idempotente |
| POST | `/api/v1/auth/logout-all` | oui | Ferme toutes les sessions du compte |
| POST | `/api/v1/auth/password` | oui | Change son propre mot de passe |
| GET | `/api/v1/auth/me` | oui | Décrit le compte connecté |
| GET | `/api/v1/users` | oui | Liste les comptes. `admin` |
| POST | `/api/v1/users` | oui | Crée un compte, rend un mot de passe provisoire. `admin` |
| PATCH | `/api/v1/users/{id}` | oui | Change le rôle ou l'activation. `admin` |
| POST | `/api/v1/users/{id}/password-reset` | oui | Réinitialise et ferme les sessions. `admin` |
| GET | `/metrics` | non | Format Prometheus. Jeton requis si `APP_METRICS_TOKEN` est posé |
| GET | `/docs`, `/redoc`, `/openapi.json` | non | Fermés en `staging` et en `prod` |
| GET | `/api/v1/health/live` | Le processus répond. Ne touche pas la base | 500 |
| GET | `/api/v1/health/ready` | La base répond **et** l'extension TimescaleDB est chargée | 503, 500 |
| POST | `/api/v1/auth/login` | Ouvre une session. Publique | 401, 422, 429, 500 |
| POST | `/api/v1/auth/refresh` | Fait tourner la session. Cookie seulement | 401, 500 |
| POST | `/api/v1/auth/logout` | Ferme la session courante. Idempotente | 500 |
| POST | `/api/v1/auth/logout-all` | Ferme toutes les sessions du compte | 401, 500 |
| POST | `/api/v1/auth/password` | Change son propre mot de passe | 401, 422, 500 |
| GET | `/api/v1/auth/me` | Décrit le compte connecté | 401, 500 |
| GET | `/api/v1/users` | Liste les comptes. `admin` | 401, 403, 500 |
| POST | `/api/v1/users` | Crée un compte, rend un mot de passe provisoire. `admin` | 401, 403, 409, 422, 500 |
| PATCH | `/api/v1/users/{id}` | Change le rôle ou l'activation. `admin` | 400, 401, 403, 404, 409, 422, 500 |
| POST | `/api/v1/users/{id}/password-reset` | Réinitialise et ferme les sessions. `admin` | 401, 403, 404, 422, 500 |
| GET | `/metrics` | Format Prometheus, hors du schéma. Jeton requis si `APP_METRICS_TOKEN` est posé | |
| GET | `/docs`, `/redoc`, `/openapi.json` | Hors du schéma. Fermés en `staging` et en `prod` | |
Les codes de la dernière colonne sont ceux que le schéma **déclare**, et le fichier
`openapi.json` versionné interdit qu'ils divergent de ce que les routes rendent.
**Quatre routes seulement sont publiques** : les deux sondes, `/auth/login` et `/auth/logout`.
`tests/api/test_route_protection.py` interroge réellement chaque autre route sans identifiant et
@@ -182,6 +185,47 @@ sequenceDiagram
end
```
## Contrat OpenAPI
Statut : `Fait`.
Le schéma est servi sur `/openapi.json`, `/docs` et `/redoc`, fermés en `staging` et en `prod`.
Il est aussi **versionné** dans [`apps/backend/openapi.json`](../../apps/backend/openapi.json) :
```bash
make openapi
```
Pourquoi un fichier en plus de la route. Une route qui change son contrat public le montre alors
dans la diff de la pull request, et le frontend dispose d'une référence lisible sans lancer l'API.
`tests/api/test_openapi.py` compare le fichier au schéma généré et échoue si l'un bouge sans
l'autre ; le fichier vivant sous `apps/backend/`, le filtre de chemins de `backend.yml` le couvre.
**Le schéma exporté ne dépend pas du poste.** `settings_du_contrat()` pose le nom, la version et
le préfixe, et coupe la lecture du `.env`. Sans cela, un `APP_API_PREFIX` local suffirait à faire
diverger le fichier d'une machine à l'autre, et le test deviendrait un oracle de configuration
plutôt qu'un garde-fou de contrat.
Trois champs sont volontairement absents d'`info`, parce qu'ils poseraient une décision qui n'est
pas prise :
| Champ | Pourquoi |
|---|---|
| `servers` | L'URL publique dépend de l'ingress, question ouverte dans [10-infra.md](10-infra.md) |
| `license_info` | Aucune licence n'est choisie |
| `contact` | Aucun canal de support n'existe |
Deux schémas de sécurité sont déclarés : `Jeton d'accès` pour le porteur JWT, et
`Cookie de rafraîchissement` pour `/auth/refresh` et `/auth/logout`. **Le second est purement
documentaire** : son `auto_error=False` garantit qu'il ne décide d'aucun refus. Le passer à vrai
ferait répondre 403 avant d'atteindre `lit_le_cookie()`, et `/auth/refresh` cesserait de rendre le
401 sur lequel le frontend déclenche sa déconnexion.
Les modèles de `app/schemas/errors.py` décrivent ce que les gestionnaires renvoient réellement.
`ValidationErrorResponse` remplace le `HTTPValidationError` par défaut de FastAPI, dont la clé
`loc` n'apparaît dans aucune réponse de cette API : `validation_error_handler()` rend `champ` et
`type`. Renommer un champ là-bas sans le faire ici rend la documentation fausse en silence.
## Sécurité
Voir la vue consolidée dans [00-vue-ensemble.md](00-vue-ensemble.md) et les décisions dans les
@@ -26,7 +26,9 @@ gérer : il suffit d'envoyer les requêtes avec `withCredentials`.
| PATCH | `/api/v1/users/{id}` | jeton d'accès, `admin` | `200` `UserResponse` |
| POST | `/api/v1/users/{id}/password-reset` | jeton d'accès, `admin` | `200` `TemporaryPasswordResponse` |
Le schéma exact est dans `/docs` (Swagger), servi en local et en développement.
Le schéma exact est dans [`apps/backend/openapi.json`](../../apps/backend/openapi.json),
lisible sans lancer l'API, et servi par `/docs` en local et en développement. La table des
codes d'erreur ci-dessous reste la référence de comportement, le schéma celle de forme.
## Charges utiles
+1 -1
View File
@@ -10,7 +10,7 @@ contredisent, c'est l'ADR qui fait foi et la vue qui est en retard.
|---|---|
| [00-vue-ensemble.md](00-vue-ensemble.md) | Jalons du projet, contexte, conteneurs, sécurité, flux bout en bout |
| [10-infra.md](10-infra.md) | Poste de développement, cible k3s, décisions figées, ports et noms |
| [20-backend.md](20-backend.md) | Couches FastAPI, séquence de démarrage, routes, configuration |
| [20-backend.md](20-backend.md) | Couches FastAPI, séquence de démarrage, routes, configuration, contrat OpenAPI |
| [30-frontend.md](30-frontend.md) | Angular, arborescence cible, flux HTTP |
| [31-contrat-authentification.md](31-contrat-authentification.md) | Ce que le frontend doit savoir pour coder la connexion |
| [40-data.md](40-data.md) | Frontières `db/` et `alembic/`, cycle de vie d'une mesure, modèle |