Files
seiche/REPRISE.md

171 lines
21 KiB
Markdown
Raw Permalink 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-26 (A1–D4 + proxy + READMEs + **déploiement LXC/NPM ✅**, git) · Statut : **stack complète validée** : serveur 30 tests, front 3 tests + build, Docker build+up, admin créé, `master` poussé sur Gitea · Reste : exécution du déploiement (LXC + règle NPM + domaine) = infra Nicolas (§5 q4/q12)
> 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. TLS : en prod derrière nginx-proxy-manager (Let's Encrypt, q4/q12 §5) ; dev LAN sans proxy = mots de passe en clair (acceptable en dev uniquement).
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** (résolu 2026-08-26) : nginx-proxy-manager (Let's Encrypt) en prod ; dev LAN sans proxy = HTTP accepté (risque dév documenté). | — |
| 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** (résolu 2026-08-26) : pas de VPS — **LXC Proxmox** (Docker + compose dans le LXC), TLS via **nginx-proxy-manager** (Let's Encrypt, déjà fonctionnel). | — |
## 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)
- [x] READMEs (E1) : racine + server + web — dev, prod, env, backup, sécurité (créés 2026-08-26)
## 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 :5173 → 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 5173 (corrigé 2026-08-26), 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`). |
| E4 : Proxy (2026-08-26) | **✅** : `SEICHE_TRUST_PROXY` (Fastify `trustProxy` → rate-limit par IP client derrière proxy) + `SEICHE_COOKIE_SECURE` (cookies Secure derrière TLS). 4 tests (`proxy.test.ts`) : 6 IPs distinctes limitées séparément, XFF ignoré par défaut, Secure on/off. Suite : 30/30. Session précédente interrompue sur cette tâche ; finalisée et commitée. |
| E1 : READMEs (2026-08-26) | **✅** : README racine (démarrage dev/Docker, env, backup, sécurité) + `server/README.md` (scripts, migrations au boot, structure, API) + `web/README.md` (scripts, structure, contrat API). Corrections doc : port dev Vite = 5173 (pas 5199), health = `/api/health` (pas `/api/v1/health`). |
| F1 : Déploiement (2026-08-26) | **✅ décision + config** : prod = **LXC Proxmox** (Docker + compose dans le LXC), TLS via **nginx-proxy-manager** (Let's Encrypt, déjà fonctionnel). `docker-compose.yml` : `SEICHE_COOKIE_SECURE=1` + `SEICHE_TRUST_PROXY=1` (variables E4 enfin câblées). README : section « Production (LXC + NPM) ». Reste côté Nicolas : créer le LXC, règle NPM (hostname + `IP_LXC:8787`), domaine/certificat. |
**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.
**Décision (2026-08-26, Nicolas, remplace « 1 VPS »)** : prod = **auto-hébergement LXC Proxmox** (Docker dans le LXC, compose), TLS par **nginx-proxy-manager** (Let's Encrypt, déjà en place). Exécution infra (LXC, règle NPM, domaine) = côté Nicolas.
**Reste** : READMEs (E1) ✅, décision + config TLS/VPS (F1) ✅. Exécution du déploiement = infra Nicolas (LXC Proxmox, règle NPM, domaine).
## 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
- [x] E1. README.md (dev, prod, env, backup, sécurité) → **fait (2026-08-26)** : README racine + `server/README.md` + `web/README.md`
- [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
- [x] E4. Proxy (Caddy/nginx devant) : `SEICHE_TRUST_PROXY` + `SEICHE_COOKIE_SECURE` + 4 tests → **fait (2026-08-26)**
**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.