Files
TetaRdPG/SPRINT1.md
Tetardtek da3237bf3f 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.
2026-03-15 05:51:02 +01:00

10 KiB
Raw Permalink Blame History

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 :

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

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

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)

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.

// À 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.

// 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 :

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

# 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".