Publier Globinours 1.0.0-rc.3

This commit is contained in:
Alexandre NOEL 2026-09-03 12:39:15 +02:00
commit 9a2b4068da
325 changed files with 38230 additions and 20 deletions

View file

@ -0,0 +1,258 @@
<?php
declare(strict_types=1);
final class PermissionService
{
public const ROLES = [
'admin' => '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;
}
}
}