mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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>
This commit is contained in:
66
apps/api/src/permissions/permissions.service.ts
Normal file
66
apps/api/src/permissions/permissions.service.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user