mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
- 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>
67 lines
1.9 KiB
TypeScript
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();
|
|
}
|
|
}
|