Files
seiche/REPRISE.md
T

164 lines
19 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-25 (A1–D4 ✅ + git) · Statut : **stack complète validée** : serveur 26 tests, front 3 tests + build, Docker build+up, admin créé, `master` poussé sur Gitea · Reste : READMEs (E1), TLS LAN + VPS (décision §5)
> 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 opérationnel** (2026-08-25) : remote SSH `gitea-lab:nicolas/seiche`, compte collaborateur `bot-claude` (clé `id_ed25519_gitea_bot_claude`), `master` poussé, tracking OK. | — |
| 12 | **VPS / mise en prod** : VPS non provisionné au 2026-08-25. Où ? Image ? Domaine/TLS ? | Développer en LAN ; prod = 1 tâche dédiée après décision |
## 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-25)
✅ = présent · ❌ = manquant
```
seiche/
├── REPRISE.md ✅ · README.md ❌ (E1 — à créer)
├── package.json ✅ · pnpm-workspace.yaml ✅ · biome.json ✅ · tsconfig.base.json ✅
├── .env.example ✅ · .gitignore ✅ · .dockerignore ✅
├── Dockerfile ✅ · docker-compose.yml ✅ (port 8787, volume `seiche-data`) · Makefile ❌
├── scripts/backup.sh ✅ (VACUUM INTO dans le conteneur — testé)
├── server/ ✅ (installé, compilé, 26 tests ✅)
│ ├── src/
│ │ ├── main.ts · app.ts · config.ts · validation.ts · setup.ts ✅
│ │ ├── db/ (client, schema) · migrations/ ✅ (0000)
│ │ ├── auth/ (password, session+CSRF, rate-limit)
│ │ ├── routes/ (auth, external, health)
│ │ └── external/ (http-client, cache, types, adapters)
│ ├── drizzle.config.ts · vitest.config.ts · tsconfig.json
│ └── tests/ ✅ (auth, api, session, rate-limit, password — 26 tests)
├── web/ ✅ (Vite+React19+TS strict+Tailwind4+Router7+TanStack5 — build ✅, 3 tests)
│ └── src/ (main.tsx · App.tsx · lib/{api,session,queryClient} · components/RequireAuth · pages/{LoginPage,DashboardPage})
└── .vscode/ ✅ (extensions + settings)
```
## 8. Starter pack — Definition of Done
- [x] `pnpm dev` : API + web en dev, login → dashboard (2026-08-24 : HTTP 200 ; 2026-08-25 : front connecté, session persistée)
- [x] `docker compose up` : 1 conteneur, SQLite persisté en volume (image buildée 30s, conteneur up, health 200, données persistées)
- [x] Auth complète : Argon2id, session httpOnly, CSRF, logout, rate-limit /login (smoke 2026-08-24 + tests 2026-08-25 : login 401/429, logout, me)
- [x] Table `users` + migration Drizzle, 1er compte créé au 1er boot (migration 0000 générée ; `pnpm setup` idempotent, admin créé)
- [x] `/api/v1/health` public, le reste protégé (401 sans session, 200 avec — testé)
- [x] Tests verts (Vitest) : 26 tests serveur + 3 tests front, tous OK
- [x] Biome + `tsc --noEmit` clean (les 2 paquets + tsc web)
- [ ] READMEs (E1) : racine + server + web — dev, prod, env, backup, sécurité (à créer)
## 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~~ → **terminé (2026-08-25)** : racine ✅, `server/` ✅ (26 tests), `web/` ✅ (3 tests + build).
3. ~~Backend : config env → DB (Drizzle, WAL, migration) → auth → routes~~ → **terminé** (boot, migration, setup admin, smoke, 26 tests).
4. ~~Front : Vite+React, login, guard, dashboard minimal, client API~~ → **terminé** (login/déconnexion, guard, dashboard, client API, 3 tests + build).
5. ~~Docker : Dockerfile multi-stage + compose + volume~~ → **terminé** (image ~235 Mo, build 30s, up OK).
6. ~~Tests + lint + typecheck~~ → **terminé** (section §8 mise à jour 2026-08-25).
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. |
### Journal des sessions 2026-08-25
| Tâche | Résultat |
| --- | --- |
| C2 : tests serveur | **✅ 26 tests / 7 fichiers** : rate-limit (429 au 5e), api (401/200/401), session (httpOnly), password (argon2id), auth (login/logout/me, CSRF, 400). 1er run : 4 échecs — `fetch` n'interceptait pas `localhost` (Node 20) → helper `tests/http.ts` avec regex IP/localhost. Suite verte. |
| D1 : front | **✅ scaffold web/ + pages** : LoginPage (email/mot de passe/erreurs), RequireAuth (redirect /login), DashboardPage (api-client → login/logout), client API (fetch relatif, httpOnly), queryClient (mutate login/logout). **3 tests** (App render, RequireAuth login→redirect, queryClient) + `tsc` + `vite build` OK. Port 5199, proxy Vite → 8787. |
| D2 : `pnpm dev` | **✅** : `concurrently` serveur + Vite ; login navigateur OK, dashboard, déconnexion → /login. |
| D3 : Docker | **✅** : Dockerfile multi-stage (deps → build → node:22-slim, `nonroot`, healthcheck) ; image 235 Mo, build ~30 s ; compose port **8787** (127.0.0.1), volume `seiche-data` ; données persistées (reboot → admin toujours là) ; `scripts/backup.sh` testé. |
| Git + Gitea | **✅** : `git init` `master`, commit `581f7e1` (69 fichiers) poussé sur `gitea-lab:nicolas/seiche` (compte `bot-claude`, clé `id_ed25519_gitea_bot_claude`). `.gitignore` corrigé (`.env` uniquement, pas `.env.example`). |
**Décision (2026-08-25, Nicolas)** : en prod, **1 seul VPS** (pas encore provisionné — q12 §5) ; **développement en local réseau (LAN)** jusqu'à la fin du développement ; **aucun compte Google/external** (q11 §5) — le scaffold `external/` est un futur point d'extension, la doc se limite à une route auth.
**Reste (E)** : READMEs racine/server/web (E1), TLS LAN + VPS (après décision §5).
## 11. État du repo — inventaire détaillé (2026-08-23)
**Faits vérifiés** (état initial du scaffold, 2026-08-23/24 — corrigés par A1–D4, voir §10) :
- `pnpm install` jamais lancé, migrations absentes, `setup.ts` manquant, pas de dépôt git → **tous corrigés** (install + migration 0000 + setup admin + git/Gitea).
**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é)
- [x] E2. ~~`git init` + commit initial~~ → **fait (2026-08-25)** : `master`, commit `581f7e1`, poussé sur Gitea (`gitea-lab:nicolas/seiche`)
- [x] E3. ~~Update §8 + statut final~~ → **fait (2026-08-25)** : §8 coché, §7/§9/§10/§11 mis à jour
**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) — **choix retenu : le scaffold `external/`** ; q11 : Google/external = futur, hors périmètre doc.