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.
319 lines
10 KiB
Markdown
319 lines
10 KiB
Markdown
# 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".
|