Adds the data model for accounts, locations, teams, users, memberships and scopes, plus roles, the 70-capability catalogue, database-backed sessions, and the audit log. Isolation is enforced twice, independently. A Prisma extension injects accountId into every query, and PostgreSQL row-level security filters underneath it, keyed on a transaction-local setting. The first alone leaves raw queries unguarded; the second alone returns empty results without saying why. Integration tests prove both against a real database rather than through the application layer, which would only prove the application layer. They create a restricted role to do it — and that exposed a trap worth naming: **a PostgreSQL superuser bypasses row-level security even with FORCE**. Connecting the app as one silently disables the second layer while every application test still passes. checkTenantIsolation now refuses to start in production on such a database, warns in development, and reports through /api/sante. The README explains the role to create. The audit log is append-only by trigger, so it resists even a superuser: a trail that can be rewritten proves nothing. Entries carrying an adjustment or an unlock are rejected without a justification, and known secret-bearing fields are redacted before writing — the log is read, exported and kept for years, so it must not become a second unencrypted copy of what is encrypted elsewhere. Sensitive columns use AES-256-GCM with the key held outside the database. Sign-in verifies a dummy hash for unknown accounts so timing does not enumerate addresses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
116 lines
5.4 KiB
Markdown
116 lines
5.4 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
|
|
|
|
**WP-00 — socle** et **implémentation du design** : six écrans sur données de démonstration.
|
|
|
|
**WP-01 — tenancy, identité, autorisation** : en cours. Modèle de données, RLS, journal d'audit append-only, catalogue de 70 capacités, cinq rôles, sessions en base. Les écrans lisent encore `src/lib/demo`.
|
|
|
|
## 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.
|
|
|
|
## 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é. |
|