'Administrateur', 'responsable' => 'Responsable', 'benevole' => 'Bénévole', 'lecture' => 'Lecture seule', ]; public const MODULES = [ 'dashboard' => ['Dashboard', 'Priorités et situation quotidienne'], 'animals' => ['Animaux', 'Fiches, portées, liens, médias et adoptions'], 'medical' => ['Médical', 'Notes, traitements, vaccinations et décès'], 'care' => ['Tournées', 'Passages tablette et configuration des box'], 'directory' => ['Annuaire', 'Personnes, structures et coordonnées'], 'agenda' => ['Agenda', 'Rendez-vous, transports, visites et tâches partagées'], 'statistics' => ['Statistiques', 'Analyses annuelles et tendances'], 'administrative' => ['Administratif', 'Registre, documents et exports'], 'accounting' => ['Comptabilité', 'Factures, avoirs et règlements'], 'grants' => ['Subventions', 'Bilans, demandes et justificatifs'], ]; public static function roleLabel(string $key): string { return t('permission.role.' . $key, [], self::ROLES[$key] ?? $key); } public static function moduleLabel(string $key): string { return t('permission.module.' . $key, [], self::MODULES[$key][0] ?? $key); } public static function moduleDescription(string $key): string { return t('permission.module_help.' . $key, [], self::MODULES[$key][1] ?? ''); } private static array $cache = []; public static function can(string $module, string $action = 'view', ?string $role = null): bool { $role ??= (string) (Auth::user()['role'] ?? ''); if ($role === 'admin') { return true; } if (!isset(self::MODULES[$module], self::ROLES[$role])) { return false; } if (!isset(self::$cache[$role])) { $s = DB::pdo()->prepare('SELECT module,can_view,can_edit FROM role_permissions WHERE role=?'); $s->execute([$role]); self::$cache[$role] = []; foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $row) { self::$cache[$role][$row['module']] = [ 'view' => (bool) $row['can_view'], 'edit' => (bool) $row['can_edit'], ]; } } return (bool) (self::$cache[$role][$module][$action === 'edit' ? 'edit' : 'view'] ?? false); } public static function matrix(): array { $matrix = []; foreach (array_keys(self::ROLES) as $role) { foreach (array_keys(self::MODULES) as $module) { $matrix[$role][$module] = [ 'view' => self::can($module, 'view', $role), 'edit' => self::can($module, 'edit', $role), ]; } } return $matrix; } public static function moduleForPath(string $path): ?string { if ($path === '/') { return 'dashboard'; } if (str_starts_with($path, '/animal/documents')) { return 'medical'; } if ( str_starts_with($path, '/animals') || str_starts_with($path, '/animal') || str_starts_with($path, '/litter') || str_starts_with($path, '/bonded') ) { foreach ( [ '/animal/add-medical', '/animal/medical-status', '/animal/add-treatment', '/animal/add-vaccine', '/animal/death', '/animal/prescription', '/animal/lab-report', '/animal/medical-document', '/animal/surgery', ] as $medical ) { if (str_starts_with($path, $medical)) { return 'medical'; } } return 'animals'; } if (str_starts_with($path, '/dashboard')) { return 'dashboard'; } if (str_starts_with($path, '/care-round')) { return 'care'; } if (str_starts_with($path, '/directory')) { return 'directory'; } if (str_starts_with($path, '/agenda')) { return 'agenda'; } if (str_starts_with($path, '/statistics')) { return 'statistics'; } if (str_starts_with($path, '/accounting')) { return 'accounting'; } if (str_starts_with($path, '/admin/grants')) { return 'grants'; } if (str_starts_with($path, '/admin') || str_starts_with($path, '/documents')) { return 'administrative'; } return null; } public static function enforce(string $path, string $method): void { if ( str_starts_with($path, '/settings') || str_starts_with($path, '/dev/') || in_array($path, ['/logout', '/evolutions'], true) ) { return; } $module = self::moduleForPath($path); if (!$module) { return; } $action = strtoupper($method) === 'POST' || self::isEditPage($path) ? 'edit' : 'view'; $allowed = self::can($module, $action) && ($module !== 'medical' || self::can('animals', 'view')); if (!$allowed) { AuditService::log('authorization_denied', $path, 'Accès refusé au module ' . $module, null, null, [ 'module' => $module, 'requested_action' => $action, ]); http_response_code(403); if (function_exists('render')) { render('access_denied.php', [ 'title' => t('access.denied'), 'deniedAction' => $action, 'deniedModule' => self::moduleLabel($module), ]); } else { echo t('access.denied'); } exit(); } } public static function firstAllowedPath(): string { foreach ( [ 'dashboard' => '/dashboard', 'animals' => '/animals', 'care' => '/care-round', 'directory' => '/directory', 'agenda' => '/agenda', 'statistics' => '/statistics', 'accounting' => '/accounting', 'administrative' => '/admin/register', 'grants' => '/admin/grants', ] as $module => $path ) { if (self::can($module)) { return $path; } } return '/evolutions'; } public static function canOpenPath(string $path): bool { $module = self::moduleForPath($path); return $module === null || (self::can($module, self::isEditPage($path) ? 'edit' : 'view') && ($module !== 'medical' || self::can('animals'))); } public static function currentPermissions(): array { $out = []; foreach (self::MODULES as $module => $unused) { $out[$module] = ['view' => self::can($module), 'edit' => self::can($module, 'edit')]; } return $out; } private static function isEditPage(string $path): bool { return in_array( $path, [ '/animal/new', '/animal/edit', '/animal/death', '/animal/surgery/new', '/litter/new', '/litter/add-existing', '/litter/add-existing/save', '/bonded/new', '/directory/new', '/directory/edit', '/care-round/setup', ], true, ); } public static function save(array $input, ?int $userId): void { $db = DB::pdo(); $stmt = $db->prepare( "INSERT INTO role_permissions(role,module,can_view,can_edit,updated_by,updated_at) VALUES(:role,:module,:view,:edit,:user,datetime('now')) ON CONFLICT(role,module) DO UPDATE SET can_view=excluded.can_view,can_edit=excluded.can_edit,updated_by=excluded.updated_by,updated_at=excluded.updated_at", ); $db->beginTransaction(); try { foreach (self::ROLES as $role => $unused) { if ($role === 'admin') { continue; } foreach (self::MODULES as $module => $unusedModule) { $view = isset($input[$role][$module]['view']) ? 1 : 0; if ($module === 'medical' && !isset($input[$role]['animals']['view'])) { $view = 0; } $edit = $view && isset($input[$role][$module]['edit']) ? 1 : 0; $stmt->execute([ ':role' => $role, ':module' => $module, ':view' => $view, ':edit' => $edit, ':user' => $userId, ]); } } $db->commit(); self::$cache = []; } catch (Throwable $e) { $db->rollBack(); throw $e; } } }