mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r4.3): file d'écriture, verrou optimiste D2, Synchro & conflits
Serveur : updatedAt exposé au détail OT ; baseUpdatedAt optionnel sur transition/coche/bilan → 409 « Conflit de version » contextualisé (qui, quand) ; toute écriture secondaire (coche, bilan, commentaire, conso, MO) fait avancer la version — sans version fournie, le web est inchangé. ADR-003 : sécurité & protocole de routage mobile (qui vit où sur l'appareil, purge complète à la déconnexion — correctif réel : la file et le cache persisté survivaient au logout). Mobile : file persistée AsyncStorage rejouée dans l'ordre — succès → propagation de la version fraîche aux saisies restantes du même OT (nos écritures ne se conflictent pas entre elles, un écart étranger reste détecté) ; coupure → tout attend ; refus → CONFLIT, la file s'arrête, l'humain tranche (voir l'OT / rejouer sur version à jour / abandonner). Transitions, coches, bilan et photos (D5, compressées ~1600 px) passent par la file avec patch optimiste du cache ; écran Synchro (badge tabbar ambre/rouge) ; préchargement parc + référentiels (le bilan hors-ligne a ses vocabulaires). Recette « mode avion » 13/13 en Expo web piloté : gestes hors-ligne → 3 en file → modification concurrente de Salma → conflit tranché → serveur Terminé avec bilan. 17 tests jest-expo, 74 tests API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
53
docs/03-architecture/adr/ADR-003-securite-routage-mobile.md
Normal file
53
docs/03-architecture/adr/ADR-003-securite-routage-mobile.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# ADR-003 — Sécurité et protocole de routage de l'app mobile (R4)
|
||||
|
||||
- **Statut** : acceptée (R4.3, 17 juillet 2026)
|
||||
- **Contexte** : l'app technicien est offline-first (D1) — elle garde des données métier
|
||||
et des saisies en attente **sur l'appareil**. Il faut dire précisément ce qui vit où,
|
||||
ce qui le protège, et comment on navigue (deep links compris). Demande du référent
|
||||
pendant la construction de R4.3.
|
||||
|
||||
## Sécurité — ce qui vit sur le téléphone, et ce qui le protège
|
||||
|
||||
| Donnée | Où | Protection | Purge |
|
||||
|---|---|---|---|
|
||||
| Jeton JWT | **SecureStore** (trousseau iOS / Keystore Android) | chiffré par l'OS, jamais dans AsyncStorage | à la déconnexion |
|
||||
| Cache de lecture (OT, parc, référentiels) | AsyncStorage (`siop.cache`, persister TanStack, 7 j max) | sandbox applicative de l'OS | à la déconnexion (loi 09-08 — minimisation) |
|
||||
| File d'écriture (D1) | AsyncStorage (`siop.file.v1`) | sandbox applicative | à la déconnexion — **jamais** un compte ne rejoue les saisies d'un autre |
|
||||
| Photos en attente (D5) | fichier compressé (~1 600 px) référencé par la file | sandbox applicative | sortent de la file à l'envoi |
|
||||
|
||||
Règles :
|
||||
|
||||
1. **L'API reste la seule autorité** : guard JWT global fermé par défaut, matrice de
|
||||
permissions, invariant « voir autre », garde de clôture, verrou optimiste (D2 —
|
||||
`baseUpdatedAt`, 409 contextualisé). Le mobile n'a AUCUNE logique de permission à lui :
|
||||
ce qu'il affiche vient d'une réponse déjà filtrée ; ce qu'il écrit est revalidé serveur.
|
||||
2. Sur **web** (vérifications de dev seulement), SecureStore n'existe pas : repli
|
||||
AsyncStorage assumé. CORS est **opt-in** (`CORS_ORIGINS`, vide par défaut) — les apps
|
||||
natives n'envoient pas d'Origin, le web de prod reste derrière son proxy même-origine.
|
||||
3. **Ni géolocalisation ni audio en R4** (décision D5, loi 09-08) ; la caméra ne sert
|
||||
qu'à lire les étiquettes du parc (scan local, D4) et aux photos d'intervention.
|
||||
4. La déconnexion purge **tout** ce qui précède (`useLogout`) ; la file survit au
|
||||
redémarrage mais pas au changement de session.
|
||||
|
||||
## Protocole de routage (expo-router)
|
||||
|
||||
- **Arborescence** : `/connexion` (publique) ; `/` = aiguillage de session ;
|
||||
`(tabs)/journee|scanner|preventif|synchro` (coquille tabbar) ; fiches empilées
|
||||
`/ascenseur/[id]`, `/ot/[id]`, `/ot/[id]/cloture`, `/ot/[id]/grille`.
|
||||
- **Garde de session** : `/` redirige vers `/connexion` sans session, vers
|
||||
`(tabs)/journee` sinon. Les écrans profonds ne « fuient » rien sans jeton :
|
||||
leurs requêtes reçoivent 401 et l'écran reste vide — la donnée est protégée par
|
||||
l'API, pas par l'obscurité du routage.
|
||||
- **Deep links** : scheme **`siop://`** (app.json). Le scan QR n'ouvre PAS l'URL du
|
||||
portail : il en **extrait la référence** (`analyseScan`) et résout localement
|
||||
(D4) — un QR étranger est refusé avec un message, jamais suivi aveuglément.
|
||||
- **Hors-ligne** : la navigation reste entière (cache + file) ; seul le contenu
|
||||
jamais synchronisé affiche son état honnêtement.
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Recette sécurité mobile : changer de compte démo sur le même appareil → file vide,
|
||||
cache vide, aucune trace de la session précédente.
|
||||
- À durcir plus tard si exigence client : chiffrement applicatif de la file
|
||||
(`expo-secure-store` ne convient pas aux volumes ; envisager SQLCipher/MMKV chiffré),
|
||||
et épinglage TLS.
|
||||
@@ -4,6 +4,25 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-17 — Pr. Daaif (+ Claude) — R4.3 : file d'écriture, verrou optimiste, Synchro & conflits
|
||||
|
||||
**Actions**
|
||||
|
||||
- **Verrou optimiste côté serveur (D2)** : `updatedAt` exposé au détail OT ; `baseUpdatedAt` optionnel sur transition/coche/bilan → **409 « Conflit de version » contextualisé** (qui, quand — depuis le dernier événement). Toute écriture « secondaire » (coche, bilan, commentaire, conso, MO) fait désormais avancer la version, sinon le verrou serait aveugle. Sans version fournie, comportement web inchangé. 4 tests e2e dédiés (74 au total).
|
||||
- **File d'écriture mobile (D1)** : store persisté AsyncStorage (survit au redémarrage, un ENVOI interrompu redevient rejouable), rejeu **dans l'ordre** ; succès → sortie de file **et propagation de la version fraîche** aux saisies restantes du même OT (nos propres écritures ne se conflictent pas entre elles — un écart étranger reste détecté) ; coupure → tout reste en attente ; refus → CONFLIT et la file s'arrête là. Transitions, coches, bilan et **photos (D5** : compressées ~1 600 px, expo-image-picker/manipulator**)** passent par la file, avec patch optimiste du cache (l'OT affiche « Terminé » localement, chips « en file » partout).
|
||||
- **Écran Synchro & conflits** (écran 7) : file visible et horodatée, badge tabbar (ambre → rouge si conflit), carte de conflit avec le message de l'API et les trois choix — voir l'OT, **rejouer sur la version à jour**, abandonner. Préchargement du parc et des référentiels à l'entrée (trou D1 débusqué par la vérif : le bilan hors-ligne n'avait pas ses vocabulaires).
|
||||
- **Sécurité & routage (demande du référent) : ADR-003** — qui vit où sur l'appareil et ce qui le protège (jeton en trousseau, cache/file en sandbox, purge complète à la déconnexion), l'API seule autorité, deep links `siop://` jamais suivis aveuglément (le scan extrait et résout localement). **Correctif réel au passage : la file et le cache persisté n'étaient pas purgés au logout** — un autre compte sur le même téléphone aurait pu rejouer les saisies du précédent.
|
||||
- **Recette « mode avion » rejouée en Expo web piloté : 13/13** — OT ouvert, avion, Démarrer + bilan + clôture hors-ligne (état local immédiat, 3 en file), Salma modifie l'OT pendant ce temps, retour réseau → rejeu auto → **conflit tranché par l'humain** → file vide → serveur : Terminé avec bilan. 17 tests jest-expo (5 sur la file), zéro erreur console inattendue.
|
||||
|
||||
**Décisions**
|
||||
|
||||
- La recette officielle « mode avion en sous-sol » reste à rejouer **sur téléphone** (Expo Go) avec le référent — le web a validé toute la mécanique, pas les capteurs ni le vrai mode avion.
|
||||
- Durcissements notés à l'ADR-003 : chiffrement applicatif de la file et épinglage TLS si exigence client.
|
||||
|
||||
**Prochaine étape** : recette R4 sur appareil avec le référent → tag `release/r4` → R5 IA (design d'abord). Redéploiement Dokploy de `release/r3` toujours en attente côté serveur.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-17 — Pr. Daaif (+ Claude) — R4.2 : scan QR, fiches terrain, grille cochable
|
||||
|
||||
**Actions**
|
||||
|
||||
@@ -6698,6 +6698,11 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -6725,7 +6730,8 @@
|
||||
"report",
|
||||
"costs",
|
||||
"allowedTransitions",
|
||||
"closureBlockers"
|
||||
"closureBlockers",
|
||||
"updatedAt"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -7528,6 +7534,11 @@
|
||||
"comment": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"baseUpdatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7655,6 +7666,11 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUpdatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -7741,6 +7757,11 @@
|
||||
"DONE",
|
||||
"NA"
|
||||
]
|
||||
},
|
||||
"baseUpdatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
Reference in New Issue
Block a user