corentin_wakdo/src/app/Auth/RoleRepository.php
Corentin JOGUET d880f2512a
All checks were successful
CI / php-lint (push) Successful in 21s
CI / js-tests (push) Successful in 21s
CI / secret-scan (push) Successful in 10s
CI / static-tests (push) Successful in 44s
CI / auto-merge (push) Has been skipped
feat(admin): RBAC - matrice roles/permissions + roles custom (PIN+audit diff) (P3) (#39)
2026-06-17 14:25:42 +02:00

214 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Auth;
use App\Core\DatabaseInterface;
/**
* Acces aux donnees RBAC en ECRITURE (mlt 10.4 MANAGE_RBAC) : roles, matrice
* role_permission, sources visibles role_visible_source. La lecture des
* permissions pour l'autorisation reste dans Authorizer (rechargee a chaque
* verification). Le catalogue `permission` est fige au seed (lecture seule ici).
*
* Invariants : le `code` d'un role est UNIQUE et IMMUABLE apres creation (cle
* referencee par la logique applicative). La matrice et les sources sont reposees
* en delete-and-reinsert dans UNE transaction (RG-T08 / mlt 10.4 RG-1).
*/
final class RoleRepository
{
public function __construct(private readonly DatabaseInterface $db)
{
}
/**
* @return array<int, array<string, mixed>>
*/
public function allRoles(): array
{
return $this->db->fetchAll(
'SELECT id, code, label, description, default_route, order_source, is_active '
. 'FROM role ORDER BY id',
);
}
/**
* @return array<string, mixed>|null
*/
public function findRole(int $id): ?array
{
return $this->db->fetch(
'SELECT id, code, label, description, default_route, order_source, is_active '
. 'FROM role WHERE id = :id',
['id' => $id],
);
}
public function codeExists(string $code, int $exceptId = 0): bool
{
return $this->db->fetch(
'SELECT id FROM role WHERE code = :code AND id <> :id',
['code' => $code, 'id' => $exceptId],
) !== null;
}
/**
* Catalogue complet des permissions (fige au seed), pour peupler la matrice.
*
* @return array<int, array<string, mixed>>
*/
public function allPermissions(): array
{
return $this->db->fetchAll('SELECT id, code, label FROM permission ORDER BY id');
}
/**
* @return list<int>
*/
public function permissionIdsFor(int $roleId): array
{
$rows = $this->db->fetchAll(
'SELECT permission_id FROM role_permission WHERE role_id = :id',
['id' => $roleId],
);
return array_map(static fn (array $r): int => (int) ($r['permission_id'] ?? 0), $rows);
}
/**
* Codes de permission d'un role (pour le diff d'audit RG-6 : add/remove).
*
* @return list<string>
*/
public function permissionCodesFor(int $roleId): array
{
$rows = $this->db->fetchAll(
'SELECT p.code FROM role_permission rp JOIN permission p ON p.id = rp.permission_id '
. 'WHERE rp.role_id = :id ORDER BY p.code',
['id' => $roleId],
);
return array_map(static fn (array $r): string => (string) ($r['code'] ?? ''), $rows);
}
/**
* Reecrit la matrice d'un role (mlt 10.4 RG-1) : DELETE puis INSERT des paires
* selectionnees, dans UNE transaction. L'appelant a deja filtre les
* permission_id au catalogue existant (PRE-3).
*
* @param list<int> $permissionIds
*/
public function setPermissions(int $roleId, array $permissionIds): void
{
$this->db->transaction(function (DatabaseInterface $db) use ($roleId, $permissionIds): void {
$this->replacePermissions($db, $roleId, $permissionIds);
});
}
/**
* Variante SANS transaction propre : reecrit la matrice sur le $db fourni, pour
* que le controleur l'enrobe dans UNE transaction avec l'ecriture d'audit (RG-6,
* audit du diff dans la meme transaction que l'effet). Ne pas appeler hors d'une
* transaction de l'appelant.
*
* @param list<int> $permissionIds
*/
public function replacePermissions(DatabaseInterface $db, int $roleId, array $permissionIds): void
{
$db->execute('DELETE FROM role_permission WHERE role_id = :id', ['id' => $roleId]);
foreach (array_values(array_unique($permissionIds)) as $permissionId) {
$db->execute(
'INSERT INTO role_permission (role_id, permission_id) VALUES (:role, :perm)',
['role' => $roleId, 'perm' => $permissionId],
);
}
}
/**
* Creation d'un role personnalise (mlt 10.4 RG-4). `is_active` pose cote serveur.
* Retourne l'id. Allowlist RG-T16.
*
* @param array{code: string, label: string, description: ?string, default_route: ?string, order_source: ?string} $data
*/
public function createRole(array $data): int
{
$this->db->execute(
'INSERT INTO role (code, label, description, default_route, order_source, is_active) '
. 'VALUES (:code, :label, :description, :route, :source, 1)',
[
'code' => $data['code'],
'label' => $data['label'],
'description' => $data['description'],
'route' => $data['default_route'],
'source' => $data['order_source'],
],
);
return (int) ($this->db->fetch('SELECT LAST_INSERT_ID() AS id')['id'] ?? 0);
}
/**
* Mise a jour d'un role. Le `code` n'est PAS lie (immuable apres creation).
*
* @param array{label: string, description: ?string, default_route: ?string, order_source: ?string, is_active: int} $data
*/
public function updateRole(int $id, array $data): void
{
$this->db->execute(
'UPDATE role SET label = :label, description = :description, default_route = :route, '
. 'order_source = :source, is_active = :active WHERE id = :id',
[
'label' => $data['label'],
'description' => $data['description'],
'route' => $data['default_route'],
'source' => $data['order_source'],
'active' => $data['is_active'],
'id' => $id,
],
);
}
/**
* @return list<string>
*/
public function visibleSources(int $roleId): array
{
$rows = $this->db->fetchAll(
'SELECT source FROM role_visible_source WHERE role_id = :id',
['id' => $roleId],
);
return array_map(static fn (array $r): string => (string) ($r['source'] ?? ''), $rows);
}
/**
* Reecrit les sources visibles d'un role (delete-and-reinsert, tx). L'appelant
* filtre $sources a l'ENUM valide ('kiosk','counter','drive').
*
* @param list<string> $sources
*/
public function setVisibleSources(int $roleId, array $sources): void
{
$this->db->transaction(function (DatabaseInterface $db) use ($roleId, $sources): void {
$this->replaceVisibleSources($db, $roleId, $sources);
});
}
/**
* Variante SANS transaction propre (cf. replacePermissions), pour enrobage par
* le controleur dans une transaction unique.
*
* @param list<string> $sources
*/
public function replaceVisibleSources(DatabaseInterface $db, int $roleId, array $sources): void
{
$db->execute('DELETE FROM role_visible_source WHERE role_id = :id', ['id' => $roleId]);
foreach (array_values(array_unique($sources)) as $source) {
$db->execute(
'INSERT INTO role_visible_source (role_id, source) VALUES (:role, :source)',
['role' => $roleId, 'source' => $source],
);
}
}
}