chore: bootstrap project (sprint 0)

Lay down the project foundation before Sprint 1 implementation:

- SPEC.md enriched with a "Décisions techniques" section that pins
  down 3-role auth (admin super-user / redteam / soc), JWT bearer,
  single-container Flask+React topology, minimal Engagement model,
  local MITRE STIX bundle, and the Makefile target list.
- .claude/agents/ defines the 6 sub-agents per SPEC.md § Team:
  backend-builder, frontend-builder, spec-reviewer (project override
  covering plan-vs-spec + code-vs-spec), code-reviewer, test-verifier,
  devil-advocate.
- tasks/todo.md holds the full Sprint 1 plan (Auth + CRUD Engagement)
  validated by spec-reviewer on 2026-05-26 after one round of fixes.
- CHANGELOG.md and tasks/lessons.md scaffolded.
- .gitignore covers Python, Node, Playwright, secrets, build artifacts
  and Claude Code worktrees.

No application code is shipped in this commit — Sprint 1 will be a
separate branch and PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-05-26 08:01:13 +02:00
parent 1194414b57
commit bd9c06e31b
11 changed files with 962 additions and 1 deletions

7
tasks/lessons.md Normal file
View File

@@ -0,0 +1,7 @@
# Lessons Learned
Recurring mistakes and the rule we adopted so the same issue doesn't bite twice. Append-only. Each entry has: date, context, lesson.
---
_(empty — to be filled by the team-lead at the end of each sprint, with input from builders and reviewers)_

315
tasks/todo.md Normal file
View File

@@ -0,0 +1,315 @@
# Sprint 1 — Auth + CRUD Engagement
**Branche** : `sprint/1-auth-engagements`
**Statut** : 🟢 PLAN APPROUVÉ (spec-reviewer 2026-05-26) — prêt pour dispatch backend-builder
**Base** : `main`
**Objectif** : poser l'infrastructure (Flask + SQLite + React + Docker + Makefile + tests) ET livrer une première feature de bout en bout testable sur l'UI — login + admin gère les comptes + tout utilisateur authentifié peut créer/lister/éditer/supprimer des engagements.
---
## 1. User stories
### US-1 — En tant qu'admin, je bootstrap le premier compte admin
**Pourquoi** : sinon impossible d'utiliser l'application au premier démarrage.
**Critères d'acceptation**
- [ ] AC-1.1 : la commande `make create-admin USER=alice PASS=p4ssw0rd` crée un user `alice` avec le rôle `admin` et le password hashé (argon2).
- [ ] AC-1.2 : la commande échoue proprement (exit ≠ 0, message clair) si le username existe déjà.
- [ ] AC-1.3 : la commande échoue si le password fait moins de 8 caractères.
- [ ] AC-1.4 : la commande s'exécute via `docker exec mimic flask create-admin …` (le Makefile encapsule cet appel).
### US-2 — En tant qu'utilisateur, je me connecte et me déconnecte
**Pourquoi** : porte d'entrée de l'application.
**Critères d'acceptation**
- [ ] AC-2.1 : `POST /api/auth/login {username, password}` retourne `{access_token, user: {id, username, role}}` (200) si credentials valides.
- [ ] AC-2.2 : 401 si credentials invalides, avec un message générique ("Invalid credentials") — pas de fuite username vs password.
- [ ] AC-2.3 : `POST /api/auth/logout` invalide le token côté client (UI supprime le token). Côté serveur : optionnel V1, on accepte un logout client-side.
- [ ] AC-2.4 : page `/login` affiche le formulaire ; soumission OK → redirection `/engagements`. Soumission KO → message d'erreur visible.
- [ ] AC-2.5 : navigation vers `/engagements` sans token → redirection `/login`.
- [ ] AC-2.6 : si une requête API retourne 401 (token expiré ou invalide), l'intercepteur axios purge le token et redirige vers `/login` avec un toast "Session expirée".
### US-3 — En tant qu'admin, je gère les comptes utilisateurs
**Pourquoi** : créer redteam/soc accounts depuis l'UI.
**Critères d'acceptation**
- [ ] AC-3.1 : `GET /api/users` (admin only) → liste `[{id, username, role, created_at}]`.
- [ ] AC-3.2 : `POST /api/users {username, password, role}` (admin only) → 201 + objet user (sans password_hash). 400 si username existe ou password < 8 chars.
- [ ] AC-3.3 : `PATCH /api/users/<id> {role?, password?}` (admin only) → 200, modifie role et/ou password.
- [ ] AC-3.4 : `DELETE /api/users/<id>` (admin only) → 204. Refuse de supprimer le dernier admin (409).
- [ ] AC-3.5 : tout autre rôle (redteam/soc) appelant ces endpoints reçoit 403.
- [ ] AC-3.6 : page `/admin/users` (admin only) liste les users avec actions "Créer", "Modifier rôle", "Reset password", "Supprimer".
- [ ] AC-3.7 : un user redteam/soc qui visite `/admin/users` est redirigé vers `/engagements` avec un toast "Accès refusé".
### US-4 — En tant qu'utilisateur authentifié, je gère les engagements
**Pourquoi** : la feature métier centrale du Sprint 1.
**Critères d'acceptation**
- [ ] AC-4.1 : `GET /api/engagements` (auth) → `[{id, name, description, start_date, end_date, status, created_at, created_by}]`.
- [ ] AC-4.2 : `POST /api/engagements {name, description?, start_date, end_date?, status?}` (auth) → 201. Valide : `name` non vide, `start_date` parseable, `end_date >= start_date` si fournie, `status ∈ {planned, active, closed}` (défaut `planned`).
- [ ] AC-4.3 : `GET /api/engagements/<id>` (auth) → 200 + objet, 404 si inconnu.
- [ ] AC-4.4 : `PATCH /api/engagements/<id>` (auth, redteam ou admin) → 200, modifie les champs fournis.
- [ ] AC-4.5 : `DELETE /api/engagements/<id>` (admin ou redteam) → 204.
- [ ] AC-4.6 : un user `soc` peut lire (GET) mais pas créer/modifier/supprimer (403).
- [ ] AC-4.7 : page `/engagements` liste les engagements avec colonnes (name, status badge, dates, created_by). Boutons "Nouveau", "Voir", "Éditer", "Supprimer" selon rôle.
- [ ] AC-4.8 : page `/engagements/new` et `/engagements/<id>/edit` (formulaire avec validation côté client + erreurs API affichées).
- [ ] AC-4.9 : page `/engagements/<id>` (détail), placeholder "Simulations à venir au Sprint 2".
### US-5 — En tant qu'utilisateur, l'UI respecte DESIGN.md
**Pourquoi** : non négociable selon SPEC.md.
**Critères d'acceptation**
- [ ] AC-5.1 : la palette, typographie, espacements, composants (boutons, inputs, badges) suivent strictement `DESIGN.md`.
- [ ] AC-5.2 : layout responsive desktop-first (≥ 1024px), pas de breakage visible jusqu'à 1280×720 minimum.
- [ ] AC-5.3 : états loading / error / empty implémentés pour la liste d'engagements et la liste d'users.
### US-6 — Le livrable se déploie via Docker + Makefile
**Pourquoi** : exigence SPEC.md.
**Critères d'acceptation**
- [ ] AC-6.1 : `make build` produit l'image docker `mimic:latest` (Dockerfile multistage : Node build → Python runtime).
- [ ] AC-6.2 : `make start` lance le container, l'app est accessible sur `http://localhost:5000` (front + API).
- [ ] AC-6.3 : `make stop`, `make restart`, `make logs` fonctionnent.
- [ ] AC-6.4 : SQLite persisté via volume nommé `mimic-data` (la DB survit à `make restart`).
- [ ] AC-6.5 : `make test-backend`, `make test-frontend`, `make test-e2e` exécutent les suites respectives.
---
## 2. Brief technique — Backend Builder
**Scope strict** : `backend/`, `Dockerfile`, `Makefile` (en collab avec ce sprint uniquement).
### Livrables
1. **Structure** :
```
backend/
__init__.py # ⚠️ requis pour que `backend.app:create_app` soit importable depuis /app dans le Dockerfile
app/
__init__.py # create_app() factory
config.py # DevConfig / ProdConfig (SECRET_KEY, JWT_SECRET, SQLALCHEMY_DATABASE_URI)
extensions.py # db = SQLAlchemy(), migrate = Migrate()
cli.py # @app.cli.command("create-admin")
models/
__init__.py
user.py # User(id, username, password_hash, role, created_at)
engagement.py # Engagement(id, name, description, start_date, end_date, status, created_at, created_by)
auth/
__init__.py
jwt.py # encode_token / decode_token
hashing.py # argon2 hash & verify
decorators.py # @login_required, @role_required("admin")
api/
__init__.py
auth.py # /login, /logout, /me
users.py # CRUD users (admin)
engagements.py # CRUD engagements
serializers.py # to_dict() helpers — engagement renvoie created_by={id, username}, jamais l'objet User brut
errors.py # uniform JSON error handler
migrations/ # alembic init + 0001 initial schema
tests/
conftest.py # app fixture, db fixture, auth helpers
test_auth.py # login OK + 401 invalid + 401 token expiré
test_users.py # CRUD admin only, 403 redteam/soc, last-admin protection (AC-3.4)
test_engagements.py # CRUD redteam/admin, 403 soc en write, serializer created_by
test_cli_create_admin.py # success + duplicate username (AC-1.2) + password < 8 chars (AC-1.3)
pyproject.toml
requirements.txt # flask, flask-sqlalchemy, flask-migrate, pyjwt, argon2-cffi, ruff, mypy, pytest
```
2. **Endpoints** : voir critères AC-2 / AC-3 / AC-4.
3. **Modèles** : voir SPEC.md § Modèle de données.
4. **Config** :
- `SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.environ.get('MIMIC_DB_PATH', '/data/mimic.sqlite')}"` — l'env var `MIMIC_DB_PATH` du Dockerfile surcharge le chemin si présent (utile pour tests ou changement de mount).
- `JWT_SECRET` lu depuis env var `MIMIC_JWT_SECRET`, requis (raise si absent en Prod).
- `JWT_EXP_MINUTES = 60`.
5. **CLI** : `flask create-admin <user> <pass>` (avec validations AC-1.2/1.3).
6. **Serializer engagement** : la réponse JSON expose `created_by` sous forme `{"id": <int>, "username": <str>}` — pas l'objet User complet, pas seulement l'id.
7. **Tests** : couverture success / failure / edge sur chaque endpoint et CLI. Les AC-1.2 et AC-1.3 doivent avoir leur propre test dans `test_cli_create_admin.py`.
8. **Lint / typing** : ruff clean, mypy clean sur `app/`.
### Règles
- Pas de touche au frontend.
- Pas d'invention de dépendances hors de la liste ci-dessus sans escalade au team-lead.
- Renvoyer le summary attendu (cf. `.claude/agents/backend-builder.md`).
---
## 3. Brief technique — Frontend Builder
**Scope strict** : `frontend/` UNIQUEMENT. Le dossier `e2e/` est **interdit** au frontend-builder — il est sous la responsabilité exclusive du test-verifier (scaffolding Playwright + tests).
### Livrables
1. **Structure** :
```
frontend/
package.json # react, react-dom, react-router-dom, @tanstack/react-query, axios, tailwindcss, vite, typescript, vitest, @testing-library/react
vite.config.ts # proxy /api -> http://localhost:5000 en dev
tailwind.config.ts # tokens issus de DESIGN.md, font-family principale = "Inter"
tsconfig.json
index.html
src/
main.tsx
App.tsx # router, QueryClientProvider
api/
client.ts # axios + interceptor (Bearer + 401 purge → /login)
auth.ts # login, me
users.ts # CRUD users
engagements.ts # CRUD engagements
types.ts
hooks/
useAuth.ts # token in memory + localStorage, role helpers (isAdmin, isRedteam, isSoc)
useEngagements.ts # TanStack Query hooks
useUsers.ts
useToast.ts # provider + hook pour notifications éphémères
pages/
LoginPage.tsx
EngagementsListPage.tsx
EngagementFormPage.tsx # new + edit
EngagementDetailPage.tsx
UsersAdminPage.tsx
components/
Layout.tsx # nav, topbar, role-aware menu
ProtectedRoute.tsx # redirect to /login if no token, role gate
StatusBadge.tsx
FormField.tsx
EmptyState.tsx
ErrorState.tsx
LoadingState.tsx
Toast.tsx # composant + ToastProvider (utilisé par AC-2.6 + AC-3.7)
styles/
index.css # tailwind base + DESIGN.md tokens
fonts.css # @font-face Inter (bundlée localement dans public/fonts/, AUCUN CDN)
public/
fonts/ # fichiers Inter .woff2 (déposés via npm install + copie post-install, ou commit direct)
tests/
components/*.test.tsx # Vitest
```
2. **Police** : `Inter` (substitut Forma DJR Micro choisi par défaut parmi les 3 options DESIGN.md §86-89). Bundlée localement en `.woff2`, jamais via Google Fonts/CDN. Configurée dans `tailwind.config.ts` comme `font-sans` et chargée via `@font-face` dans `styles/fonts.css`.
3. **Routing** :
- `/login`
- `/engagements` (auth, all roles)
- `/engagements/new` (auth, redteam|admin)
- `/engagements/:id` (auth, all roles)
- `/engagements/:id/edit` (auth, redteam|admin)
- `/admin/users` (auth, admin only)
- `/` → redirige vers `/engagements` ou `/login`
4. **Auth** : token JWT en mémoire + `localStorage` ; intercepteur axios ajoute `Authorization: Bearer <token>` ; 401 → purge token + redirect `/login` + toast "Session expirée" (AC-2.6).
5. **Tests Vitest** : 1 test par composant non trivial (états loading/error/empty, comportement des rôles dans `ProtectedRoute`, Toast déclenché par 401).
### Règles
- Lit le summary du backend-builder EN PREMIER.
- Pas d'invention d'endpoints. Mismatch → escalade au team-lead.
- Respect strict de `DESIGN.md` pour palette/typo/composants.
- Pas de CDN remote au runtime — bundle local (police Inter incluse).
- **Interdiction absolue de toucher `e2e/`** (responsabilité test-verifier).
---
## 4. Brief — Docker / Makefile (réalisé par le backend-builder)
### `docker/Dockerfile` (multistage)
```dockerfile
# Stage 1: build front
FROM node:20-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Stage 2: python runtime
FROM python:3.12-slim
WORKDIR /app
COPY backend/requirements.txt ./backend/
RUN pip install --no-cache-dir -r backend/requirements.txt
COPY backend/ ./backend/
COPY --from=frontend-build /app/frontend/dist ./backend/app/static
ENV FLASK_APP=backend.app:create_app
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app
# Variables surchargeables au `docker run` :
ENV MIMIC_PORT=5000
ENV MIMIC_DB_PATH=/data/mimic.sqlite
VOLUME ["/data"]
EXPOSE 5000
# Entrypoint : applique les migrations Alembic puis lance Flask
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
```
**`docker/entrypoint.sh`** :
```bash
#!/bin/sh
set -e
flask db upgrade
exec flask run --host=0.0.0.0 --port="${MIMIC_PORT:-5000}"
```
Flask sert `backend/app/static` en racine `/` ET expose les blueprints sous `/api/*`. La DB SQLite vit dans `/data/mimic.sqlite` (volume nommé `mimic-data`) — survit à `make restart` (AC-6.4).
### `Makefile`
Targets requis et leur sémantique :
| Target | Action |
|---|---|
| `build` | `docker build -f docker/Dockerfile -t mimic:latest .` |
| `start` | `docker run -d --name mimic -p $(PORT):5000 -v mimic-data:/data --env-file .env mimic:latest` (`PORT ?= 5000`) |
| `stop` | `docker stop mimic && docker rm mimic` |
| `restart` | `$(MAKE) stop && $(MAKE) start` |
| `update` | `git pull && $(MAKE) build && $(MAKE) restart` |
| `logs` | `docker logs -f mimic` |
| `create-admin` | `docker exec mimic flask create-admin $(USER) $(PASS)` (requiert `USER=` et `PASS=`) |
| `update-mitre` | placeholder no-op Sprint 1 (`@echo "MITRE update: Sprint 2+"`) |
| `test-backend` | `docker exec mimic pytest -q backend/tests/` (ou run local en venv) |
| `test-frontend` | `cd frontend && npm run test -- --run` |
| `test-e2e` | `cd e2e && npx playwright test` (container doit être up) |
| `clean` | `docker rm -f mimic 2>/dev/null; docker volume rm mimic-data 2>/dev/null; rm -rf backend/__pycache__ frontend/node_modules frontend/dist` |
---
## 5. Definition of Done — Sprint 1
- [ ] Tous les critères d'acceptation des US 1→6 passent.
- [ ] `pytest`, `ruff`, `mypy` clean côté backend.
- [ ] `npm run typecheck`, `lint`, `test` clean côté frontend.
- [ ] Playwright suite verte côté `e2e/`.
- [ ] Image docker se build (`make build`), démarre (`make start`), répond sur `:5000`.
- [ ] Code review (Opus) sans BLOCKER ouvert.
- [ ] `SPEC.md`, `README.md`, `CHANGELOG.md` à jour.
- [ ] PR ouverte sur la branche sprint, validée par l'utilisateur après récap synthétique.
---
## 6. Risques & questions à clarifier avec l'utilisateur AVANT de coder
1. **DESIGN.md** : faut-il que je relise DESIGN.md (27 Ko, déjà présent dans le repo) avant que le frontend-builder l'utilise ? OUI
2. **Persistance SQLite** : DB stockée dans `/data/mimic.sqlite` montée comme volume nommé `mimic-data`. OK ? OK
3. **Port** : `5000` (Flask défaut). Conflit possible avec macOS AirPlay si jamais — on s'en fiche en Linux. Surcharge du port possible via dockerfile et container.
4. **CSRF** : on est en API JWT pure, pas de cookie session, donc pas de CSRF protection nécessaire côté serveur. OK ? OK
5. **Refresh token** : exclu V1. JWT court (60 min) ; user devra se reconnecter. OK ? OK
6. **Logout** : V1 = client-side uniquement (purge du token). Pas de blacklist. OK ? OK
7. **CI** : pas mentionné dans la spec. Skip pour Sprint 1 ? (à confirmer) On verra plus tard.
8. **README.md** : actuellement absent. Le team-lead le crée en fin de Sprint 1 avec instructions `make build / start / create-admin`. OK.
---
## 7. Plan d'exécution (séquence)
**Branche unique pour ce sprint** : `sprint/1-auth-engagements` (pas de sous-branches builders, le sprint est séquentiel).
1. ✅ **Spec-reviewer** (`.claude/agents/spec-reviewer.md`, override projet du built-in) valide ce plan vs SPEC.md.
2. 🔵 **Backend-builder** implémente `backend/` + `docker/Dockerfile` + `docker/entrypoint.sh` + `Makefile`, livre son summary (incluant le contrat API).
3. 🔵 **Frontend-builder** lit le summary backend puis implémente le front (`frontend/` UNIQUEMENT, jamais `e2e/`).
4. 🔵 **Code-reviewer** relit le diff du sprint (LSP-first, focus bugs/qualité/scope).
5. 🔵 **Test-verifier** **scaffolds** `e2e/` (Playwright config, package.json, fixtures auth) **puis écrit** les acceptance tests Playwright et exécute la suite contre le container démarré via `make start`.
6. 🟢 **Team-lead** (moi) prépare la PR + récap synthétique → user valide.
**Responsabilité `e2e/`** : exclusivement le test-verifier. Backend-builder et frontend-builder n'y touchent jamais.