Files
seiche/REPRISE.md
T

151 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Projet « Seiche » — doc de reprise
> À jour : 2026-08-24 (A1–A5 ✅) · Statut : **scaffold backend installé, compilé, linté, boot + setup admin + smoke login OK** — front absent
> Mode d'emploi : doc auto-suffisant. En reprise avec un assistant IA : lui faire lire ce fichier en entier, traiter le §5, puis **planifier les tâches restantes une par une** (§9 + §11).
> Mode de travail imposé par Nicolas : **petites tâches, planifier avant d'exécuter, ne jamais lancer de run autonome**.
## 1. Projet en une phrase
Appli web auto-hébergée, **réseau local uniquement, mono-utilisateur** (Nicolas), qui gère un service externe via son API REST. Accès protégé par mot de passe. Le périmètre va grossir et devenir sensible : rigueur et sécurité **dès le départ**, pas en rattrapage.
## 2. Exigences (telles que déclarées)
- Gestion d'un service externe via API REST (service + périmètre MVP **à préciser**, §5)
- Auto-hébergée, accessible uniquement en réseau local, par le propriétaire
- Protection par mot de passe obligatoire (LAN ≠ confiance totale)
- Deviendra « compliqué et sensible » → fondations sérieuses dès le starter
- Stack imposée : Docker, SQLite, backend Node, front React (ou équivalent validé), tests
## 3. Stack recommandée (défauts à valider)
| Couche | Choix | Pourquoi |
|---|---|---|
| Langage | TypeScript partout (strict) | Une seule langue, contrats API↔front typés |
| Backend | Node 22 LTS + Fastify 5 | Rapide, validation Zod, erreurs propres, plugins |
| DB | SQLite (better-sqlite3) + Drizzle ORM | Migrations versionnées, typage, mode WAL |
| Front | Vite + React 18 + React Router + TanStack Query | Fetch/caching de l'API REST sans douleur |
| UI | Tailwind CSS + shadcn/ui | Dashboard admin pro, composants en repo |
| Auth | Argon2id + session cookie httpOnly + CSRF | Solide et simple, pas de JWT d'État |
| Tests | Vitest (API via inject, DB de test) + React Testing Library | Tooling TS unique, rapide |
| Qualité | Biome (lint + format) | Un seul outil, très rapide |
| Conteneur | Dockerfile multi-stage + compose, user non-root | Image fine, SQLite en volume |
## 4. Sécurité — non négociables
1. Hash **Argon2id** (sel par compte), jamais en clair, jamais de hash faible.
2. **Rate-limit** sur /login (anti brute-force).
3. Session : cookie **httpOnly + SameSite=Lax** (+ Secure si TLS), durée limitée, révocation au logout.
4. **CSRF** : token sur toutes les mutations (POST/PUT/DELETE).
5. Secrets **uniquement en variables d'env** (`.env` hors repo, `.env.example` versionné).
6. Logs **pino** structurés : jamais de mot de passe / token / cookie dans les logs.
7. SQLite **hors de l'image** : volume nommé `/data/app.db` + **script de backup** testé.
8. LAN sans TLS = mots de passe en clair sur le réseau → à trancher (§5 q4) : Caddy self-signed, Tailscale, ou HTTP assumé.
9. Dépendances : lockfile figé en prod, `npm audit` au rythme des maj.
10. Surface API minimale, pas de debug en prod.
## 5. Questions ouvertes (à répondre avant le MVP)
| # | Question | Défaut si pas de réponse |
|---|---|---|
| 1 | **Quel service externe** ? URL, doc API, mode d'auth (token statique / OAuth / Basic ?) | Client isolé `server/src/external/` (adaptateurs), config env, mock en dev |
| 2 | **Périmètre concret du MVP** ? (lister X, modifier Y, déclencher Z, logs…) | 1 CRUD exemple + health |
| 3 | Un seul compte, ou plusieurs possibles ? | Schéma multi-comptes, 1 compte créé au 1er boot |
| 4 | **TLS en LAN** ? (Caddy / Tailscale / HTTP accepté) | HTTP + risque documenté dans le README |
| 5 | UI Tailwind + shadcn/ui, interface en français ? | Oui / FR |
| 6 | Persistance : volume nommé + backup régulier OK ? | Oui |
| 7 | Node 22 LTS dans les conteneurs ? | Oui |
| 8 | ✅ **Docker opérationnel** (résolu 2026-08-23) : daemon 29.7.2 démarré (Arch Linux), compose 5.5.0 (plugin + standalone). | — |
| 9 | **Gitea auto-hébergé** : URL + nom du repo à confirmer. Accès **SSH** choisi ; Nicolas configure la clé le 2026-08-24. (rien sur `localhost:3000` au 2026-08-23) | `git init` local d'abord, remote Gitea dès que la clé sera opérationnelle |
## 6. Architecture
- **1 conteneur = API + front prébuildé** servi par Fastify → zéro CORS, 1 port, déploiement simple.
- `server/` (Fastify) : plugins (auth, error-handler, rate-limit) → routes `/api/v1/*` → DB + client externe.
- `web/` (Vite+React) : pages, guard de session, TanStack Query devant `/api/v1`.
- Service externe : **un seul package** `server/src/external/` (http-client, cache, types, adaptateurs) — le seul endroit qui le connaît.
- SQLite en **WAL**, migrations Drizzle appliquées au boot.
## 7. Structure du repo (état réel, inventaire 2026-08-23)
✅ = présent · ❌ = manquant
```
seiche/
├── REPRISE.md ✅ · README.md ❌
├── package.json ✅ · pnpm-workspace.yaml ✅ · biome.json ✅ · tsconfig.base.json ✅
├── .env.example ✅ · .gitignore ✅ · .dockerignore ❌
├── Dockerfile ❌ · docker-compose.yml ❌ · Makefile ❌
├── server/ ✅ (scaffold ~850 lignes — non installé, non compilé, non testé)
│ ├── src/
│ │ ├── main.ts · app.ts · config.ts · validation.ts
│ │ ├── db/ (client, schema) · migrations ❌ (à générer)
│ │ ├── auth/ (password, session+CSRF, rate-limit)
│ │ ├── routes/ (auth, external, health)
│ │ ├── external/ (http-client, cache, types, adapters/{http,example,index})
│ │ └── setup.ts ❌ (referencé par `pnpm setup`, absent)
│ ├── drizzle.config.ts · vitest.config.ts · tsconfig.json
│ └── test/ ❌ (aucun test)
└── web/ ❌ (front entier absent)
```
## 8. Starter pack — Definition of Done
- [ ] `pnpm dev` : API + web en dev, login → dashboard
- [ ] `docker compose up` : 1 conteneur, SQLite persisté en volume
- [ ] Auth complète : Argon2id, session httpOnly, CSRF, logout, rate-limit /login
- [ ] Table `users` + migration Drizzle, 1er compte créé au 1er boot
- [ ] `/api/v1/health` public, le reste protégé
- [ ] Tests verts (Vitest) : login ok/échec, CSRF, error-handler, DB round-trip
- [ ] Biome + `tsc --noEmit` clean
- [ ] README : dev, prod, env, backup, sécurité
## 9. Plan d'exécution (session de reprise)
1. Lire ce doc → répondre au §5 (ou valider les défauts) → loger les réponses dans §10.
2. ~~Scaffolder le monorepo~~ → **partiel (2026-08-23)** : racine ✅, `server/` ✅ (non vérifié), `web/` ❌.
3. Backend : config env → DB (Drizzle, WAL, migration) → auth (Argon2id, session, CSRF, rate-limit) → routes. → **code écrit ; reste : installer, migrer, compiler, tester**.
4. Front : Vite+React, login, guard, dashboard minimal, client API. → **à faire**.
5. Docker : Dockerfile multi-stage + compose + volume.
6. Tests + lint + typecheck → cocher le §8.
7. **Mettre à jour ce doc** (statut + §10) à la fin.
## 10. Journal des décisions
| Date | Décision |
|---|---|
| 2026-08-23 | Stack proposée : TS / Node22+Fastify / SQLite+Drizzle / Vite+React+Tailwind+shadcn / Vitest / Biome (à valider) |
| 2026-08-23 | 1 conteneur pour API + front (pas de CORS, déploiement simple) |
| 2026-08-23 | Workflow fichiers : contenu ajouté par petits blocs en edit (les gros writes timoutent) |
| 2026-08-23 | Docker 29.7.2 présent, plugin compose absent sur l'hôte → à résoudre avant test prod |
| 2026-08-23 | Scaffold `server/` écrit de manière autonome (21 fichiers, ~850 lignes) — **non installé, non compilé, non testé**. Front `web/` absent. |
| 2026-08-23 | Divergences du scaffold vs doc, à trancher : driver SQLite `@libsql/client` (pas better-sqlite3) ; service externe en package `external/` avec adaptateurs (pas un seul fichier) ; rate-limit custom (pas @fastify/rate-limit). |
| 2026-08-23 | Mode de travail imposé : planifier chaque tâche en mode plan, exécuter en petites tâches validées par Nicolas. Inventaire complet dans §11. |
| 2026-08-23 | Audit outils : node v26.7.0 (hôte — conteneur à fixer en 22 LTS), pnpm 11.3.0 ✅, npm 12, git 2.55 (identité user.name/email **non configurée**), sqlite3 CLI 3.53 ✅, make ✅, curl ✅. Gitea : rien sur `localhost:3000` → URL + accès à fournir par Nicolas. |
| 2026-08-23 | Re-test Docker : daemon démarré (server 29.7.2), `docker ps` OK (0 conteneur), compose 5.5.0 présent en plugin **et** standalone. Prêt pour la tâche Docker du §11. Gitea : accès SSH, clé à configurer par Nicolas le 2026-08-24. |
| 2026-08-24 | **A1–A4 faits** : `pnpm install` ✅, migration 0000 générée + appliquée au boot ✅, `tsc --noEmit` clean ✅, Biome clean ✅. Corrections : `biome.json` migré Biome 2.x (`preset`, `clientKind`, `noConsole`, patterns d'ignore), 12 auto-fixes, import inutile retiré, `pino-pretty` ajouté en devDep (boot cassé sans). **A5** : `setup.ts` créé (idempotent, refuse d'écraser, mdp admin ≥ 12, guard sans creds = erreur propre) — smoke : setup + idempotence + boot + `/api/health` + login ok (cookie + CSRF) + session + mdp faux → 401, tous ✅. |
| 2026-08-24 | **B1–B2 faits** : `tests/helpers.ts` (sqlite :memory:, unique par worker, applyMigrations, seedUser, cookies) + `tests/auth.test.ts` (6 tests : health, login ok/échec, corps invalide, argon2) + `tests/api.test.ts` (9 tests : health, login, 401/403 CSRF double-submit, logout + session morte, rate limit 429, error-handler 500 sans fuite, DB round-trip). Suite : 26/26 ✅, tsc ✅, biome ✅. |
| 2026-08-24 | **C3 fait** : `lib/session.ts` (`useSession()` sur `sessionQueryKey`, cache partagé login→guard→dashboard) ; `components/RequireAuth.tsx` (pending = spinner plein écran, `isError` → `navigate('/login', {replace:true})` via useEffect) ; `DashboardPage.tsx` réel (username, expiry locale `fr-FR`, bouton logout → `api.logout` + `removeQueries` + navigate — le catch du logout couvre la session déjà morte côté serveur) ; `App.tsx` : `/login` public, `/` sous `RequireAuth`, catch-all `*` → `/` ; `main.tsx` : QueryClient avec `staleTime: 30s` et `retry` qui exclut les 4xx (pas de retry sur 401/400). Pitfall TS noté : le narrowing de `session` ne propage pas dans le closure `handleLogout` → bind `const s = session` après le guard. tsc ✅, build ✅, biome ✅. |
| 2026-08-24 | **C2 fait** : client API `web/src/lib/api.ts` — `request()` fetch + `credentials: same-origin` + content-type JSON (merge headers corrigé : le spread `...init` écrasait le content-type des POST), `ApiError{status,code,retryAfterMs}`, `sessionQueryKey` pour le cache TanStack. `LoginPage.tsx` — formulaire contrôlé, `useMutation(api.login)`, validation miroir (user 1–64, pass 1–256), `onSuccess` → `setQueryData(sessionQueryKey)` + navigate `/`, erreurs mappées (invalid_credentials / too_many_attempts + retryAfterMs / 500). Contrat aligné `server/src/routes/auth.ts` (codes `invalid_body|invalid_credentials|too_many_attempts|not_authenticated`). tsc ✅, build ✅, biome ✅. |
| 2026-08-24 | **C1 fait** : `web/` scaffoldé (workspace `seiche-web` déjà câblé en racine) : Vite 7 + React 19 + TS strict + Tailwind 4 (plugin vite) + react-router 7 + TanStack Query 5, proxy dev `/api`→8787, `index.html` + entry + App (routes `/` et `/login` placeholders) + `tests/setup.ts` (jsdom + jest-dom 7.0.1 — 6.10 est déprécié/broken, peer `@testing-library/dom` 10.x déclaré). Vérif : tsc ✅, `vite build` ✅ (CSS TW émis), `vitest run` passWithNoTests ✅, biome ✅, dev server :5199 → HTTP 200. |
## 11. État du repo — inventaire détaillé (2026-08-23)
**Faits vérifiés :**
- `pnpm install` **jamais lancé** : pas de `node_modules`, pas de `pnpm-lock.yaml` → aucun fichier du scaffold n'a été compilé ni exécuté.
- Migrations Drizzle **absentes** (`drizzle-kit generate` non lancé) alors que `db/client.ts` les applique au boot.
- `server/src/setup.ts` **manquant** alors que `server/package.json` le référence (`pnpm setup`).
- Pas de dépôt git.
**Plan d'exécution détaillé (2026-08-23, validé par Nicolas — petites tâches, doc mise à jour après chacune) :**
- [x] A1. `pnpm install` (workspace racine + server)
- [x] A2. Migrations Drizzle : `drizzle-kit generate` (0000_heavy_saracen)
- [x] A3. `tsc --noEmit` (fixer les erreurs de compilation)
- [x] A4. `biome check` (fixer lint/format)
- [x] A5. `server/src/setup.ts` (compte admin initial) + smoke test : boot, `/api/v1/health`, login
- [x] B1. Tests server : helper DB de test + login ok/échec (`tests/helpers.ts` + `tests/auth.test.ts`, 6 tests)
- [x] B2. Tests server : CSRF + error-handler + DB round-trip (+rate limit) — `tests/api.test.ts`, 9 tests ; suite : 26/26
- [x] C1. Scaffold `web/` (Vite+React+TS+Tailwind+Router+TanStack Query) — 2026-08-24, 10 fichiers, typecheck/build/biome ✅, dev :200, proxy `/api`→8787
- [x] C2. Front : client API + CSRF + page login — 2026-08-24 : `lib/api.ts` (fetch + cookies, `ApiError`, `sessionQueryKey`, login/session/logout) + `LoginPage.tsx` (useMutation, validation miroir serveur, mapping erreurs 401/429/500) ; tsc/build/biome ✅
- [x] C3. Front : guard session + dashboard minimal + wiring API — 2026-08-24 : `useSession()` (hook TanStack), `RequireAuth` (pending → spinner, erreur → redirect `/login`), `DashboardPage` réel (user + expiry + logout), QueryClient global (staleTime 30 s, pas de retry 4xx) ; tsc/build/biome ✅
- [x] C4. Tests front (smoke : render login, redirect) — 2026-08-24 : `web/tests/smoke.test.tsx` (vitest+jsdom+RTL, fetch stubbé) : rendu formulaire login, redirect non-auth → /login, login → dashboard ; `cleanup()` explicite dans `tests/setup.ts` (auto-cleanup inactif sans `globals`) ; QueryClient factorisé `web/src/lib/queryClient.ts` (partagé main.tsx ↔ tests) ; tsc/build/biome ✅, 3/3 tests
- [x] D1. Dockerfile multi-stage (Node 22, user non-root) — 2026-08-24 : `Dockerfile` racine (PAS multi-stage au final : le serveur n'a pas d'étape de build, TS exécuté via tsx ; base `node:22-slim` glibc pour les prébuilds `argon2`) : pnpm workspace frozen-lockfile, front `vite build` servi par @fastify/static (`SEICHE_PUBLIC_DIR=/app/web/dist`), `WORKDIR /app/server` (sinon `node --import tsx` ne résout pas tsx avec le layout pnpm), `USER node`, `SEICHE_HOST=0.0.0.0`, `VOLUME /data`, HEALTHCHECK `/api/health` ; `.dockerignore` ; testé : `docker build` + `run` → health ok, `/` et `/login` en 200
- [x] D2. docker-compose.yml + .dockerignore + volume /data — 2026-08-24 : `docker-compose.yml` racine (build local, image `seiche:latest`, port 8787, volume nommé `seiche-data:/data`, `restart: unless-stopped`, aucune variable requise au runtime) ; testé : up → health ok, `seiche.db` créé dans le volume (owner node)
- [x] D3. Test prod : build image, up, login via API, persistence volume — 2026-08-24 : image buildée + up compose → `/api/health` ok, front 200 ; persistance : `down` (sans -v) → `up` → db intacte ; `setup` exécuté DANS le conteneur (admin créé dans /data/seiche.db) ; `POST /api/auth/login` → 200 avec session + csrfToken
- [x] D4. Script backup SQLite (testé) — 2026-08-24 : `scripts/backup.sh [dir_sortie]` (défaut `backups/`, gitignored) : `VACUUM INTO` dans le conteneur (snapshot cohérent EN LIGNE via `@libsql/client`, le serveur continue de servir) → `docker compose cp` HORS du volume (un backup dans le même volume n'est pas un backup) → nettoyage du tmp → vérif en-tête SQLite + non-vide ; testé : backup de 40K créé, ré-ouvert dans le conteneur et interrogé (`users` → admin présent), /data propre après exécution ; prérequis : conteneur en marche
- [ ] E1. README.md (dev, prod, env, backup, sécurité)
- [ ] E2. `git init` + commit initial (identité git à fournir par Nicolas)
- [ ] E3. Update §8 (Definition of Done) + statut final de ce doc
**Divergences du scaffold vs doc (à valider ou corriger) :**
- Driver SQLite : `@libsql/client` (scaffold) vs `better-sqlite3` (§3).
- Service externe : package `server/src/external/` avec adaptateurs (scaffold) vs module unique `clients/external.ts` (§6) — fonctionnellement équivalent, à confirmer.