feat(r0.10): apps/api — auth fermée par défaut, matrice en base, démo-login ADR-002, seed

- packages/shared : rôles/catégories, schémas Zod, contrat d'API ;
  pnpm contract → docs/openapi.json committée (règle d'or ADR-001)
- apps/api : NestJS 11 + Prisma 6, migration r0_identity (Role/Permission/User) ;
  guard JWT global + @Public() ; PermissionsGuard (@RequirePermission,
  matrice relue en base, cache 60 s) ; FileStorage (seul import MinIO) ; /health
- démo-login ADR-002 : module conditionnel DEMO_MODE (404 sinon, testé e2e),
  double verrou production, refus des comptes isDemo=false
- seed idempotent : 7 rôles, matrice complète (70 lignes), 7 comptes démo
- 19 tests Jest (unit + e2e) ; smoke test sur build de prod

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-15 21:55:04 +01:00
parent ddc9dc52b5
commit 8adb561b63
62 changed files with 8509 additions and 2 deletions

View File

@@ -0,0 +1,104 @@
import type { z } from 'zod';
import {
AuthResponseSchema,
DemoAccountsResponseSchema,
DemoLoginRequestSchema,
LoginRequestSchema,
} from './schemas/auth';
import { MeResponseSchema } from './schemas/users';
import { HealthResponseSchema } from './schemas/health';
/**
* Contrat d'API R0 — source unique de vérité (règle d'or ADR-001).
* `scripts/generate-openapi.ts` en dérive `docs/openapi.json` (committée) ;
* les clients web/mobile sont générés depuis cette spec, dans le même commit.
*/
export interface ApiOperation {
operationId: string;
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
path: string;
summary: string;
tags: string[];
/** Route annotée @Public() côté API (pas de JWT requis). */
isPublic?: boolean;
/** ADR-002 : la route N'EXISTE PAS (404) si DEMO_MODE n'est pas actif. */
demoOnly?: boolean;
request?: { name: string; schema: z.ZodType };
responses: Record<
number,
{ description: string; name?: string; schema?: z.ZodType }
>;
}
export const API_CONTRACT: ApiOperation[] = [
{
operationId: 'login',
method: 'post',
path: '/auth/login',
summary: 'Connexion par e-mail et mot de passe',
tags: ['auth'],
isPublic: true,
request: { name: 'LoginRequest', schema: LoginRequestSchema },
responses: {
200: { description: 'Jeton émis', name: 'AuthResponse', schema: AuthResponseSchema },
401: { description: 'Identifiants invalides ou compte inactif' },
},
},
{
operationId: 'listDemoAccounts',
method: 'get',
path: '/auth/demo-accounts',
summary: 'Comptes de démonstration (ADR-002 — jamais de secret)',
tags: ['auth', 'demo'],
isPublic: true,
demoOnly: true,
responses: {
200: {
description: 'Comptes isDemo actifs',
name: 'DemoAccountsResponse',
schema: DemoAccountsResponseSchema,
},
},
},
{
operationId: 'demoLogin',
method: 'post',
path: '/auth/demo-login',
summary: 'Connexion 1 clic sur un compte de démonstration (ADR-002)',
tags: ['auth', 'demo'],
isPublic: true,
demoOnly: true,
request: { name: 'DemoLoginRequest', schema: DemoLoginRequestSchema },
responses: {
200: { description: 'Jeton émis', name: 'AuthResponse', schema: AuthResponseSchema },
403: { description: 'Le compte nest pas un compte de démonstration' },
404: { description: 'Compte inconnu' },
},
},
{
operationId: 'getMe',
method: 'get',
path: '/users/me',
summary: 'Profil courant + matrice de permissions du rôle',
tags: ['users'],
responses: {
200: { description: 'Profil', name: 'MeResponse', schema: MeResponseSchema },
401: { description: 'Non authentifié' },
},
},
{
operationId: 'getHealth',
method: 'get',
path: '/health',
summary: 'État des dépendances (base, Redis, stockage)',
tags: ['health'],
isPublic: true,
responses: {
200: {
description: 'État agrégé',
name: 'HealthResponse',
schema: HealthResponseSchema,
},
},
},
];