# 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/ {role?, password?}` (admin only) → 200, modifie role et/ou password. - [ ] AC-3.4 : `DELETE /api/users/` (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/` (auth) → 200 + objet, 404 si inconnu. - [ ] AC-4.4 : `PATCH /api/engagements/` (auth, redteam ou admin) → 200, modifie les champs fournis. - [ ] AC-4.5 : `DELETE /api/engagements/` (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//edit` (formulaire avec validation côté client + erreurs API affichées). - [ ] AC-4.9 : page `/engagements/` (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 ` (avec validations AC-1.2/1.3). 6. **Serializer engagement** : la réponse JSON expose `created_by` sous forme `{"id": , "username": }` — 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 ` ; 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.