feat: roles, permissions, grille full-width, SSO display name

- Admin/roles : tableau des roles avec edition par role (/admin/role/<nom>)
- Permissions par role : cases a cocher groupees (Articles, Acces & lecture)
- Nouvelles capacites : propose/validate/publish articles (own/all), view_previews
- Nom technique auto-genere depuis le label (JS + fallback serveur)
- Blocage suppression du dernier administrateur
- user_capabilities table ajoutee en DB
- Navbar : dropdown unique (nom + Mon identite + Administration + Deconnexion)
- SSO callback : preserve le nom personnalise, ne l ecrase plus a la connexion
- Grille articles : CSS Grid auto-fill full-width, hauteur uniforme par ligne
- CSP : add_files.js et post_confirm.js externalises
This commit is contained in:
Cedric Abonnel
2026-05-12 15:51:06 +02:00
parent 5275edfd20
commit 1d2e3d9a24
15 changed files with 1029 additions and 332 deletions
+83 -147
View File
@@ -144,6 +144,12 @@ function adminStatusBadge(array $a, int $now): string
<!-- ─────────────────────────── UTILISATEURS ─────────────────────────── -->
<?php elseif ($tab === 'users' && isAdmin()): ?>
<?php if (($_GET['error'] ?? '') === 'last_admin'): ?>
<div class="alert alert-danger py-2 mb-3">
Impossible de retirer le rôle Administrateur : il doit rester au moins un administrateur.
</div>
<?php endif; ?>
<!-- Ajouter / attribuer un rôle -->
<div class="card mb-4">
<div class="card-header">Attribuer un rôle</div>
@@ -198,13 +204,13 @@ function adminStatusBadge(array $a, int $now): string
<?php endif; ?>
</td>
<td>
<?php foreach ($u['roles'] as $roleName): ?>
<span class="badge bg-primary me-1"><?= htmlspecialchars($roleName) ?></span>
<?php foreach ($u['roles'] as $role): ?>
<span class="badge bg-primary me-1"><?= htmlspecialchars($role['label']) ?></span>
<form method="post" action="/?action=admin_revoke_role"
class="d-inline"
data-confirm="Retirer le rôle <?= htmlspecialchars($roleName) ?> à <?= htmlspecialchars($u['email']) ?> ?">
data-confirm="Retirer le rôle «<?= htmlspecialchars($role['label']) ?>» à <?= htmlspecialchars($u['email']) ?> ?">
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
<input type="hidden" name="role" value="<?= htmlspecialchars($roleName) ?>">
<input type="hidden" name="role" value="<?= htmlspecialchars($role['name']) ?>">
<button type="submit" class="btn btn-link btn-sm p-0 text-danger" title="Retirer">×</button>
</form>
<?php endforeach; ?>
@@ -215,8 +221,8 @@ function adminStatusBadge(array $a, int $now): string
<td class="text-end">
<!-- Ajout rapide d'un rôle existant -->
<?php
$currentRoles = $u['roles'];
$missing = array_filter($adminData['roles'], fn ($r) => !in_array($r['name'], $currentRoles, true));
$currentRoleNames = array_column($u['roles'], 'name');
$missing = array_filter($adminData['roles'], fn ($r) => !in_array($r['name'], $currentRoleNames, true));
?>
<?php if ($missing): ?>
<form method="post" action="/?action=admin_grant_role" class="d-inline-flex gap-1">
@@ -241,151 +247,81 @@ function adminStatusBadge(array $a, int $now): string
<!-- ─────────────────────────── RÔLES ─────────────────────────── -->
<?php elseif ($tab === 'roles' && isAdmin()): ?>
<div class="row g-4">
<div class="row g-4">
<!-- Liste des rôles existants -->
<div class="col-lg-8">
<h5 class="mb-3">Rôles existants</h5>
<?php if (empty($adminData['roles'])): ?>
<p class="text-muted">Aucun rôle défini.</p>
<?php else: ?>
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th style="width:160px">Nom technique</th>
<th>Label affiché</th>
<th class="text-center" style="width:90px">Utilisateurs</th>
<th style="width:100px"></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['roles'] as $r): ?>
<tr>
<td><code class="text-body"><?= htmlspecialchars($r['name']) ?></code></td>
<td>
<form method="post" action="/?action=admin_update_role"
class="d-flex gap-2 align-items-center">
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<input type="text" name="label"
value="<?= htmlspecialchars($r['label']) ?>"
class="form-control form-control-sm" required>
<button type="submit" class="btn btn-outline-secondary btn-sm text-nowrap">
Sauver
</button>
</form>
</td>
<td class="text-center">
<span class="badge bg-secondary"><?= (int)$r['user_count'] ?></span>
</td>
<td class="text-end">
<form method="post" action="/?action=admin_delete_role"
data-confirm="Supprimer le rôle «<?= htmlspecialchars($r['name']) ?>» ?<?= (int)$r['user_count'] > 0 ? ' ' . (int)$r['user_count'] . ' utilisateur(s) perdront ce rôle.' : '' ?>">
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<button type="submit" class="btn btn-outline-danger btn-sm">
Supprimer
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Créer un rôle -->
<div class="col-lg-4">
<div class="card">
<div class="card-header">Nouveau rôle</div>
<div class="card-body">
<form method="post" action="/?action=admin_create_role">
<div class="mb-3">
<label class="form-label small fw-semibold">Nom technique</label>
<input type="text" name="name" class="form-control form-control-sm"
placeholder="ex : moderator"
pattern="[a-z0-9_-]+"
title="Lettres minuscules, chiffres, tirets et underscores uniquement"
required>
<div class="form-text">Utilisé dans le code — ne change pas.</div>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Label affiché</label>
<input type="text" name="label" class="form-control form-control-sm"
placeholder="ex : Modérateur" required>
</div>
<button type="submit" class="btn btn-primary btn-sm w-100">Créer</button>
</form>
</div>
</div>
</div>
<!-- Permissions par rôle -->
<?php if (!empty($adminData['roles'])): ?>
<div class="col-12 mt-2">
<h5 class="mb-3">Permissions par rôle</h5>
<p class="text-muted small mb-3">Le rôle <code>admin</code> a toutes les permissions implicitement.</p>
<div class="row g-3">
<?php foreach ($adminData['roles'] as $r):
if ($r['name'] === 'admin') {
continue;
} ?>
<div class="col-md-6 col-lg-4">
<div class="card h-100">
<div class="card-header py-2 d-flex align-items-center justify-content-between">
<span class="fw-semibold"><?= htmlspecialchars($r['label']) ?></span>
<code class="text-muted small"><?= htmlspecialchars($r['name']) ?></code>
</div>
<div class="card-body py-3">
<form method="post" action="/?action=admin_update_role_caps">
<input type="hidden" name="role_id" value="<?= (int)$r['id'] ?>">
<?php foreach (CAPABILITY_GROUPS as $group): ?>
<?php if (isset($group['single'])): ?>
<?php $cap = $group['single']; ?>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox"
name="caps[]" value="<?= htmlspecialchars($cap) ?>"
id="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>"
<?= in_array($cap, $r['capabilities'], true) ? 'checked' : '' ?>>
<label class="form-check-label small fw-semibold"
for="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>">
<?= htmlspecialchars($group['label']) ?>
</label>
</div>
<?php else: ?>
<div class="mb-3">
<div class="small fw-semibold mb-1"><?= htmlspecialchars($group['label']) ?></div>
<div class="d-flex gap-3 ps-1">
<?php foreach (['own' => 'Propres articles', 'all' => 'Tous'] as $scope => $scopeLabel):
$cap = $group[$scope]; ?>
<div class="form-check">
<input class="form-check-input" type="checkbox"
name="caps[]" value="<?= htmlspecialchars($cap) ?>"
id="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>"
<?= in_array($cap, $r['capabilities'], true) ? 'checked' : '' ?>>
<label class="form-check-label small"
for="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>">
<?= $scopeLabel ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
<button type="submit" class="btn btn-outline-secondary btn-sm mt-1 w-100">
Enregistrer
</button>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Tableau des rôles existants -->
<div class="col-lg-8">
<?php if (empty($adminData['roles'])): ?>
<p class="text-muted">Aucun rôle défini.</p>
<?php else: ?>
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Rôle</th>
<th class="text-center" style="width:100px">Utilisateurs</th>
<th style="width:110px"></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['roles'] as $r): ?>
<tr>
<td>
<?= htmlspecialchars($r['label']) ?>
<?php if ($r['name'] !== 'admin'): ?>
<div class="text-muted small">
<?php
$capLabels = array_map(
fn ($c) => KNOWN_CAPABILITIES[$c] ?? $c,
$r['capabilities']
);
echo htmlspecialchars(implode(', ', $capLabels) ?: '');
?>
</div>
<?php else: ?>
<div class="text-muted small">Toutes les permissions</div>
<?php endif; ?>
</td>
<td class="text-center">
<span class="badge bg-secondary"><?= (int)$r['user_count'] ?></span>
</td>
<td class="text-end d-flex gap-1 justify-content-end">
<a href="/admin/role/<?= rawurlencode($r['name']) ?>"
class="btn btn-outline-secondary btn-sm">Éditer</a>
<?php if ((int)$r['user_count'] === 0 && $r['name'] !== 'admin'): ?>
<form method="post" action="/?action=admin_delete_role"
data-confirm="Supprimer le rôle «<?= htmlspecialchars($r['name']) ?>» ?">
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<button type="submit" class="btn btn-outline-danger btn-sm">×</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Créer un rôle -->
<div class="col-lg-4">
<div class="card">
<div class="card-header">Nouveau rôle</div>
<div class="card-body">
<form method="post" action="/?action=admin_create_role">
<div class="mb-3">
<label for="role-label" class="form-label small fw-semibold">Nom du rôle</label>
<input type="text" id="role-label" name="label" class="form-control form-control-sm"
placeholder="ex : Modérateur" required autocomplete="off">
</div>
<input type="hidden" id="role-name" name="name">
<button type="submit" class="btn btn-primary btn-sm w-100">Créer</button>
</form>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php