Files
siop2/apps/api/src/permissions/permissions.service.ts
pr-daaif 8adb561b63 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>
2026-07-15 21:55:04 +01:00

67 lines
1.9 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import type {
ObjectCategory,
PermissionEntry,
PermissionRight,
} from '@siop/shared';
import { PrismaService } from '../prisma/prisma.service';
const CACHE_TTL_MS = 60_000; // invariant R0 : la matrice est relue au plus toutes les 60 s
interface CacheEntry {
expiresAt: number;
permissions: PermissionEntry[];
}
@Injectable()
export class PermissionsService {
private readonly cache = new Map<string, CacheEntry>();
constructor(private readonly prisma: PrismaService) {}
async getForRole(roleId: string): Promise<PermissionEntry[]> {
const cached = this.cache.get(roleId);
if (cached && cached.expiresAt > Date.now()) return cached.permissions;
const rows = await this.prisma.permission.findMany({ where: { roleId } });
const permissions = rows.map((r) => ({
objectCategory: r.objectCategory as ObjectCategory,
canView: r.canView,
canViewOther: r.canViewOther,
canCreate: r.canCreate,
canEdit: r.canEdit,
canDelete: r.canDelete,
}));
this.cache.set(roleId, { expiresAt: Date.now() + CACHE_TTL_MS, permissions });
return permissions;
}
async can(
roleId: string,
category: ObjectCategory,
right: PermissionRight,
): Promise<boolean> {
const permissions = await this.getForRole(roleId);
const entry = permissions.find((p) => p.objectCategory === category);
if (!entry) return false;
switch (right) {
case 'view':
return entry.canView;
case 'viewOther':
return entry.canViewOther;
case 'create':
return entry.canCreate;
case 'edit':
return entry.canEdit;
case 'delete':
return entry.canDelete;
}
}
/** À appeler après toute modification de la matrice (admin, R1+). */
invalidate(roleId?: string): void {
if (roleId) this.cache.delete(roleId);
else this.cache.clear();
}
}