La matrice n° 15 l'impose ; les colonnes existaient en base depuis WP-01 mais rien ne les remplissait. TOTP (RFC 6238) plutôt qu'un code envoyé par message : le second facteur ne doit pas dépendre du canal qui sert déjà à réinitialiser le mot de passe, faute de quoi un accès à la boîte donnerait les deux d'un coup. L'algorithme est écrit ici plutôt qu'emprunté — trente lignes, et une dépendance de moins sur le chemin d'authentification. Il est éprouvé contre les vecteurs publiés de la RFC, ce qui distingue « le code change toutes les trente secondes » de « le code est celui qu'attend l'application du téléphone ». L'obligation est adossée aux capacités, pas à des rôles nommés qu'un client renomme librement : distribuer les droits, ou lire les rémunérations. Elle est tenue au point de passage de toutes les routes applicatives, où l'écran d'enrôlement remplace le contenu. Il remplace plutôt qu'il ne redirige : une redirection depuis un layout se joue aussi pendant la navigation qui suit la connexion, et Next y répond par une page vide. Le secret n'est enregistré qu'après qu'un code en a été tiré — l'enregistrer d'avance laisserait des comptes porteurs d'un facteur que leur détenteur ne sait pas produire, c'est-à-dire des comptes fermés. Le rejeu d'un code est refusé dans sa propre fenêtre : sans cela le facteur protège d'un mot de passe volé, pas d'un code lu par-dessus l'épaule. Dix codes de secours accompagnent chaque activation, conservés hachés. Et parce que PlanFlow est auto-hébergé et qu'il n'y a pas d'éditeur à appeler, un retrait depuis le serveur existe — l'accès « break glass » que demande la même ligne de la matrice. Sans lui, le second facteur deviendrait le risque principal plutôt que la protection. Un défaut trouvé par les tests, et il aurait été grave : la réactualisation de la route après activation remplaçait l'écran par celui d'un compte déjà enrôlé, emportant les codes de secours avant que leur destinataire ait pu les noter. La suite de tests suit le produit : la mise en place enrôle réellement le compte de direction et calcule les codes comme le ferait un téléphone. Les écrans qui n'éprouvent pas l'authentification ont migré vers la session commune — deux connexions parallèles sur un même compte se heurtent au refus de rejeu, ce qui est le comportement voulu et non un défaut à contourner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
156 lines
7.6 KiB
Markdown
156 lines
7.6 KiB
Markdown
# PlanFlow
|
|
|
|
Gestion du personnel, des plannings et des temps, multi-établissements, auto-hébergée. La paie est **exportée vers Silae** ; PlanFlow ne produit ni bulletin ni DSN.
|
|
|
|
La spécification de construction est [`PLAN.md`](PLAN.md). Elle est normative : en cas d'écart entre le code et le plan, c'est le plan qui a raison, ou le plan qui doit être corrigé — jamais l'écart qui s'installe.
|
|
|
|
| Document | Rôle |
|
|
|---|---|
|
|
| [`PLAN.md`](PLAN.md) | Spécification : périmètre, modèle de données, règles, lots de travail |
|
|
| [`matrice-conformite-rh-france-2026.md`](matrice-conformite-rh-france-2026.md) | Exigences réglementaires françaises, cotées P0/P1/P2 |
|
|
| [`Audit Combo/`](Audit%20Combo/INDEX.md) | Audit fonctionnel du produit de référence |
|
|
|
|
## État
|
|
|
|
Neuf lots livrés. **Aucun écran ne lit plus de données de démonstration** : le
|
|
répertoire `src/lib/demo` a disparu.
|
|
|
|
| Lot | Contenu |
|
|
|---|---|
|
|
| WP-00 | Socle : Next.js, PostgreSQL, CSP restrictive, CI, sauvegardes |
|
|
| WP-01 | Tenancy, identité, autorisation : RLS, audit append-only, 70 capacités, cinq rôles |
|
|
| WP-02 | Référentiels et registre de paramétrage juridique |
|
|
| WP-03 | Dossiers salariés, contrats et avenants, forfait jours |
|
|
| WP-04 | Planning : quatre vues, publication par équipe, impression |
|
|
| WP-05 | Moteur de règles de convention, effectif-daté |
|
|
| WP-06 | Absences, registre de compteurs, calendrier |
|
|
| WP-07 | Heures prévu/réalisé/payé, périodes de paie verrouillables |
|
|
| WP-08 | Export Silae, format relevé sur un export réel du dossier |
|
|
| WP-09 | Tableau de bord RH : indicateurs explicables |
|
|
|
|
Restent WP-10 (documents) et WP-11 (communication, optionnel), ainsi que deux
|
|
points nommés à l'intérieur des lots livrés : la régularisation automatique sur
|
|
la période suivante et l'information au retour d'arrêt.
|
|
|
|
### Ce qui attend une décision du client
|
|
|
|
Ces points sont des **signaux d'arrêt** au sens de `PLAN.md` : ils ne se
|
|
devinent pas.
|
|
|
|
- Les **codes d'absence Silae** (`AB-100`, `AB-200`, `AB-300`, `AB-630`) : leur
|
|
existence est connue, leur signification non. L'export refuse de tourner tant
|
|
que la correspondance n'est pas confirmée.
|
|
- Le **régime dominical** applicable aux magasins, qui détermine si la
|
|
majoration de 100 % et le repos compensateur sont les bonnes contreparties.
|
|
- Les **règles applicables aux mineurs**, sans source primaire au dossier.
|
|
|
|
## Démarrer
|
|
|
|
### Avec Docker
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
# Renseigner POSTGRES_PASSWORD et ENCRYPTION_KEY (voir ci-dessous)
|
|
docker compose up --build
|
|
```
|
|
|
|
L'application écoute sur <http://localhost:3000>. Les migrations s'appliquent au démarrage du conteneur.
|
|
|
|
### En local
|
|
|
|
Nécessite Node 22, pnpm 10 et un PostgreSQL 16 accessible.
|
|
|
|
```bash
|
|
pnpm install
|
|
cp .env.example .env # renseigner DATABASE_URL et ENCRYPTION_KEY
|
|
pnpm db:generate
|
|
pnpm db:deploy
|
|
pnpm dev
|
|
```
|
|
|
|
### Clé de chiffrement
|
|
|
|
`ENCRYPTION_KEY` chiffre au repos les colonnes sensibles exigées par le plan (§3.6) : NIR, IBAN, BIC.
|
|
|
|
```bash
|
|
openssl rand -base64 32
|
|
```
|
|
|
|
Elle vit **hors de la base** : une sauvegarde volée ne doit pas suffire à lire ces colonnes. La perdre rend ces données irrécupérables — la sauvegarder séparément et documenter sa rotation. Elle chiffre également les secrets de second facteur et le mot de passe du serveur d'envoi.
|
|
|
|
### Second facteur — accès de secours
|
|
|
|
Les rôles qui lisent les rémunérations ou distribuent les droits doivent porter un second facteur (matrice n° 15) : tant qu'il n'est pas activé, l'application ne leur ouvre aucun écran. Chaque activation délivre dix codes de secours, affichés **une seule fois**.
|
|
|
|
PlanFlow étant auto-hébergé, il n'y a pas d'éditeur à appeler si un administrateur perd à la fois son téléphone et ses codes. Le retrait se fait alors depuis le serveur :
|
|
|
|
```bash
|
|
pnpm mfa:reset adresse@example.fr
|
|
```
|
|
|
|
Le retrait révoque les sessions ouvertes et s'inscrit au journal d'audit. Il n'est délibérément pas exposé dans l'application : l'exécuter demande déjà un accès au serveur, c'est-à-dire davantage que ce que le second facteur protège.
|
|
|
|
## Vérifier
|
|
|
|
```bash
|
|
pnpm verify # typecheck + lint + tests unitaires
|
|
pnpm test:e2e # build, serveur standalone, tests de bout en bout
|
|
```
|
|
|
|
`pnpm verify` est ce que la CI exécute sur chaque *pull request*, suivi du build et des tests end-to-end.
|
|
|
|
## Configuration de la base — à ne pas rater
|
|
|
|
**L'application ne doit pas se connecter en superutilisateur PostgreSQL.**
|
|
|
|
Un superutilisateur contourne la *row-level security*, y compris déclarée en `FORCE`. Connecter PlanFlow avec un tel compte désactive silencieusement la seconde couche d'isolation multi-tenant : les requêtes fonctionnent, les tests applicatifs passent, et rien n'indique que la protection a disparu — jusqu'au jour où quelqu'un lit les données d'un autre établissement.
|
|
|
|
```sql
|
|
CREATE ROLE planflow_app LOGIN PASSWORD '…' NOSUPERUSER NOBYPASSRLS;
|
|
GRANT USAGE ON SCHEMA public TO planflow_app;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO planflow_app;
|
|
```
|
|
|
|
Les migrations, elles, s'appliquent avec un compte propriétaire distinct.
|
|
|
|
L'application vérifie ce point au démarrage : elle refuse de démarrer en production sur une base mal configurée, et se contente d'un avertissement en développement. `GET /api/sante` expose l'état sous `tenantIsolation`.
|
|
|
|
## Choix structurants
|
|
|
|
**Aucun traceur tiers.** L'audit du produit de référence a intercepté 2102 requêtes de traçage — Segment, LinkedIn Ads, Google Ads, DoubleClick, Clarity, Hotjar — et aucune requête métier. Une application RH ne doit pas envoyer un contexte de navigation portant sur des salariés identifiables à des régies publicitaires. Deux garde-fous rendent la règle vérifiable plutôt que déclarative :
|
|
|
|
- une Content-Security-Policy qui ne nomme **aucune** origine externe, posée par requête avec un nonce (`src/proxy.ts`) ;
|
|
- un test qui échoue si une dépendance de traçage apparaît dans `package.json`.
|
|
|
|
Pour de la télémétrie technique, passer par une interface abstraite auto-hébergée.
|
|
|
|
**Le serveur testé est celui qui est déployé.** Les tests end-to-end lancent le serveur `standalone`, celui que l'image Docker exécute — pas `next dev`, dont la politique de sécurité est volontairement plus permissive.
|
|
|
|
## Structure
|
|
|
|
```
|
|
src/
|
|
├── app/ écrans (App Router)
|
|
├── lib/
|
|
│ ├── env.ts contrat d'environnement, validé à l'import
|
|
│ └── security/csp.ts politique de sécurité, fonction pure et testable
|
|
├── server/
|
|
│ ├── db.ts client Prisma — le scoping multi-tenant s'y greffe au WP-01
|
|
│ └── health.ts
|
|
└── proxy.ts en-têtes de sécurité par requête
|
|
prisma/ schéma et migrations
|
|
tests/
|
|
├── unit/ Vitest
|
|
└── e2e/ Playwright
|
|
```
|
|
|
|
## Écarts assumés par rapport au plan
|
|
|
|
Trois choix diffèrent de ce qu'annonçait `PLAN.md` §2, et le plan a été mis à jour en conséquence.
|
|
|
|
| Sujet | Plan initial | Retenu | Raison |
|
|
|---|---|---|---|
|
|
| Next.js | 15 | **16.3** | Version stable courante ; démarrer un greenfield une majeure en retard n'a pas de contrepartie. |
|
|
| Authentification | Auth.js v5 | **Sessions maison en base** | Auth.js v5 est encore en beta. Le besoin se limite à identifiants et invitation, sans OAuth, et la matrice de conformité (n° 23) impose la **révocation de session** — immédiate avec des sessions en base, malaisée avec des jetons JWT. |
|
|
| Convention Next | `middleware.ts` | **`proxy.ts`** | Next 16 a renommé la convention ; `middleware` est déprécié. |
|