mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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:
84
packages/shared/scripts/generate-openapi.ts
Normal file
84
packages/shared/scripts/generate-openapi.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Règle d'or (ADR-001) : Zod → OpenAPI → clients typés.
|
||||
* Génère docs/openapi.json (COMMITTÉE) depuis src/contract.ts.
|
||||
* Usage : pnpm --filter @siop/shared contract (raccourci racine : pnpm contract)
|
||||
* La CI (ci-contract, R0.12) échoue si le fichier committé diffère de la génération.
|
||||
*/
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { API_CONTRACT } from '../src/contract';
|
||||
|
||||
const OUT = resolve(__dirname, '../../../docs/openapi.json');
|
||||
|
||||
const schemas: Record<string, unknown> = {};
|
||||
|
||||
function register(name: string, schema: z.ZodType): { $ref: string } {
|
||||
if (!(name in schemas)) {
|
||||
schemas[name] = z.toJSONSchema(schema, { target: 'draft-2020-12' });
|
||||
}
|
||||
return { $ref: `#/components/schemas/${name}` };
|
||||
}
|
||||
|
||||
const paths: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
for (const op of API_CONTRACT) {
|
||||
const responses: Record<string, unknown> = {};
|
||||
for (const [status, res] of Object.entries(op.responses)) {
|
||||
responses[status] = {
|
||||
description: res.description,
|
||||
...(res.schema && res.name
|
||||
? { content: { 'application/json': { schema: register(res.name, res.schema) } } }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
paths[op.path] = {
|
||||
...(paths[op.path] ?? {}),
|
||||
[op.method]: {
|
||||
operationId: op.operationId,
|
||||
summary: op.summary,
|
||||
tags: op.tags,
|
||||
...(op.isPublic ? {} : { security: [{ bearerAuth: [] }] }),
|
||||
...(op.demoOnly
|
||||
? {
|
||||
'x-demo-only': true,
|
||||
description:
|
||||
'ADR-002 : cette route est absente (404) quand DEMO_MODE n’est pas actif.',
|
||||
}
|
||||
: {}),
|
||||
...(op.request
|
||||
? {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': { schema: register(op.request.name, op.request.schema) },
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
responses,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const doc = {
|
||||
openapi: '3.1.0',
|
||||
info: {
|
||||
title: 'SIOP V2 API',
|
||||
version: '0.1.0',
|
||||
description:
|
||||
'GMAO ascenseurs — contrat R0 (auth, démo-login ADR-002, profil, santé). ' +
|
||||
'Généré depuis packages/shared/src/contract.ts — NE PAS ÉDITER À LA MAIN.',
|
||||
},
|
||||
paths,
|
||||
components: {
|
||||
schemas,
|
||||
securitySchemes: {
|
||||
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mkdirSync(dirname(OUT), { recursive: true });
|
||||
writeFileSync(OUT, JSON.stringify(doc, null, 2) + '\n');
|
||||
console.log(`OpenAPI générée : ${OUT} (${API_CONTRACT.length} opérations)`);
|
||||
Reference in New Issue
Block a user