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

479
docs/openapi.json Normal file
View File

@@ -0,0 +1,479 @@
{
"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": {
"/auth/login": {
"post": {
"operationId": "login",
"summary": "Connexion par e-mail et mot de passe",
"tags": [
"auth"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginRequest"
}
}
}
},
"responses": {
"200": {
"description": "Jeton émis",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResponse"
}
}
}
},
"401": {
"description": "Identifiants invalides ou compte inactif"
}
}
}
},
"/auth/demo-accounts": {
"get": {
"operationId": "listDemoAccounts",
"summary": "Comptes de démonstration (ADR-002 — jamais de secret)",
"tags": [
"auth",
"demo"
],
"x-demo-only": true,
"description": "ADR-002 : cette route est absente (404) quand DEMO_MODE nest pas actif.",
"responses": {
"200": {
"description": "Comptes isDemo actifs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DemoAccountsResponse"
}
}
}
}
}
}
},
"/auth/demo-login": {
"post": {
"operationId": "demoLogin",
"summary": "Connexion 1 clic sur un compte de démonstration (ADR-002)",
"tags": [
"auth",
"demo"
],
"x-demo-only": true,
"description": "ADR-002 : cette route est absente (404) quand DEMO_MODE nest pas actif.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DemoLoginRequest"
}
}
}
},
"responses": {
"200": {
"description": "Jeton émis",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResponse"
}
}
}
},
"403": {
"description": "Le compte nest pas un compte de démonstration"
},
"404": {
"description": "Compte inconnu"
}
}
}
},
"/users/me": {
"get": {
"operationId": "getMe",
"summary": "Profil courant + matrice de permissions du rôle",
"tags": [
"users"
],
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Profil",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MeResponse"
}
}
}
},
"401": {
"description": "Non authentifié"
}
}
}
},
"/health": {
"get": {
"operationId": "getHealth",
"summary": "État des dépendances (base, Redis, stockage)",
"tags": [
"health"
],
"responses": {
"200": {
"description": "État agrégé",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HealthResponse"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"AuthResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"accessToken": {
"type": "string"
},
"user": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"email": {
"type": "string",
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"displayName": {
"type": "string"
},
"role": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"name": {
"type": "string",
"enum": [
"Administrateur",
"Dispatcher",
"Technicien",
"Technicien limité",
"Gestionnaire",
"Demandeur",
"Vue seule"
]
}
},
"required": [
"id",
"name"
],
"additionalProperties": false
},
"isDemo": {
"type": "boolean"
}
},
"required": [
"id",
"email",
"displayName",
"role",
"isDemo"
],
"additionalProperties": false
}
},
"required": [
"accessToken",
"user"
],
"additionalProperties": false
},
"LoginRequest": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"password": {
"type": "string",
"minLength": 1
}
},
"required": [
"email",
"password"
],
"additionalProperties": false
},
"DemoAccountsResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"accounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"displayName": {
"type": "string"
},
"roleName": {
"type": "string",
"enum": [
"Administrateur",
"Dispatcher",
"Technicien",
"Technicien limité",
"Gestionnaire",
"Demandeur",
"Vue seule"
]
},
"initials": {
"type": "string",
"minLength": 1,
"maxLength": 3
}
},
"required": [
"id",
"displayName",
"roleName",
"initials"
],
"additionalProperties": false
}
}
},
"required": [
"accounts"
],
"additionalProperties": false
},
"DemoLoginRequest": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"userId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
}
},
"required": [
"userId"
],
"additionalProperties": false
},
"MeResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"email": {
"type": "string",
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"displayName": {
"type": "string"
},
"role": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"name": {
"type": "string",
"enum": [
"Administrateur",
"Dispatcher",
"Technicien",
"Technicien limité",
"Gestionnaire",
"Demandeur",
"Vue seule"
]
}
},
"required": [
"id",
"name"
],
"additionalProperties": false
},
"isDemo": {
"type": "boolean"
},
"permissions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"objectCategory": {
"type": "string",
"enum": [
"WORK_ORDERS",
"REQUESTS",
"ASSETS",
"LOCATIONS",
"METERS",
"PARTS",
"PURCHASE_ORDERS",
"PEOPLE_TEAMS",
"ANALYTICS",
"SETTINGS"
]
},
"canView": {
"type": "boolean"
},
"canViewOther": {
"type": "boolean"
},
"canCreate": {
"type": "boolean"
},
"canEdit": {
"type": "boolean"
},
"canDelete": {
"type": "boolean"
}
},
"required": [
"objectCategory",
"canView",
"canViewOther",
"canCreate",
"canEdit",
"canDelete"
],
"additionalProperties": false
}
}
},
"required": [
"id",
"email",
"displayName",
"role",
"isDemo",
"permissions"
],
"additionalProperties": false
},
"HealthResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"ok",
"degraded"
]
},
"services": {
"type": "object",
"properties": {
"database": {
"type": "string",
"enum": [
"up",
"down"
]
},
"redis": {
"type": "string",
"enum": [
"up",
"down"
]
},
"storage": {
"type": "string",
"enum": [
"up",
"down"
]
}
},
"required": [
"database",
"redis",
"storage"
],
"additionalProperties": false
}
},
"required": [
"status",
"services"
],
"additionalProperties": false
}
},
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
}
}