Merge remote-tracking branch 'origin/dev' into feat/design-system
# Conflicts: # apps/frontend/src/app/features/auth/change-password/change-password.html # apps/frontend/src/app/features/auth/change-password/change-password.ts # apps/frontend/src/app/features/auth/login/login.html # apps/frontend/src/app/features/auth/login/login.ts
This commit is contained in:
@@ -74,9 +74,10 @@ collecteur ne vient le lire.
|
||||
|
||||
| Domaine | Technologie | Emplacement | Statut | Ce qui existe réellement |
|
||||
|---|---|---|---|---|
|
||||
| Backend | FastAPI, Python 3.14 | `apps/backend` | `En cours` | Factory, configuration, journalisation, 2 sondes de santé, `/metrics`, contrat OpenAPI versionné, routes `sites` et `recommendations` en lecture (endpoints → services → repositories → models) |
|
||||
| Backend | FastAPI, Python 3.14 | `apps/backend` | `En cours` | Factory, configuration, journalisation, 2 sondes de santé, `/metrics`, contrat OpenAPI versionné, routes `sites`, `alerts`, `recommendations`, `stats/summary` et `readings` en lecture (endpoints → services → repositories → models) |
|
||||
| Frontend | Angular 22, Node 24 | `apps/frontend` | `En cours` | Tableau de bord sur route `/dashboard`, deux services HTTP, graphiques Chart.js, données servies par des fixtures |
|
||||
| Base | PostgreSQL 17 + TimescaleDB | `db` | `Fait` | Bootstrap de l'extension, base de test, chaîne Alembic. Schéma applicatif créé (`site`, `dataset`, `reading` en hypertable, `prediction`, `alert`, `recommendation`) |
|
||||
| ML | LightGBM, MLflow | `ml` | `En cours` | Pipeline d'entraînement (features par lags/moyennes glissantes, baseline de persistance saisonnière, suivi MLflow local), voir [ADR 0005](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../../ML-START.md). Scoring, endpoint et surveillance de dérive pas encore construits |
|
||||
| Infra | Terraform, k3s single-node | `infra/terraform` | `En cours` | Module d'installation du cluster. Jamais appliqué, aucune ressource Kubernetes déclarée |
|
||||
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Cible` | Rien, hors le `/metrics` exposé par l'API |
|
||||
| ETL | Apache Airflow | `etl/airflow` | `Cible` | Rien |
|
||||
|
||||
@@ -146,6 +146,7 @@ Deux fichiers d'environnement, deux usages : `.env` à la racine alimente `docke
|
||||
| GET | `/api/v1/recommendations` | Liste les recommandations. `lecteur` | 401, 403, 500 |
|
||||
| GET | `/api/v1/recommendations/{recommendation_id}` | Décrit une recommandation. `lecteur` | 401, 403, 404, 422, 500 |
|
||||
| GET | `/api/v1/stats/summary` | Résume la consommation instantanée du parc. `lecteur` | 401, 403, 500 |
|
||||
| GET | `/api/v1/readings` | Historique des lectures, filtrable par `site_id`, fenêtre `start`/`end` (24h par défaut, 90 jours maximum) et paginé par `limit`/`offset`. `lecteur` | 400, 401, 403, 422, 500 |
|
||||
| GET | `/api/v1/sensors/status` | État de santé des capteurs par site, dérivé de la dernière lecture. `admin` | 401, 403, 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` | |
|
||||
@@ -159,21 +160,33 @@ Les codes de la dernière colonne sont ceux que le schéma **déclare**, et le f
|
||||
donc de modifier la liste dans ce fichier de test.
|
||||
|
||||
`GET /sites` et `GET /sites/{site_id}` sont la première route métier, et le gabarit repris pour
|
||||
`GET /alerts` puis pour les suivantes (`reading`, `dataset`, `prediction`, `recommendation`) : les
|
||||
quatre couches `endpoints → services → repositories → models` y sont toutes présentes, sur des
|
||||
tables déjà créées par la révision Alembic `e6d2026091501`. Elles n'exigent que le rôle `lecteur`,
|
||||
contrairement aux routes d'administration qui exigent `admin`. `SiteRepository` lit par
|
||||
`AsyncSession.scalar()` (une ligne) et `AsyncSession.scalars()` (plusieurs lignes) plutôt que par
|
||||
`execute()`, ce qui la rend testable par la fixture `fake_session` au niveau endpoint sans base
|
||||
réelle. `GET /recommendations` et `GET /recommendations/{recommendation_id}` reprennent le même
|
||||
gabarit à la lettre, `recommendation_id` étant un entier plutôt qu'un texte. Une recommandation ne
|
||||
porte pas `site_id` : elle remonte à un site par sa seule `alert_id`, `alert` n'étant pas encore
|
||||
exposée. `GET /stats/summary` et `GET /sensors/status` agrègent chacune deux repositories
|
||||
(`SiteRepository`, `ReadingRepository`) dans un service dédié plutôt que d'exposer une table :
|
||||
elles n'entrent donc pas dans ce gabarit route-par-table. Le contrat détaillé pour le frontend est
|
||||
dans
|
||||
`GET /alerts` puis pour les suivantes (`dataset`, `prediction`) : les quatre couches
|
||||
`endpoints → services → repositories → models` y sont toutes présentes, sur des tables déjà créées
|
||||
par la révision Alembic `e6d2026091501`. Elles n'exigent que le rôle `lecteur`, contrairement aux
|
||||
routes d'administration qui exigent `admin`. `SiteRepository` lit par `AsyncSession.scalar()` (une
|
||||
ligne) et `AsyncSession.scalars()` (plusieurs lignes) plutôt que par `execute()`, ce qui la rend
|
||||
testable par la fixture `fake_session` au niveau endpoint sans base réelle. `GET /recommendations`
|
||||
et `GET /recommendations/{recommendation_id}` reprennent le même gabarit à la lettre,
|
||||
`recommendation_id` étant un entier plutôt qu'un texte. Une recommandation ne porte pas `site_id` :
|
||||
elle remonte à un site par sa seule `alert_id`, `alert` n'étant pas encore exposée. `GET
|
||||
/stats/summary` et `GET /sensors/status` agrègent chacune deux repositories (`SiteRepository`,
|
||||
`ReadingRepository`) dans un service dédié plutôt que d'exposer une table : elles n'entrent donc
|
||||
pas dans ce gabarit route-par-table. Le contrat détaillé pour le frontend est dans
|
||||
[31-contrat-authentification.md](31-contrat-authentification.md).
|
||||
|
||||
`GET /readings` reprend le même gabarit mais s'en écarte sur un point : `reading` est l'hypertable,
|
||||
donc la seule table métier pouvant porter des années d'historique, ce que `docs/architecture/
|
||||
owasp-traceabilite.md` documentait comme un risque ouvert (API4, aucune pagination plafonnée ni
|
||||
fenêtre temporelle maximale). `ReadingService` porte donc une couche de validation absente des
|
||||
autres routes de lecture : `start`/`end` sont optionnels (24 dernières heures par défaut si les
|
||||
deux sont omis, l'un défaut par rapport à l'autre sinon), l'écart entre les deux est plafonné à 90
|
||||
jours (`FENETRE_MAXIMALE`), et `limit`/`offset` (défaut 500, plafond 2000) empêchent qu'une fenêtre
|
||||
large mais peu dense reste malgré tout coûteuse. Un dépassement de plafond répond `400` (règle
|
||||
métier, portée par le service) plutôt que `422` (réservé à la validation structurelle de FastAPI,
|
||||
par exemple `limit` hors bornes). Un datetime sans fuseau dans `start`/`end` est traité comme de
|
||||
l'UTC plutôt que rejeté : le comparer tel quel à `reading.timestamp` (`timestamptz`) échouerait
|
||||
côté pilote, en `500` plutôt qu'un refus propre.
|
||||
|
||||
### `/health/ready`
|
||||
|
||||
Cette sonde porte une garde décrite dans l'[ADR 0001](../adr/0001-postgresql-timescaledb.md) : un
|
||||
@@ -249,7 +262,7 @@ Les modèles de `app/schemas/errors.py` décrivent ce que les gestionnaires renv
|
||||
### Ajouter une route métier
|
||||
|
||||
Checklist pour toute nouvelle route sur le gabarit `sites`/`alerts`/`recommendations`/`stats`/
|
||||
`sensors` (`reading`, `dataset`, `prediction`) :
|
||||
`readings`/`sensors` (`dataset`, `prediction`) :
|
||||
|
||||
1. Composer ses `responses=` depuis `app/api/openapi.py` : `REPONSES_LECTEUR`/`REPONSES_ADMIN`
|
||||
au niveau de l'`include_router()` du routeur, `REPONSE_VALIDATION` et les codes locaux
|
||||
@@ -326,7 +339,9 @@ Trois fichiers méritent d'être connus avant de toucher à l'authentification :
|
||||
agir sur le site B. C'est la limite connue du modèle, et le risque BOLA du top 10 API.
|
||||
- **Rôles PostgreSQL cantonnés** pour l'ETL et le travail d'apprentissage, plus le `REVOKE` sur
|
||||
`audit_log`. Dette assumée, décrite dans les ADR 0003 et 0004.
|
||||
- **Pagination et fenêtrage** des lectures de séries temporelles, qui conditionnent la forme des
|
||||
endpoints métier. Sans plafond dur, une requête sur dix ans d'historique suffit à faire tomber
|
||||
l'API.
|
||||
- **Pagination et fenêtrage** : posés sur `GET /readings` (fenêtre plafonnée à 90 jours,
|
||||
`limit`/`offset` plafonné à 2000), mais toujours en `limit`/`offset` simple — pas de curseur ni
|
||||
de plan de secours si un `offset` élevé sur une fenêtre dense devient lent en pratique.
|
||||
`statement_timeout` reste absent au niveau de la connexion, donc rien n'empêche une requête
|
||||
individuelle de tourner longtemps si les plafonds au-dessus d'elle s'avéraient insuffisants.
|
||||
- **Politique de versionnement de l'API** au-delà du préfixe `/api/v1`.
|
||||
|
||||
@@ -20,6 +20,8 @@ gérer : il suffit d'envoyer les requêtes avec `withCredentials`.
|
||||
| POST | `/api/v1/auth/logout` | cookie | `204` |
|
||||
| POST | `/api/v1/auth/logout-all` | jeton d'accès | `204` |
|
||||
| POST | `/api/v1/auth/password` | jeton d'accès | `200` `TokenResponse` |
|
||||
| POST | `/api/v1/auth/forgot-password` | aucune | `202` (toujours, que le compte existe ou non) |
|
||||
| POST | `/api/v1/auth/reset-password` | aucune (jeton dans le corps) | `200` `TokenResponse` |
|
||||
| GET | `/api/v1/auth/me` | jeton d'accès | `200` `PrincipalResponse` |
|
||||
| GET | `/api/v1/users` | jeton d'accès, `admin` | `200` `UserResponse[]` |
|
||||
| POST | `/api/v1/users` | jeton d'accès, `admin` | `201` `TemporaryPasswordResponse` |
|
||||
@@ -51,7 +53,17 @@ codes d'erreur ci-dessous reste la référence de comportement, le schéma celle
|
||||
}
|
||||
|
||||
// POST /auth/password
|
||||
{ "current_password": "...", "new_password": "..." } // 12 à 128 caractères
|
||||
{ "current_password": "...", "new_password": "..." } // 8 à 128 caractères, au moins 1 majuscule, 1 minuscule, 1 chiffre, 1 caractère spécial
|
||||
|
||||
// POST /auth/forgot-password
|
||||
{ "email": "operateur@enervision.fr" }
|
||||
// Répond toujours 202, sans corps, que le compte existe, soit inactif, ou soit inconnu.
|
||||
|
||||
// POST /auth/reset-password
|
||||
{ "token": "...", "new_password": "..." } // même règle de complexité que /auth/password
|
||||
// Le jeton vient du lien reçu par email, valable 15 minutes, à usage unique. Répond
|
||||
// TokenResponse au succès (l'appareil qui pose le nouveau mot de passe reste connecté), ou 400
|
||||
// si le jeton est invalide, déjà utilisé, ou expiré.
|
||||
```
|
||||
|
||||
Le secret de rafraîchissement **n'apparaît jamais** dans le corps de la réponse.
|
||||
@@ -70,6 +82,9 @@ Le secret de rafraîchissement **n'apparaît jamais** dans le corps de la répon
|
||||
| `403` avec `detail: "Droits insuffisants"` | rôle trop bas | masquer ou griser l'action, ne pas déconnecter |
|
||||
| `403` sur `/auth/refresh`, `/logout`, `/logout-all`, `/password` | origine hors liste autorisée (voir « Origines autorisées ») | erreur de configuration réseau, pas un cas à gérer par l'utilisateur |
|
||||
| `422` | corps invalide | le détail donne `champ` et `type`, jamais la valeur envoyée |
|
||||
| `429` sur `/auth/forgot-password` | trop de demandes | afficher l'attente, l'en-tête `Retry-After` donne les secondes |
|
||||
| `400` sur `/auth/reset-password` | lien invalide, déjà utilisé, ou expiré | inviter à redemander un lien depuis `/forgot-password` |
|
||||
| `403` sur `/auth/reset-password` | origine hors liste autorisée | erreur de configuration réseau, pas un cas à gérer par l'utilisateur |
|
||||
|
||||
## Les quatre règles qui comptent
|
||||
|
||||
|
||||
@@ -231,3 +231,73 @@ et ne sont pas considérées comme des alertes actuelles.
|
||||
- Les mesures API ne sont pas rattachées à un dataset historique.
|
||||
- Une alerte peut être associée à une prévision du même site.
|
||||
- Une alerte peut donner lieu à plusieurs recommandations.
|
||||
|
||||
## Ingestion des données historiques
|
||||
|
||||
Le MVP EnerVision initialise les données énergétiques à partir du dataset fourni dans le cadre du projet.
|
||||
|
||||
Le dataset de référence contient 122 647 mesures issues de 7 sites et couvre la période du 1er janvier 2023 au 31 décembre 2024.
|
||||
|
||||
Les fichiers sources CSV et JSON sont nécessaires uniquement pour l'initialisation des données. Ils ne sont pas versionnés dans Git et sont placés localement dans `data/raw/`.
|
||||
|
||||
### Architecture du flux
|
||||
|
||||
```text
|
||||
Dataset CSV + métadonnées JSON
|
||||
|
|
||||
v
|
||||
historical_import.py
|
||||
|
|
||||
+------+------+
|
||||
| |
|
||||
v v
|
||||
Validation SHA-256
|
||||
| Traçabilité
|
||||
+------+------+
|
||||
|
|
||||
v
|
||||
Normalisation
|
||||
+ qualité data
|
||||
|
|
||||
v
|
||||
Chargement par batches
|
||||
|
|
||||
v
|
||||
PostgreSQL / TimescaleDB
|
||||
| | |
|
||||
v v v
|
||||
dataset site reading
|
||||
```
|
||||
|
||||
Le pipeline est développé en Python.
|
||||
|
||||
Pandas est utilisé pour l'extraction, la validation et la préparation des données. SQLAlchemy Async assure le chargement transactionnel dans PostgreSQL/TimescaleDB.
|
||||
|
||||
Une empreinte SHA-256 permet d'identifier le dataset utilisé et d'assurer sa traçabilité.
|
||||
|
||||
Les valeurs manquantes sont conservées pendant l'ingestion afin de préserver les données sources. Aucune imputation n'est réalisée à cette étape.
|
||||
|
||||
Le chargement des mesures est effectué par batches de 1 000 lignes.
|
||||
|
||||
Les données provenant du dataset CSV sont identifiées par `source = "csv"` et associées à leur `dataset_id`.
|
||||
|
||||
### Résultats validés
|
||||
|
||||
Le chargement de référence a permis d'obtenir :
|
||||
|
||||
- 1 dataset ;
|
||||
- 7 sites ;
|
||||
- 122 647 mesures ;
|
||||
- 0 doublon détecté dans le dataset source.
|
||||
|
||||
L'idempotence a également été vérifiée par une deuxième exécution du pipeline : aucune nouvelle mesure n'a été créée et le nombre de `reading` est resté à 122 647.
|
||||
|
||||
La procédure détaillée d'installation, d'exécution, de validation et de contrôle du pipeline est disponible dans `etl/README.md`.
|
||||
|
||||
### Évolution prévue
|
||||
|
||||
L'étape suivante consiste à orchestrer les traitements Data avec Apache Airflow.
|
||||
|
||||
L'orchestration réutilisera la logique ETL existante afin de séparer la logique de traitement de la planification, du suivi des exécutions et de la gestion des erreurs.
|
||||
|
||||
Le pipeline servira ensuite de base à la préparation des données nécessaires au modèle de Machine Learning.
|
||||
|
||||
@@ -22,6 +22,7 @@ lecture seule ; plusieurs lignes resteront à compléter une fois les endpoints
|
||||
| Argon2id m=19456 t=2 p=1, re-hachage passif quand les paramètres changent | `app/core/hashing.py` | A02 Cryptographic Failures, A07 Identification and Authentication Failures |
|
||||
| Message et temps de réponse identiques quelle que soit la cause de l'échec, haché leurre sur adresse inconnue | `app/services/auth.py` | A07, API2 |
|
||||
| Limitation de débit à fenêtre glissante sur trois clés, évaluée avant le hachage | `app/services/auth.py`, `app/repositories/login_attempt.py` | A07, API4 Unrestricted Resource Consumption |
|
||||
| `GET /readings` : fenêtre temporelle plafonnée à 90 jours (24h par défaut), `limit`/`offset` plafonné à 2000, refus `400` si la fenêtre est inversée ou trop large | `app/services/reading.py` | API4 |
|
||||
| Absence de verrouillage de compte, qui serait un déni de service | ADR 0002 | API4 |
|
||||
| Jeton de rafraîchissement opaque, haché en base, rotation avec détection de réutilisation | `app/services/auth.py`, `app/repositories/refresh_token.py` | A07, API2 |
|
||||
| Séparation structurelle accès / rafraîchissement, impossible à confondre | ADR 0002 | API2 |
|
||||
@@ -50,7 +51,7 @@ règles Bandit. Ajouter Bandit à la CI serait redondant, contrairement à ce qu
|
||||
| Item | État | Raison |
|
||||
|---|---|---|
|
||||
| **API1 Broken Object Level Authorization** | **ouvert** | Les rôles sont globaux, il n'y a pas de portée par site : `GET /sites/{site_id}` et `GET /recommendations/{recommendation_id}` répondent à tout compte `lecteur` pour n'importe quel site ou recommandation, sans vérifier une affectation compte-site qui n'existe pas encore. Un opérateur du site A pourra agir sur le site B dès que les endpoints d'écriture métier existeront. Correctif prévu : table d'affectation compte-site, contrôle d'appartenance dans la même dépendance que le contrôle de rôle. |
|
||||
| **API4, lectures de séries temporelles** | **ouvert** | Pas encore d'endpoint métier, donc ni pagination plafonnée, ni fenêtre temporelle maximale, ni `statement_timeout`. C'est la façon la plus probable dont la démonstration tombera : une requête sur dix ans d'historique suffit. |
|
||||
| **API4, lectures de séries temporelles** | **partiel** | `GET /readings` plafonne la fenêtre temporelle (90 jours) et la pagination (`limit` ≤ 2000), voir plus haut. Reste ouvert : pagination en `limit`/`offset` simple plutôt qu'en curseur (un `offset` élevé sur une fenêtre dense reste coûteux), et aucun `statement_timeout` au niveau de la connexion pour borner une requête individuelle si les plafonds au-dessus s'avéraient insuffisants. |
|
||||
| **API8 Security Misconfiguration, transport** | **ouvert** | Pas de TLS, donc ni HSTS, ni cookie `Secure` réellement posé en production. Ils appartiennent au terminateur TLS, qui n'existe pas. |
|
||||
| **API10 Unsafe Consumption of APIs** | **ouvert, et spécifique à ce projet** | L'API Mock de l'école n'a aucune authentification, tourne en HTTP clair sur le réseau de l'école, et expose un endpoint mutatif à quiconque. Sa réponse doit être traitée comme une entrée hostile : bornes physiques, taille de tableau plafonnée, timeout, et frontière d'anti-corruption. La conséquence la plus sérieuse n'est pas la fausse alerte, c'est l'empoisonnement du jeu d'entraînement du modèle de prédiction. |
|
||||
| **A08 Software and Data Integrity Failures** | **partiel** | La CI vérifie le code mais n'analyse ni les dépendances ni les images. `.terraform.lock.hcl` reste ignoré par git, ce qui contredit une chaîne d'approvisionnement maîtrisée. |
|
||||
|
||||
Reference in New Issue
Block a user