mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r1.1): socle backend du référentiel — modèle, contrat, API, seed, tests
- migration r1_referentiel : Category (EQUIPMENT/COMPONENT_TYPE), Location (site → zone, lat/lng + colonne PostGIS générée geography(Point,4326) + index GIST), Asset (statut d'équipement), AssetComponent (organe sans emplacement PAR CONSTRUCTION), Team, invitation sur User ; migration autosuffisante (CREATE EXTENSION IF NOT EXISTS postgis) - contrat : 21 nouvelles opérations (26 total), générateur OpenAPI étendu aux paramètres de chemin ; spec + client web régénérés dans ce commit - API : modules categories/locations/assets/teams + gestion des personnes (liste, rôles, invitation lien 7 j à usage unique, activation publique qui connecte directement, mise à jour rôle/équipes) — tout sous @RequirePermission ; invariants en service (profondeur 2, kinds, catégorie jamais supprimée) - seed : parc de la maquette validée (5 sites + 8 zones, 8 appareils, organes A1/B2, 9 catégories, 2 équipes) — idempotent - 36 tests verts (couverture 96 % stmts / 85 % branches) : recette site→zone→appareil→organes, matrice vivante, invitation→activation ; smoke test sur build de prod - CI : postgres → postgis/postgis:18-3.6 (la migration R1 l'exige) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,36 @@ import {
|
||||
} from './schemas/auth';
|
||||
import { MeResponseSchema } from './schemas/users';
|
||||
import { HealthResponseSchema } from './schemas/health';
|
||||
import {
|
||||
AssetCreateSchema,
|
||||
AssetComponentCreateSchema,
|
||||
AssetComponentSchema,
|
||||
AssetDetailSchema,
|
||||
AssetsResponseSchema,
|
||||
AssetUpdateSchema,
|
||||
CategoriesResponseSchema,
|
||||
CategoryCreateSchema,
|
||||
CategorySchema,
|
||||
CategoryUpdateSchema,
|
||||
LocationCreateSchema,
|
||||
LocationSchema,
|
||||
LocationsResponseSchema,
|
||||
LocationUpdateSchema,
|
||||
TeamCreateSchema,
|
||||
TeamSchema,
|
||||
TeamsResponseSchema,
|
||||
TeamUpdateSchema,
|
||||
} from './schemas/referentiel';
|
||||
import {
|
||||
ActivateRequestSchema,
|
||||
ActivateResponseSchema,
|
||||
InvitationCreateSchema,
|
||||
InvitationResponseSchema,
|
||||
RolesResponseSchema,
|
||||
UserAdminSchema,
|
||||
UsersResponseSchema,
|
||||
UserUpdateSchema,
|
||||
} from './schemas/users-admin';
|
||||
|
||||
/**
|
||||
* Contrat d'API R0 — source unique de vérité (règle d'or ADR-001).
|
||||
@@ -23,6 +53,8 @@ export interface ApiOperation {
|
||||
isPublic?: boolean;
|
||||
/** ADR-002 : la route N'EXISTE PAS (404) si DEMO_MODE n'est pas actif. */
|
||||
demoOnly?: boolean;
|
||||
/** Paramètres de chemin (`{id}` dans path) — tous UUID en R1. */
|
||||
pathParams?: string[];
|
||||
request?: { name: string; schema: z.ZodType };
|
||||
responses: Record<
|
||||
number,
|
||||
@@ -86,6 +118,255 @@ export const API_CONTRACT: ApiOperation[] = [
|
||||
401: { description: 'Non authentifié' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'activateAccount',
|
||||
method: 'post',
|
||||
path: '/auth/activate',
|
||||
summary: 'Activer un compte invité (lien 7 jours) — connecte directement',
|
||||
tags: ['auth'],
|
||||
isPublic: true,
|
||||
request: { name: 'ActivateRequest', schema: ActivateRequestSchema },
|
||||
responses: {
|
||||
200: { description: 'Compte activé et connecté', name: 'AuthResponse', schema: ActivateResponseSchema },
|
||||
400: { description: 'Lien invalide ou expiré' },
|
||||
},
|
||||
},
|
||||
|
||||
// ————— R1 · Référentiel —————
|
||||
{
|
||||
operationId: 'listCategories',
|
||||
method: 'get',
|
||||
path: '/categories',
|
||||
summary: 'Référentiels administrables (catégories d’équipement, types d’organes)',
|
||||
tags: ['categories'],
|
||||
responses: {
|
||||
200: { description: 'Liste', name: 'CategoriesResponse', schema: CategoriesResponseSchema },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'createCategory',
|
||||
method: 'post',
|
||||
path: '/categories',
|
||||
summary: 'Ajouter une catégorie',
|
||||
tags: ['categories'],
|
||||
request: { name: 'CategoryCreate', schema: CategoryCreateSchema },
|
||||
responses: {
|
||||
201: { description: 'Créée', name: 'Category', schema: CategorySchema },
|
||||
409: { description: 'Nom déjà utilisé pour ce type' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'updateCategory',
|
||||
method: 'patch',
|
||||
path: '/categories/{id}',
|
||||
summary: 'Renommer ou (dés)activer — jamais de suppression si utilisée',
|
||||
tags: ['categories'],
|
||||
pathParams: ['id'],
|
||||
request: { name: 'CategoryUpdate', schema: CategoryUpdateSchema },
|
||||
responses: {
|
||||
200: { description: 'Mise à jour', name: 'Category', schema: CategorySchema },
|
||||
404: { description: 'Inconnue' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'listLocations',
|
||||
method: 'get',
|
||||
path: '/locations',
|
||||
summary: 'Sites et zones (liste plate, le client construit l’arbre)',
|
||||
tags: ['locations'],
|
||||
responses: {
|
||||
200: { description: 'Liste', name: 'LocationsResponse', schema: LocationsResponseSchema },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'createLocation',
|
||||
method: 'post',
|
||||
path: '/locations',
|
||||
summary: 'Créer un site (sans parent) ou une zone (profondeur max 2)',
|
||||
tags: ['locations'],
|
||||
request: { name: 'LocationCreate', schema: LocationCreateSchema },
|
||||
responses: {
|
||||
201: { description: 'Créé', name: 'Location', schema: LocationSchema },
|
||||
400: { description: 'Hiérarchie trop profonde' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'updateLocation',
|
||||
method: 'patch',
|
||||
path: '/locations/{id}',
|
||||
summary: 'Modifier un emplacement',
|
||||
tags: ['locations'],
|
||||
pathParams: ['id'],
|
||||
request: { name: 'LocationUpdate', schema: LocationUpdateSchema },
|
||||
responses: {
|
||||
200: { description: 'Mis à jour', name: 'Location', schema: LocationSchema },
|
||||
404: { description: 'Inconnu' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'listAssets',
|
||||
method: 'get',
|
||||
path: '/assets',
|
||||
summary: 'Inventaire des appareils',
|
||||
tags: ['assets'],
|
||||
responses: {
|
||||
200: { description: 'Liste', name: 'AssetsResponse', schema: AssetsResponseSchema },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'getAsset',
|
||||
method: 'get',
|
||||
path: '/assets/{id}',
|
||||
summary: 'Fiche appareil (identité + organes)',
|
||||
tags: ['assets'],
|
||||
pathParams: ['id'],
|
||||
responses: {
|
||||
200: { description: 'Fiche', name: 'AssetDetail', schema: AssetDetailSchema },
|
||||
404: { description: 'Inconnu' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'createAsset',
|
||||
method: 'post',
|
||||
path: '/assets',
|
||||
summary: 'Créer un appareil (avec ses organes) — le QR découle de la référence',
|
||||
tags: ['assets'],
|
||||
request: { name: 'AssetCreate', schema: AssetCreateSchema },
|
||||
responses: {
|
||||
201: { description: 'Créé', name: 'AssetDetail', schema: AssetDetailSchema },
|
||||
409: { description: 'Référence déjà utilisée' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'updateAsset',
|
||||
method: 'patch',
|
||||
path: '/assets/{id}',
|
||||
summary: 'Modifier un appareil (dont son statut d’équipement)',
|
||||
tags: ['assets'],
|
||||
pathParams: ['id'],
|
||||
request: { name: 'AssetUpdate', schema: AssetUpdateSchema },
|
||||
responses: {
|
||||
200: { description: 'Mis à jour', name: 'AssetDetail', schema: AssetDetailSchema },
|
||||
404: { description: 'Inconnu' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'addAssetComponent',
|
||||
method: 'post',
|
||||
path: '/assets/{id}/components',
|
||||
summary: 'Ajouter un organe (jamais d’emplacement propre)',
|
||||
tags: ['assets'],
|
||||
pathParams: ['id'],
|
||||
request: { name: 'AssetComponentCreate', schema: AssetComponentCreateSchema },
|
||||
responses: {
|
||||
201: { description: 'Ajouté', name: 'AssetComponent', schema: AssetComponentSchema },
|
||||
400: { description: 'Le type choisi n’est pas un type d’organe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'removeAssetComponent',
|
||||
method: 'delete',
|
||||
path: '/assets/{id}/components/{componentId}',
|
||||
summary: 'Retirer un organe',
|
||||
tags: ['assets'],
|
||||
pathParams: ['id', 'componentId'],
|
||||
responses: {
|
||||
204: { description: 'Retiré' },
|
||||
404: { description: 'Inconnu' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'listTeams',
|
||||
method: 'get',
|
||||
path: '/teams',
|
||||
summary: 'Équipes et leurs membres',
|
||||
tags: ['teams'],
|
||||
responses: {
|
||||
200: { description: 'Liste', name: 'TeamsResponse', schema: TeamsResponseSchema },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'createTeam',
|
||||
method: 'post',
|
||||
path: '/teams',
|
||||
summary: 'Créer une équipe',
|
||||
tags: ['teams'],
|
||||
request: { name: 'TeamCreate', schema: TeamCreateSchema },
|
||||
responses: {
|
||||
201: { description: 'Créée', name: 'Team', schema: TeamSchema },
|
||||
409: { description: 'Nom déjà utilisé' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'updateTeam',
|
||||
method: 'patch',
|
||||
path: '/teams/{id}',
|
||||
summary: 'Modifier une équipe (nom, description, membres)',
|
||||
tags: ['teams'],
|
||||
pathParams: ['id'],
|
||||
request: { name: 'TeamUpdate', schema: TeamUpdateSchema },
|
||||
responses: {
|
||||
200: { description: 'Mise à jour', name: 'Team', schema: TeamSchema },
|
||||
404: { description: 'Inconnue' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'listUsers',
|
||||
method: 'get',
|
||||
path: '/users',
|
||||
summary: 'Personnes (statut dérivé : actif / invité / désactivé)',
|
||||
tags: ['users'],
|
||||
responses: {
|
||||
200: { description: 'Liste', name: 'UsersResponse', schema: UsersResponseSchema },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'listRoles',
|
||||
method: 'get',
|
||||
path: '/roles',
|
||||
summary: 'Les 7 rôles (pour l’invitation)',
|
||||
tags: ['users'],
|
||||
responses: {
|
||||
200: { description: 'Liste', name: 'RolesResponse', schema: RolesResponseSchema },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'inviteUser',
|
||||
method: 'post',
|
||||
path: '/users/invitations',
|
||||
summary: 'Inviter — crée le compte inactif et émet le lien d’activation (7 j)',
|
||||
tags: ['users'],
|
||||
request: { name: 'InvitationCreate', schema: InvitationCreateSchema },
|
||||
responses: {
|
||||
201: { description: 'Invitation émise', name: 'InvitationResponse', schema: InvitationResponseSchema },
|
||||
409: { description: 'Email déjà utilisé' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'resendInvitation',
|
||||
method: 'post',
|
||||
path: '/users/{id}/invitation',
|
||||
summary: 'Régénérer le lien d’activation d’un compte non activé',
|
||||
tags: ['users'],
|
||||
pathParams: ['id'],
|
||||
responses: {
|
||||
201: { description: 'Nouveau lien', name: 'InvitationResponse', schema: InvitationResponseSchema },
|
||||
409: { description: 'Compte déjà activé' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'updateUser',
|
||||
method: 'patch',
|
||||
path: '/users/{id}',
|
||||
summary: 'Modifier une personne (rôle, équipes, activation du compte)',
|
||||
tags: ['users'],
|
||||
pathParams: ['id'],
|
||||
request: { name: 'UserUpdate', schema: UserUpdateSchema },
|
||||
responses: {
|
||||
200: { description: 'Mise à jour', name: 'UserAdmin', schema: UserAdminSchema },
|
||||
404: { description: 'Inconnue' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'getHealth',
|
||||
method: 'get',
|
||||
|
||||
Reference in New Issue
Block a user