feat(r6): R6.6 — Demandeur restreint à son site

Trouvé en recette : GET /assets/options n'avait aucun filtre — tout
Demandeur voyait le parc complet dans le sélecteur d'équipement de
"Nouvelle demande", sur le web ET le mobile (même endpoint partagé).
Risque réel : signaler accidentellement une panne sur un ascenseur qu'on
ne gère pas.

Corrigé à la racine, sur les deux plateformes à la fois :

- Relation many-to-many User↔Location (assignedSites/assignedUsers,
  migration r6_demandeur_sites, même style que Team.members) — vide =
  aucune restriction, comportement historique inchangé pour tous les
  rôles sauf un Demandeur affecté à un site.
- AssetsService.allowedLocationIds(user) : sites + zones filles
  autorisés, ou null si aucune restriction — réutilisée par options()
  ET par RequestsService.create (défense en profondeur : un assetId
  soumis directement hors périmètre est rejeté, 400).
- UsersService : assertTopLevelSites (un Demandeur est affecté à un
  site, jamais une zone) ; invite()/update() branchent locationIds
  (remplace l'affectation, comme teamIds).
- Web (personnes.tsx) : ModaleInvitation affiche les sites à cocher pour
  un rôle Demandeur ; colonne "Sites" éditable via une modale dédiée.
- Mobile (formulaire-demande.tsx) : bouton "Scanner l'étiquette" en
  raccourci — résout uniquement contre les options déjà chargées (déjà
  filtrées), jamais de repli sur le parc complet qui annulerait la
  restriction. Aucun changement à useAssetOptions() : le filtrage
  serveur profite automatiquement au formulaire mobile.
- Seed : Karim Doukkali (démo) rattaché à Tour Atlas.

Bug trouvé en vérification avant tout commit : create() comparait
allowed.includes(dto.assetId), mais allowed est une liste d'ids de
sites/zones, pas d'ids d'appareils — aurait rejeté à tort tout
signalement d'un Demandeur affecté, y compris dans son propre périmètre.
Corrigé (comparaison sur asset.locationId) ; méthode renommée
allowedAssetIds → allowedLocationIds pour que le nom dise ce qu'elle
retourne.

exploitation.e2e-spec.ts mis à jour (A1/C1 → A2/B1, dans le site de
Karim — sinon rejetés par la nouvelle règle, comportement voulu).
79/80 tests API verts, le seul échec (documents-analytics, monthCost)
est le flake calendaire déjà identifié cette session, sans rapport.
Typecheck/tests/lint verts sur les 4 paquets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-08-02 20:22:50 +01:00
parent 1ae0529a6a
commit c9168ece16
17 changed files with 472 additions and 17 deletions

View File

@@ -17,6 +17,7 @@ import {
type AssetCreate,
type AssetUpdate,
} from '@siop/shared';
import { type AuthenticatedUser, CurrentUser } from '../auth/current-user.decorator';
import { ZodValidationPipe } from '../common/zod-validation.pipe';
import { RequirePermission } from '../permissions/require-permission.decorator';
import { AssetsService } from './assets.service';
@@ -32,10 +33,11 @@ export class AssetsController {
}
/** Avant ':id' (ordre des routes) — authentification seule : le demandeur
* doit pouvoir désigner l'appareil qu'il signale. */
* doit pouvoir désigner l'appareil qu'il signale (filtré à son site s'il
* en a un, R6.6). */
@Get('options')
options() {
return this.assetsService.options();
options(@CurrentUser() user: AuthenticatedUser) {
return this.assetsService.options(user);
}
@Get(':id')