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,53 @@
import { Controller, Get, Inject } from '@nestjs/common';
import type { HealthResponse } from '@siop/shared';
import Redis from 'ioredis';
import { Public } from '../auth/public.decorator';
import { loadEnv } from '../config/env';
import { FILE_STORAGE, type FileStorage } from '../files/file-storage';
import { PrismaService } from '../prisma/prisma.service';
@Controller('health')
export class HealthController {
constructor(
private readonly prisma: PrismaService,
@Inject(FILE_STORAGE) private readonly storage: FileStorage,
) {}
@Public()
@Get()
async health(): Promise<HealthResponse> {
const [database, redis, storage] = await Promise.all([
this.check(() => this.prisma.$queryRaw`SELECT 1`),
this.check(async () => {
const client = new Redis(loadEnv().REDIS_URL, {
lazyConnect: true,
maxRetriesPerRequest: 0,
connectTimeout: 2000,
});
try {
await client.connect();
await client.ping();
} finally {
client.disconnect();
}
}),
this.check(() => this.storage.healthCheck()),
]);
const services = { database, redis, storage };
return {
status: Object.values(services).every((s) => s === 'up')
? 'ok'
: 'degraded',
services,
};
}
private async check(fn: () => Promise<unknown>): Promise<'up' | 'down'> {
try {
await fn();
return 'up';
} catch {
return 'down';
}
}
}