Specs : rattrapage de l'écart avec le code + règle de synchro continue
Aligne requirements.md et design.md sur l'état réel du code (écart accumulé depuis le 14/09 en mode dev direct). requirements.md : - Intro : compte unique authentifié (au lieu de « sans authentification ». - Glossaire : Compte, Adresse_Email, Mot_De_Passe, Session, JWT, Jeton_Reinitialisation, Aller_Retour, Distance_Effective. - Exigence 1 : case Aller/Retour. Exigence 10 : dénomination véhicule. - Exigence 12 : refonte consultation par mois (sélecteur année/mois, liste mensuelle groupée par jour repliable, nav jour bornée aux jours avec trajets). - Exigence 13 : tranches de barème multiples (dernière illimitée, coef 3 décimales) + recopie du dernier barème. - Exigence 16 : onboarding relié à l'authentification. - Nouvelles exigences 17 (Authentification) et 18 (Trajet aller/retour). design.md : - Overview + contraintes : compte authentifié, port 2202, Caddy externe HTTPS. - Déploiement : compose (port 2202, JWT_SECRET_KEY obligatoire, e-mail/MailKit, AUTH_FRONTEND_BASE_URL), reverse-proxy Caddy externe, pas de UseHttpsRedirection. - Nouvelle section « Évolutions majeures » : décisions d'architecture pour l'auth, l'onboarding 2 étapes, l'aller/retour, la consultation par mois, le barème multi-tranches, la dénomination véhicule et le recalcul ciblé des distances. Ajoute .kiro/steering/spec-sync.md : règle de mise à jour des specs dans le même commit que le code, à chaque évolution validée.
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
**SchotzIAD KM Tracker** est une application web mono-utilisateur permettant de saisir, calculer et consulter l'historique kilométrique de déplacements professionnels ou personnels. L'application calcule automatiquement les distances routières via l'API OpenRouteService (ORS), agrège les données par mois et par année, et permet l'export en PDF des résumés.
|
||||
**SchotzIAD KM Tracker** est une application web mono-utilisateur, protégée par un **compte unique authentifié** (e-mail + mot de passe, session JWT), permettant de saisir, calculer et consulter l'historique kilométrique de déplacements professionnels ou personnels. L'application calcule automatiquement les distances routières via l'API OpenRouteService (ORS), agrège les données par mois et par année, et permet l'export en PDF des résumés.
|
||||
|
||||
### Périmètre fonctionnel
|
||||
|
||||
@@ -22,25 +22,28 @@
|
||||
| 10 | Profil / adresse domicile | Saisie du domicile, bouton "Utiliser mon domicile" dans le formulaire |
|
||||
| 11 | Commentaire par trajet | Champ optionnel texte libre (0–500 car.) associé à chaque Trajet |
|
||||
| 12 | Insertion entre deux trajets | Formulaire pré-rempli avec adresse d'arrivée du précédent / adresse de départ du suivant |
|
||||
| 13 | Navigation temporelle | Sélecteur année/mois/jour avec surbrillance des périodes ayant des données, pas de date future |
|
||||
| 14 | Mise en page consultation | Zone principale (trajets du jour, scroll) + panneau latéral totaux + carte + navigation jour±1 |
|
||||
| 13 | Navigation temporelle | Sélecteur année/mois (repliable) avec surbrillance des mois ayant des données, pas de mois futur |
|
||||
| 14 | Mise en page consultation | Liste mensuelle groupée par jour (repliable, scroll) + panneau totaux + carte du jour ciblé + navigation jour bornée au mois |
|
||||
| 15 | Profil enrichi & véhicules | Nom, prénom, matricule (8 car.), plaque, puissance fiscale ; historique des véhicules daté |
|
||||
| 16 | Association véhicule par trajet | Chaque trajet référence un véhicule + puissance fiscale ; totaux filtrés par véhicule |
|
||||
| 17 | Barème kilométrique annuel | Saisie par tranches (puissance fiscale, bornes km, coefficient, part fixe) ; montant prévisionnel minoré 80% |
|
||||
| 18 | Montants réglés & régularisation | Saisie du montant réglé par mois ; rapport annuel avec montant réel, versé, dû et régularisation |
|
||||
| 19 | Carnet d'adresses nommées | Menu CRUD d'Adresses_Nommées (Dénomination unique + adresse BAN) ; réutilisation par Dénomination dans la saisie et l'insertion de Trajets |
|
||||
| 20 | Onboarding première utilisation | Présentation de la saisie du profil (identité + véhicule) si le Profil n'est pas configuré, avant tout accès à la saisie / consultation des trajets |
|
||||
| 21 | Authentification | Compte unique (e-mail + mot de passe), connexion par JWT (12 h par défaut), réinitialisation de mot de passe par e-mail, garde de session serveur |
|
||||
| 22 | Trajet aller/retour | Case à cocher A/R doublant la distance comptée (Distance_Effective) sans ligne supplémentaire ; badge A/R en liste et mention « (A/R) » sur la carte |
|
||||
| 23 | Barème multi-tranches & recopie | Tranches paramétrables par puissance fiscale (dernière tranche illimitée), coefficient sur 3 décimales, bouton « Recopier le dernier barème connu » |
|
||||
|
||||
### Contraintes techniques
|
||||
|
||||
- **Mono-utilisateur** : pas d'authentification ni de gestion de comptes
|
||||
- **Mono-utilisateur authentifié** : un seul Compte (e-mail + mot de passe), session par jeton JWT signé HS256 (durée configurable, 12 h par défaut) ; tous les endpoints de données sont protégés
|
||||
- **Frontend** : Angular (TypeScript), compilé et servi en SPA
|
||||
- **Backend** : ASP.NET Core Web API (C#)
|
||||
- **Base de données** : SQLite (choix retenu pour simplicité mono-utilisateur, zero-configuration, fichier unique portable)
|
||||
- **Base de données** : SQLite (simplicité mono-utilisateur, zero-configuration, fichier unique portable) ; migrations EF Core appliquées automatiquement au démarrage
|
||||
- **Service externe — adresses** : API BAN (Base Adresses Nationale) — `https://api-adresse.data.gouv.fr` — autocomplétion et géocodage des adresses françaises, sans clé d'authentification
|
||||
- **Service externe — itinéraires** : OpenRouteService (ORS) — calcul de distance routière et géométrie d'itinéraire entre coordonnées géographiques connues
|
||||
- **Génération PDF** : côté backend, bibliothèque QuestPDF
|
||||
- **Hébergement** : l'application est packagée et hébergée via **Docker Compose** — un conteneur `frontend` (Angular servi par nginx) + un conteneur `backend` (ASP.NET Core API). Le fichier SQLite est persisté sur un volume nommé (ou bind mount) monté dans le conteneur backend, afin de survivre aux redémarrages et mises à jour des conteneurs
|
||||
- **Hébergement** : l'application est packagée et hébergée via **Docker Compose** — un conteneur `frontend` (Angular servi par nginx en interne) publié sur le port hôte **2202**, et un conteneur `backend` (ASP.NET Core API) non exposé à l'hôte. Le fichier SQLite est persisté sur un volume monté dans le conteneur backend. La terminaison **HTTPS est assurée par un reverse-proxy Caddy externe** (sur une autre machine) qui relaie vers `IP_hôte:2202` ; le backend ne fait donc pas de redirection HTTPS lui-même
|
||||
|
||||
---
|
||||
|
||||
@@ -1347,7 +1350,7 @@ graph LR
|
||||
end
|
||||
```
|
||||
|
||||
> **Approche retenue** : reverse-proxy unique via nginx (frontend). Le backend n'est pas exposé directement à l'hôte ; tout le trafic transite par nginx, qui sert les fichiers statiques et relaie `/api` vers le backend sur le réseau interne Compose.
|
||||
> **Approche retenue** : à l'intérieur de Compose, nginx (conteneur frontend) sert les fichiers statiques Angular et relaie `/api` vers le backend sur le réseau interne ; le backend n'est pas exposé directement à l'hôte. La terminaison **HTTPS est déléguée à un reverse-proxy Caddy externe** (sur une autre machine) qui expose l'application sur le net via une URL HTTPS et relaie vers `IP_hôte:2202`. Le port hôte 2202 est donc à restreindre par pare-feu à l'IP du proxy Caddy. Le backend ne réalise pas de redirection HTTPS (`UseHttpsRedirection` retiré), la terminaison TLS étant assurée en amont.
|
||||
|
||||
### Backend `Dockerfile` (multi-stage)
|
||||
|
||||
@@ -1438,6 +1441,10 @@ services:
|
||||
- ConnectionStrings__Default=Data Source=/data/trajets.db
|
||||
# La clé ORS est fournie par l'environnement / .env — jamais intégrée à l'image
|
||||
- ORS__ApiKey=${ORS_API_KEY}
|
||||
# Clé de signature JWT : OBLIGATOIRE (le backend refuse de démarrer si absente)
|
||||
- JWT__SecretKey=${JWT_SECRET_KEY:?JWT_SECRET_KEY requis}
|
||||
# URL publique du frontend (liens de réinitialisation de mot de passe), ex. https://mon-domaine
|
||||
- Auth__FrontendBaseUrl=${AUTH_FRONTEND_BASE_URL}
|
||||
volumes:
|
||||
- trajets-data:/data
|
||||
restart: unless-stopped
|
||||
@@ -1453,7 +1460,7 @@ services:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:80" # hôte 8080 → nginx (frontend)
|
||||
- "2202:80" # hôte 2202 → nginx (frontend) ; 8080 évité (conflit hôte fréquent)
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
@@ -1468,6 +1475,9 @@ volumes:
|
||||
### Configuration et secrets
|
||||
|
||||
- La **clé ORS** est fournie exclusivement via variable d'environnement (`ORS_API_KEY`), typiquement à partir d'un fichier `.env` **non commité** (ajouté au `.gitignore`) et lu automatiquement par Docker Compose. Elle n'est **jamais** intégrée dans une image Docker.
|
||||
- La **clé secrète JWT** (`JWT_SECRET_KEY`) est **obligatoire** : le backend refuse de démarrer si elle est absente. Elle est fournie via `.env`, jamais commitée.
|
||||
- L'**URL publique du frontend** (`AUTH_FRONTEND_BASE_URL`, ex. `https://mon-domaine`) sert à construire les liens de réinitialisation de mot de passe envoyés par e-mail — et non le port interne.
|
||||
- La **configuration e-mail** (SMTP via MailKit) est optionnelle : si elle est absente, le lien de réinitialisation est journalisé en repli (fallback) au lieu d'être envoyé.
|
||||
- L'**API BAN** ne nécessite aucune clé — aucune configuration de secret n'est requise pour le géocodage.
|
||||
- Le **volume nommé `trajets-data`** (monté sur `/data`) garantit que le fichier `trajets.db` persiste à travers les redémarrages, recréations et mises à jour des conteneurs.
|
||||
|
||||
@@ -1477,6 +1487,58 @@ Les **migrations EF Core sont appliquées automatiquement au démarrage du backe
|
||||
|
||||
---
|
||||
|
||||
## Évolutions majeures postérieures à la conception initiale
|
||||
|
||||
> Cette section documente les décisions d'architecture des fonctionnalités ajoutées après la conception initiale. Elle fait autorité en cas de divergence avec les sections antérieures rédigées pour la version mono-utilisateur sans compte.
|
||||
|
||||
### Authentification par compte unique (Exigence 17)
|
||||
|
||||
- **Modèle** : une entité `Compte` (Id, Email, MotDePasseHash, champs de réinitialisation) — un seul enregistrement autorisé. Le mot de passe est stocké en **hachage salé** (jamais en clair).
|
||||
- **Session** : à la connexion, le backend émet un **JWT signé HS256** portant l'identifiant du Compte (`sub`) et une expiration configurable (`Jwt:DurationHours`, 12 h par défaut). Le frontend conserve le jeton dans `localStorage` et l'ajoute en `Authorization: Bearer` via un intercepteur HTTP.
|
||||
- _Décision_ : `localStorage` retenu malgré le risque XSS, acceptable pour une application personnelle mono-utilisateur ; cookie HttpOnly rejeté pour éviter la complexité CSRF.
|
||||
- **Protection des endpoints** : tous les endpoints de données exigent un JWT valide (`[Authorize]` global) ; 401 sinon.
|
||||
- **Validation renforcée** : `OnTokenValidated` rejette un JWT dont le Compte associé n'existe plus (lecture du claim via `sub` **ou** `ClaimTypes.NameIdentifier`). Un endpoint protégé `GET /api/auth/moi` permet au frontend de valider la Session ; un `authGuard` Angular appelle cette validation avant d'afficher les vues protégées.
|
||||
- **Réinitialisation de mot de passe** : `POST` d'une demande → génération d'un `Jeton_Reinitialisation` à usage unique et durée limitée, envoyé par e-mail (**MailKit/SMTP**). Si l'e-mail n'est pas configurable, le lien est **journalisé en repli**. Un second endpoint consomme le jeton et remplace le hachage.
|
||||
- **Clé secrète obligatoire** : `JWT_SECRET_KEY` est requise au démarrage (`:?` dans Compose) ; le backend refuse de démarrer sans elle.
|
||||
- **Endpoints** : `POST /api/auth/inscription` (création du Compte unique), `POST /api/auth/connexion` (émission du JWT), `GET /api/auth/moi` (validation de session), `POST /api/auth/mot-de-passe/oubli` et `POST /api/auth/mot-de-passe/reinitialisation`.
|
||||
|
||||
### Onboarding en deux étapes (Exigences 16, 17)
|
||||
|
||||
L'initialisation se fait en deux temps : (1) création du Compte (e-mail + mot de passe ≥ 8 caractères) puis redirection vers la connexion ; (2) une fois authentifié, saisie de l'identité (nom, prénom, matricule), d'une adresse de domicile optionnelle et d'au moins un Véhicule. La première ouverture sans Compte redirige directement vers la page de création de compte.
|
||||
|
||||
### Trajet aller/retour (Exigence 18)
|
||||
|
||||
- **Stockage** : le `Trajet` conserve la **distance simple** mesurée et un booléen `AllerRetour`. Une propriété calculée `DistanceEffective` vaut `Distance × 2` si `AllerRetour`, sinon `Distance`.
|
||||
- _Décision_ : on stocke la distance simple + le booléen plutôt que la distance déjà doublée, afin de conserver la valeur réelle et d'éviter toute ambiguïté à l'édition.
|
||||
- **Agrégats** : `DistanceEffective` est la seule distance exposée par les DTO (liste, carte, PDF) et utilisée dans tous les totaux et calculs de barème.
|
||||
- **UI** : badge « A/R » dans une colonne dédiée en début de ligne de la liste ; mention « (A/R) » après la distance dans l'infobulle de la carte.
|
||||
- **Migration** : `TrajetAllerRetour` (ajout de la colonne `AllerRetour`).
|
||||
|
||||
### Consultation par mois (Exigence 12, refonte)
|
||||
|
||||
- **Sélecteur temporel** : réduit à année → mois (repliable, libellé « Mois Année »). Output `moisSelectionne: { annee, mois }` (l'ancien segment jour est retiré). Les mois futurs sont désactivés.
|
||||
- _Décision_ : émettre `{ annee, mois }` plutôt qu'une date ISO du 1er du mois, car le conteneur a besoin des deux valeurs sans reparser.
|
||||
- **Conteneur `ConsultationJour`** : deux états — un signal `mois` pilote la **liste mensuelle**, un signal `date` (ISO) pilote la **carte** et la date par défaut d'ajout/insertion.
|
||||
- **Liste** : tous les trajets du mois, groupés par journée (`GroupeJour`), chaque groupe étant un `cdkDropList` **non connecté** aux autres → le glisser-déposer réordonne uniquement au sein d'une journée. Chaque journée est **repliable** ; par défaut tout est replié sauf le dernier jour du mois comportant des trajets.
|
||||
- **Navigation jour** : bande « ‹ Jour préc. / date / Jour suiv. › » au-dessus des totaux et de la carte. Elle ne s'arrête que sur les **jours ayant des trajets** dans le mois ; exception : dans le mois courant, le jour d'aujourd'hui reste navigable même sans trajet. La navigation jour ne recharge que la carte et les totaux du jour, pas la liste.
|
||||
- **Mois vide** : traité comme un cas normal — totaux à 0 et un unique message « Aucun trajet pour ce mois », **sans alerte d'erreur**.
|
||||
|
||||
### Barème multi-tranches et recopie (Exigence 13)
|
||||
|
||||
- **Modèle** : les `Tranche_Bareme` d'une (année, puissance fiscale) ne sont plus figées à trois bornes fixes. `KmMax` est **nullable** pour matérialiser une dernière tranche **illimitée**, et plusieurs tranches peuvent coexister pour une même puissance fiscale (bornes exclusives, coefficient sur 3 décimales).
|
||||
- **Migrations** : `TrancheBaremeKmMaxNullable`, ainsi que les évolutions associées (bornes exclusives, coefficient numérique).
|
||||
- **Recopie** : un endpoint/opération recopie l'intégralité des tranches de la dernière année antérieure comportant un barème vers l'année affichée (annule et remplace après confirmation si l'année cible en contient déjà). L'action est indisponible sur l'année du dernier barème connu.
|
||||
|
||||
### Dénomination de véhicule (Exigence 10)
|
||||
|
||||
Le `Vehicule` porte un champ `Denomination` (libellé libre, ex. « Marque, Modèle », watermark « Ex. Marque, Modèle »). Migration `VehiculeDenomination`.
|
||||
|
||||
### Recalcul ciblé des distances
|
||||
|
||||
Un endpoint `POST /api/trajets/recalculer-distances` recalcule via ORS uniquement les trajets **sans géométrie routière** (`GeometrieJson` NULL), afin de limiter les appels ORS aux seuls trajets calculés en mode dégradé (ex. clé ORS invalide au moment de la saisie). La géométrie de l'itinéraire est portée par le `Trajet` lui-même (paire départ→arrivée), le cache d'adresses servant de repli pour la rétrocompatibilité.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Stratégie globale
|
||||
|
||||
Reference in New Issue
Block a user