From 3347fa5bdbcf7f348d469410fc037914fe6c924b Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Wed, 16 Sep 2026 10:14:19 +0200 Subject: [PATCH] docs(architecture): acte le contrat OpenAPI dans la vue backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/backend/README.md | 6 +- docs/architecture/20-backend.md | 74 +++++++++++++++---- .../31-contrat-authentification.md | 4 +- docs/architecture/README.md | 2 +- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/apps/backend/README.md b/apps/backend/README.md index 12fd9ba..400498c 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -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). diff --git a/docs/architecture/20-backend.md b/docs/architecture/20-backend.md index 8688a6a..1454a39 100644 --- a/docs/architecture/20-backend.md +++ b/docs/architecture/20-backend.md @@ -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 diff --git a/docs/architecture/31-contrat-authentification.md b/docs/architecture/31-contrat-authentification.md index f02fd1b..ec852c1 100644 --- a/docs/architecture/31-contrat-authentification.md +++ b/docs/architecture/31-contrat-authentification.md @@ -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 diff --git a/docs/architecture/README.md b/docs/architecture/README.md index c6b91f0..1c8c9a9 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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 |