feat: Sprint 1 — backend fondations TetaRdPG
Auth SuperOAuth (JWT validation + httpOnly cookie), entités users/characters/level_thresholds, lazy calculation endurance, seed 100 niveaux, config prod-ready (trust proxy, helmet, CORS, rate limit). Validé : health 200, auth flow, character CRUD, endurance lazy, 401 sans cookie.
This commit is contained in:
318
SPRINT1.md
Normal file
318
SPRINT1.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# TetaRdPG — Brief Sprint 1
|
||||
|
||||
> Statut : ⬜ À démarrer
|
||||
> Objectif : Backend jouable en local — Auth + Personnage + Endurance
|
||||
> Stack : TypeScript · NestJS · PostgreSQL · Redis · Docker Compose
|
||||
|
||||
---
|
||||
|
||||
## Contexte
|
||||
|
||||
Le GDD est complet sur les systèmes core (voir `GDD.md`).
|
||||
Ce sprint pose les fondations backend : auth déléguée à SuperOAuth, personnage joueur, gestion d'endurance.
|
||||
**Pas de Twitch. Pas de combat.** Ce sera Sprint 2.
|
||||
|
||||
Architecture locale d'abord — mais production-ready dès le départ (VPS probable).
|
||||
|
||||
---
|
||||
|
||||
## Scope — contrainte d'autonomie agents
|
||||
|
||||
```
|
||||
Répertoire de travail : /home/tetardtek/Dev/Gitea/TetaRdPG/
|
||||
Interdit d'écriture : brain/, originsdigital/, super-oauth/, tout autre projet
|
||||
SuperOAuth : service externe consommé via env vars — jamais modifié
|
||||
Brain : lecture seule si besoin de patterns — zéro écriture
|
||||
```
|
||||
|
||||
> Toute écriture hors de ce répertoire = violation de scope → STOP immédiat, signaler à l'humain.
|
||||
|
||||
---
|
||||
|
||||
## Périmètre Sprint 1
|
||||
|
||||
### ✅ In scope
|
||||
|
||||
- Projet NestJS + TypeScript initialisé
|
||||
- Docker Compose local : PostgreSQL + Redis + backend
|
||||
- Auth via SuperOAuth (consommer le service existant — ne pas réimplémenter)
|
||||
- Entités DB : `users`, `characters`, `level_thresholds` (seed)
|
||||
- API : création personnage, lecture personnage, état endurance
|
||||
- Endurance : lazy calculation (pas de timer actif)
|
||||
- Config production-ready dès le départ (trust proxy, CORS env, rate limiting, httpOnly cookies)
|
||||
- Health endpoint `/api/health`
|
||||
|
||||
### ❌ Out of scope
|
||||
|
||||
- Twitch OAuth / EventSub
|
||||
- Combat PvE
|
||||
- Forge / Artisanat
|
||||
- Frontend
|
||||
- Déploiement VPS
|
||||
|
||||
---
|
||||
|
||||
## Auth — Intégration SuperOAuth
|
||||
|
||||
SuperOAuth est le service d'auth mutualisé de Tetardtek.
|
||||
Le client (TetaRdPG) ne gère jamais les credentials OAuth — il délègue et valide le JWT.
|
||||
|
||||
**Variables d'environnement requises :**
|
||||
```env
|
||||
SUPER_OAUTH_URL=http://localhost:3000 # local — https://superoauth.tetardtek.com en prod
|
||||
SUPER_OAUTH_JWT_SECRET=<secret partagé> # même secret que SuperOAuth
|
||||
```
|
||||
|
||||
**Flow :**
|
||||
```
|
||||
Joueur clique "Se connecter"
|
||||
→ Frontend redirige vers SuperOAuth (/auth/twitch ou /auth/discord)
|
||||
→ SuperOAuth gère l'OAuth provider
|
||||
→ SuperOAuth émet un JWT signé avec SUPER_OAUTH_JWT_SECRET
|
||||
→ TetaRdPG backend reçoit le JWT → valide la signature → stocke en httpOnly cookie
|
||||
→ Toutes les routes protégées : AuthGuard vérifie le cookie
|
||||
```
|
||||
|
||||
**Endpoints auth à implémenter :**
|
||||
```
|
||||
POST /api/auth/session → reçoit JWT de SuperOAuth → valide → set cookie httpOnly
|
||||
GET /api/auth/me → lit cookie → retourne profil user
|
||||
POST /api/auth/logout → clear cookie
|
||||
```
|
||||
|
||||
**AuthGuard NestJS :**
|
||||
- Lit le cookie `session`
|
||||
- Vérifie la signature JWT avec `SUPER_OAUTH_JWT_SECRET`
|
||||
- Injecte le user dans le request context
|
||||
- Retourne 401 si invalide ou absent
|
||||
|
||||
---
|
||||
|
||||
## Schéma DB
|
||||
|
||||
### `users`
|
||||
```sql
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
||||
oauth_id VARCHAR(255) UNIQUE NOT NULL -- ID chez le provider (Twitch ID, Discord ID)
|
||||
provider VARCHAR(50) NOT NULL -- 'twitch' | 'discord' | 'google' | 'github'
|
||||
username VARCHAR(255) NOT NULL
|
||||
avatar_url VARCHAR(500)
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
```
|
||||
|
||||
### `characters`
|
||||
```sql
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
||||
user_id UUID NOT NULL REFERENCES users(id)
|
||||
name VARCHAR(100) NOT NULL
|
||||
level INTEGER DEFAULT 1
|
||||
xp INTEGER DEFAULT 0
|
||||
gold INTEGER DEFAULT 0
|
||||
|
||||
-- Stats (cap : 101)
|
||||
force INTEGER DEFAULT 1
|
||||
agilite INTEGER DEFAULT 1
|
||||
intelligence INTEGER DEFAULT 1
|
||||
chance INTEGER DEFAULT 1
|
||||
vitalite INTEGER DEFAULT 1
|
||||
hp_current INTEGER DEFAULT 100
|
||||
hp_max INTEGER DEFAULT 100
|
||||
|
||||
-- Endurance (lazy calculation)
|
||||
endurance_saved INTEGER DEFAULT 100
|
||||
last_endurance_ts TIMESTAMP DEFAULT NOW()
|
||||
endurance_max INTEGER DEFAULT 100 -- 150 avec équipement (v2)
|
||||
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
```
|
||||
|
||||
### `level_thresholds` (seed — immuable)
|
||||
```sql
|
||||
level INTEGER PRIMARY KEY -- 1 à 100
|
||||
xp_required INTEGER -- 100 × level^1.5
|
||||
```
|
||||
> Précalculé au seed — jamais recalculé à la requête.
|
||||
|
||||
---
|
||||
|
||||
## Endurance — Pattern lazy calculation
|
||||
|
||||
**Règle absolue : pas de timer par joueur.**
|
||||
|
||||
```typescript
|
||||
// À chaque lecture de l'endurance :
|
||||
const elapsedMinutes = (Date.now() - character.lastEnduranceTs.getTime()) / 60_000;
|
||||
const recharge = Math.floor(elapsedMinutes / 6); // 10 pts/heure = 1 pt / 6 min
|
||||
const enduranceCurrent = Math.min(
|
||||
character.enduranceSaved + recharge,
|
||||
character.enduranceMax
|
||||
);
|
||||
|
||||
// Lors d'une action (ex: combat) :
|
||||
// 1. Calculer l'endurance actuelle (ci-dessus)
|
||||
// 2. Vérifier que enduranceCurrent >= coût de l'action
|
||||
// 3. Stocker : endurance_saved = enduranceCurrent - coût, last_endurance_ts = NOW()
|
||||
```
|
||||
|
||||
> Ce pattern est fondamental. L'endurance n'existe en DB que comme deux colonnes.
|
||||
> Tout le reste est calculé. Zéro cron job. Zéro timer. Scalable à N joueurs.
|
||||
|
||||
---
|
||||
|
||||
## API — Endpoints Sprint 1
|
||||
|
||||
```
|
||||
GET /api/health → { status: 'ok', timestamp }
|
||||
|
||||
POST /api/auth/session → valide JWT SuperOAuth → set cookie
|
||||
GET /api/auth/me → profil user connecté
|
||||
POST /api/auth/logout → clear cookie
|
||||
|
||||
POST /api/characters → crée un personnage (5 pts stats à répartir)
|
||||
GET /api/characters/me → personnage du user connecté + endurance calculée
|
||||
GET /api/characters/me/endurance → endurance actuelle calculée lazy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Config production-ready (pattern OriginsDigital)
|
||||
|
||||
Ces éléments sont **obligatoires dès le Sprint 1** — pas des ajouts post-lancement.
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
app.set('trust proxy', 1); // VPS derrière Apache / reverse proxy
|
||||
|
||||
// CORS — depuis l'env, multi-origin supporté
|
||||
const allowedOrigins = (process.env.FRONTEND_URL ?? 'http://localhost:5173')
|
||||
.split(',')
|
||||
.map(o => o.trim());
|
||||
|
||||
// Cookies httpOnly pour le JWT
|
||||
// Rate limiting sur /api/auth/*
|
||||
// Logging structuré (Pino ou Winston)
|
||||
// Helmet pour les headers de sécurité
|
||||
```
|
||||
|
||||
**Variables d'environnement — `.env.example` :**
|
||||
```env
|
||||
PORT=4000
|
||||
NODE_ENV=development
|
||||
|
||||
DATABASE_URL=postgresql://tetardpg:password@localhost:5432/tetardpg
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
SUPER_OAUTH_URL=http://localhost:3000
|
||||
SUPER_OAUTH_JWT_SECRET=
|
||||
|
||||
COOKIE_SECRET=
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose local
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: tetardpg
|
||||
POSTGRES_USER: tetardpg
|
||||
POSTGRES_PASSWORD: password
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
> Le backend tourne hors Docker en local (hot reload). Seuls PostgreSQL et Redis sont conteneurisés.
|
||||
> En prod VPS : tout dans Docker (backend inclus) — Dockerfile à préparer dès Sprint 1.
|
||||
|
||||
---
|
||||
|
||||
## Structure NestJS cible
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.ts → bootstrap, trust proxy, CORS, helmet
|
||||
├── app.module.ts
|
||||
├── auth/
|
||||
│ ├── auth.module.ts
|
||||
│ ├── auth.controller.ts → /api/auth/*
|
||||
│ ├── auth.service.ts → valide JWT SuperOAuth, gère session
|
||||
│ └── guards/
|
||||
│ └── auth.guard.ts → vérifie cookie sur routes protégées
|
||||
├── character/
|
||||
│ ├── character.module.ts
|
||||
│ ├── character.controller.ts → /api/characters/*
|
||||
│ ├── character.service.ts
|
||||
│ └── entities/
|
||||
│ ├── character.entity.ts
|
||||
│ └── level-threshold.entity.ts
|
||||
├── user/
|
||||
│ ├── user.module.ts
|
||||
│ └── user.entity.ts
|
||||
└── common/
|
||||
├── health.controller.ts → /api/health
|
||||
└── logger/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chaîne d'agents — Sprint 1
|
||||
|
||||
```
|
||||
tech-lead → gate d'entrée (valide l'approche, contention map)
|
||||
↓
|
||||
migration → schema DB + seed level_thresholds (AVANT tout build)
|
||||
↓
|
||||
build ×3 → [auth module] [character module] [docker-compose + config]
|
||||
↓
|
||||
security → validation JWT handling, httpOnly, CORS, rate limiting
|
||||
↓
|
||||
integrator → critères de validation (voir ci-dessous)
|
||||
```
|
||||
|
||||
**Critères de validation integrator :**
|
||||
- [ ] `docker-compose up` → PostgreSQL + Redis up
|
||||
- [ ] `npm run start:dev` → backend démarre sans erreur
|
||||
- [ ] `GET /api/health` → 200 `{ status: 'ok' }`
|
||||
- [ ] Auth flow SuperOAuth → cookie httpOnly posé
|
||||
- [ ] `GET /api/auth/me` → profil user retourné
|
||||
- [ ] `POST /api/characters` → personnage créé en DB
|
||||
- [ ] `GET /api/characters/me` → endurance calculée correctement
|
||||
- [ ] Requête sans cookie → 401
|
||||
- [ ] `.env.example` complet
|
||||
- [ ] `Dockerfile` présent (non testé en prod — validé au Sprint 2)
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Coach — lecture obligatoire avant de démarrer
|
||||
|
||||
Ce sprint est le premier code réel de TetaRdPG. C'est aussi le premier test de la chaîne sur du vrai code.
|
||||
|
||||
**Ce qui va bien se passer :** la structure est claire, les patterns sont connus (SuperOAuth + OriginsDigital), les entités sont définies.
|
||||
|
||||
**Ce qui va être l'enjeu réel :**
|
||||
- Le pattern lazy endurance — ne pas le simplifier en timer "parce que c'est plus simple". C'est le coeur du système idle.
|
||||
- La séparation auth user / character — un user peut ne pas avoir de personnage. Gérer ce cas dès le départ.
|
||||
- `trust proxy: 1` — si oublié, les rate limiters et les IP logs seront faux dès le VPS.
|
||||
|
||||
**Signal de graduation à surveiller :**
|
||||
Si le pattern lazy calculation est implémenté correctement sans intervention du coach → le concept de "calcul à la demande vs état persisté en continu" est acquis. C'est une compétence backend avancée.
|
||||
|
||||
**Objectif pédagogique du sprint :**
|
||||
Produire un backend NestJS de qualité professionnelle — structure de modules, séparation des responsabilités, config 12-factor (env vars), sécurité dès le départ. Pas juste "ça marche en local".
|
||||
Reference in New Issue
Block a user