mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r2.4): portail public QR — signalement sans compte, suivi par jeton
- 3 routes publiques /portal (les seules @Public métier), throttlées
(@nestjs/throttler : 10 signalements/min, 60 lectures/min) : résolution
du QR, signalement (retourne un jeton de suivi opaque), suivi par
référence + jeton — pas de jeton, pas de lecture, jamais de liste
- migration r2_portail : Request.publicToken (unique)
- page /q/{réf} fidèle à l'écran 6 validé R0 (mobile d'abord) : équipement
prérempli « détecté par le QR », interrupteur personne bloquée,
suivi sans jargon Reçu → Intervention → Résolu (dérivé de l'OT lié),
rejet affiché « Sans suite : {motif} », photo annoncée (upload R3) ;
signalements gardés sur le téléphone (localStorage, 10 max)
- e2e : LA boucle produit — gardien sans compte → traitement → ✓ Résolu ;
11 tests Playwright verts, 50 tests API, 52 opérations au contrat
- générateur OpenAPI : paramètres non-« id » plus typés uuid
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,11 @@ for (const op of API_CONTRACT) {
|
||||
name,
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string', format: 'uuid' },
|
||||
// Convention : « id » / «…Id » sont des UUID, le reste est libre
|
||||
schema:
|
||||
name === 'id' || name.endsWith('Id')
|
||||
? { type: 'string', format: 'uuid' }
|
||||
: { type: 'string' },
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -38,6 +38,12 @@ import {
|
||||
UsersResponseSchema,
|
||||
UserUpdateSchema,
|
||||
} from './schemas/users-admin';
|
||||
import {
|
||||
PortalAssetSchema,
|
||||
PortalRequestCreatedSchema,
|
||||
PortalRequestCreateSchema,
|
||||
PortalRequestStatusSchema,
|
||||
} from './schemas/portail';
|
||||
import {
|
||||
MeterReadingCreateSchema,
|
||||
MetersResponseSchema,
|
||||
@@ -413,6 +419,52 @@ export const API_CONTRACT: ApiOperation[] = [
|
||||
404: { description: 'Inconnue' },
|
||||
},
|
||||
},
|
||||
// ————— R2 · Portail public (QR cabine — aucun compte) —————
|
||||
{
|
||||
operationId: 'getPortalAsset',
|
||||
method: 'get',
|
||||
path: '/portal/assets/{reference}',
|
||||
summary: 'Résoudre le QR scanné (référence → appareil, préremplissage)',
|
||||
tags: ['portal'],
|
||||
isPublic: true,
|
||||
pathParams: ['reference'],
|
||||
responses: {
|
||||
200: { description: 'Appareil', name: 'PortalAsset', schema: PortalAssetSchema },
|
||||
404: { description: 'Référence inconnue' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'createPortalRequest',
|
||||
method: 'post',
|
||||
path: '/portal/requests',
|
||||
summary: 'Signaler sans compte — renvoie le jeton de suivi (à garder sur le téléphone)',
|
||||
tags: ['portal'],
|
||||
isPublic: true,
|
||||
request: { name: 'PortalRequestCreate', schema: PortalRequestCreateSchema },
|
||||
responses: {
|
||||
201: {
|
||||
description: 'Signalement enregistré',
|
||||
name: 'PortalRequestCreated',
|
||||
schema: PortalRequestCreatedSchema,
|
||||
},
|
||||
404: { description: 'Référence inconnue' },
|
||||
429: { description: 'Trop de signalements — réessayez dans une minute' },
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: 'getPortalRequestStatus',
|
||||
method: 'get',
|
||||
path: '/portal/requests/{reference}/{token}',
|
||||
summary: 'Suivi sans jargon (Reçu → Intervention → Résolu) — jeton requis',
|
||||
tags: ['portal'],
|
||||
isPublic: true,
|
||||
pathParams: ['reference', 'token'],
|
||||
responses: {
|
||||
200: { description: 'Avancement', name: 'PortalRequestStatus', schema: PortalRequestStatusSchema },
|
||||
404: { description: 'Signalement inconnu ou jeton invalide' },
|
||||
},
|
||||
},
|
||||
|
||||
// ————— R2 · Préventif & compteurs —————
|
||||
{
|
||||
operationId: 'listTaskTemplates',
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from './referentiel';
|
||||
export * from './exploitation';
|
||||
export * from './schemas/exploitation';
|
||||
export * from './schemas/preventif';
|
||||
export * from './schemas/portail';
|
||||
export * from './schemas/auth';
|
||||
export * from './schemas/users';
|
||||
export * from './schemas/users-admin';
|
||||
|
||||
42
packages/shared/src/schemas/portail.ts
Normal file
42
packages/shared/src/schemas/portail.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
import { REQUEST_STATUSES } from '../exploitation';
|
||||
|
||||
/**
|
||||
* Portail public (QR cabine) — AUCUN compte, AUCUN jargon (charte §7).
|
||||
* Le suivi se fait par jeton opaque gardé sur le téléphone du gardien.
|
||||
*/
|
||||
|
||||
export const PortalAssetSchema = z.object({
|
||||
reference: z.string(),
|
||||
siteName: z.string(),
|
||||
locationName: z.string(),
|
||||
});
|
||||
export type PortalAsset = z.infer<typeof PortalAssetSchema>;
|
||||
|
||||
export const PortalRequestCreateSchema = z.object({
|
||||
assetReference: z.string().min(1).max(30),
|
||||
description: z.string().min(3).max(1000),
|
||||
isPersonTrapped: z.boolean().optional(),
|
||||
requesterName: z.string().max(120).optional(),
|
||||
});
|
||||
export type PortalRequestCreate = z.infer<typeof PortalRequestCreateSchema>;
|
||||
|
||||
/** Étapes lisibles par tous : Reçu → Intervention → Résolu. */
|
||||
export const PORTAL_STEPS = ['RECEIVED', 'IN_PROGRESS', 'RESOLVED'] as const;
|
||||
export type PortalStep = (typeof PORTAL_STEPS)[number];
|
||||
|
||||
export const PortalRequestStatusSchema = z.object({
|
||||
reference: z.string(),
|
||||
description: z.string(),
|
||||
isPersonTrapped: z.boolean(),
|
||||
status: z.enum(REQUEST_STATUSES),
|
||||
step: z.enum(PORTAL_STEPS),
|
||||
rejectionReason: z.string().nullable(),
|
||||
createdAt: z.iso.datetime(),
|
||||
});
|
||||
export type PortalRequestStatus = z.infer<typeof PortalRequestStatusSchema>;
|
||||
|
||||
export const PortalRequestCreatedSchema = PortalRequestStatusSchema.extend({
|
||||
publicToken: z.string(), // à conserver côté téléphone — seul accès au suivi
|
||||
});
|
||||
export type PortalRequestCreated = z.infer<typeof PortalRequestCreatedSchema>;
|
||||
Reference in New Issue
Block a user