Files
varlog/templates/admin.php
T
Cedric Abonnel 4bf0b65088 Fix: masquer les avant-premières pour les visiteurs sans view_previews
Les articles en avant-première (published_at > now) s'affichaient dans
la grille pour tous les visiteurs, occupant des slots de pagination sans
être lisibles. Ils sont désormais filtrés côté serveur pour les
utilisateurs sans la capability view_previews, cohérent avec le
comportement déjà en place dans la recherche et les pages auteur.
2026-05-13 01:57:02 +02:00

541 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
ob_start();
$now = time();
function adminStatusBadge(array $a, int $now): string
{
if (!$a['published']) {
return '<span class="badge bg-secondary">Brouillon</span>';
}
if (strtotime((string)($a['published_at'] ?? '')) > $now) {
return '<span class="badge bg-warning text-dark">Avant-première</span>';
}
return '<span class="badge bg-success">Publié</span>';
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Administration</h1>
<a href="/new" class="btn btn-primary btn-sm">+ Nouvel article</a>
</div>
<!-- Onglets -->
<ul class="nav nav-tabs mb-4">
<?php if (isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $tab === 'dashboard' ? 'active' : '' ?>"
href="/admin/dashboard">Tableau de bord</a>
</li>
<?php endif; ?>
<li class="nav-item">
<a class="nav-link <?= $tab === 'articles' ? 'active' : '' ?>"
href="/admin/articles"><?= isAdmin() ? 'Articles' : 'Mes articles' ?></a>
</li>
<?php if (isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $tab === 'users' ? 'active' : '' ?>"
href="/admin/users">Utilisateurs</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'roles' ? 'active' : '' ?>"
href="/admin/roles">Rôles</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/categories">Catégories</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'site' ? 'active' : '' ?>"
href="/admin/site">Site</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'comments' ? 'active' : '' ?>"
href="/admin/comments">Commentaires</a>
</li>
<?php endif; ?>
</ul>
<!-- ─────────────────────────── DASHBOARD ─────────────────────────── -->
<?php if ($tab === 'dashboard' && isAdmin()): ?>
<div class="row g-3 mb-4">
<?php
$stats = [
['label' => 'Publiés', 'value' => $adminData['published'], 'color' => 'success'],
['label' => 'Avant-premières', 'value' => $adminData['previews'], 'color' => 'warning'],
['label' => 'Brouillons', 'value' => $adminData['drafts'], 'color' => 'secondary'],
['label' => 'Total', 'value' => $adminData['total'], 'color' => 'primary'],
];
foreach ($stats as $s): ?>
<div class="col-6 col-md-3">
<div class="card text-center">
<div class="card-body">
<div class="display-6 fw-bold text-<?= $s['color'] ?>"><?= $s['value'] ?></div>
<div class="text-muted small"><?= $s['label'] ?></div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<h5>Activité récente</h5>
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Titre</th>
<th>Auteur</th>
<th>Statut</th>
<th>Modifié le</th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['recent'] as $a): ?>
<tr>
<td>
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
<?= htmlspecialchars($a['title']) ?>
</a>
</td>
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '') ?></td>
<td><?= adminStatusBadge($a, $now) ?></td>
<td class="text-muted small">
<?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($a['updated_at'] ?? $a['created_at'] ?? '')))) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- ─────────────────────────── ARTICLES ─────────────────────────── -->
<?php elseif ($tab === 'articles'): ?>
<!-- Filtres -->
<form class="row g-2 align-items-center mb-3" method="get" action="/admin/articles">
<?php if (isAdmin() && !empty($adminData['filter_authors'])): ?>
<div class="col-auto">
<select name="filter_author" class="form-select form-select-sm">
<option value="">Tous les auteurs</option>
<?php foreach ($adminData['filter_authors'] as $_fa): ?>
<option value="<?= htmlspecialchars($_fa) ?>"
<?= ($adminData['filter_author'] ?? '') === $_fa ? 'selected' : '' ?>>
<?= htmlspecialchars($_fa) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<?php if (!empty($adminData['filter_categories'])): ?>
<div class="col-auto">
<select name="filter_category" class="form-select form-select-sm">
<option value="">Toutes les catégories</option>
<?php foreach ($adminData['filter_categories'] as $_fc): ?>
<option value="<?= htmlspecialchars($_fc) ?>"
<?= ($adminData['filter_category'] ?? '') === $_fc ? 'selected' : '' ?>>
<?= htmlspecialchars($_fc) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-auto">
<select name="filter_status" class="form-select form-select-sm">
<option value="">Tous les statuts</option>
<option value="published" <?= ($adminData['filter_status'] ?? '') === 'published' ? 'selected' : '' ?>>Publié</option>
<option value="draft" <?= ($adminData['filter_status'] ?? '') === 'draft' ? 'selected' : '' ?>>Brouillon</option>
<option value="preview" <?= ($adminData['filter_status'] ?? '') === 'preview' ? 'selected' : '' ?>>Avant-première</option>
</select>
</div>
<div class="col-auto d-flex gap-2">
<button type="submit" class="btn btn-secondary btn-sm">Filtrer</button>
<?php $hasFilter = ($adminData['filter_author'] ?? '') !== '' || ($adminData['filter_category'] ?? '') !== '' || ($adminData['filter_status'] ?? '') !== ''; ?>
<?php if ($hasFilter): ?>
<a href="/admin/articles" class="btn btn-link btn-sm p-0">Réinitialiser</a>
<?php endif; ?>
</div>
<?php if ($hasFilter): ?>
<div class="col-auto">
<span class="text-muted small"><?= count($adminData['articles']) ?> résultat(s)</span>
</div>
<?php endif; ?>
</form>
<?php if (empty($adminData['articles'])): ?>
<p class="text-muted">Aucun article.</p>
<?php else: ?>
<form method="post" action="/?action=admin_bulk_delete" id="bulk-form">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" id="check-all">
<label class="form-check-label small text-muted" for="check-all">Tout sélectionner</label>
</div>
<button type="submit" class="btn btn-danger btn-sm"
onclick="return document.querySelectorAll('.bulk-check:checked').length > 0 && confirm('Supprimer les articles sélectionnés ? Cette action est irréversible.')">
Supprimer la sélection
</button>
</div>
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th style="width:2rem"></th>
<th>Titre</th>
<?php if (isAdmin()): ?><th>Auteur</th><?php endif; ?>
<th>Catégorie</th>
<th>Statut</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['articles'] as $a): ?>
<tr>
<td>
<input class="form-check-input bulk-check" type="checkbox"
name="uuids[]" value="<?= htmlspecialchars($a['uuid']) ?>">
</td>
<td>
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
<?= htmlspecialchars($a['title']) ?>
</a>
</td>
<?php if (isAdmin()): ?>
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '') ?></td>
<?php endif; ?>
<td class="text-muted small"><?= htmlspecialchars($a['category'] ?? '') ?></td>
<td><?= adminStatusBadge($a, $now) ?></td>
<td class="text-muted small text-nowrap">
<?= htmlspecialchars(date('d/m/Y', strtotime((string)($a['published_at'] ?? $a['created_at'] ?? '')))) ?>
</td>
<td class="text-end text-nowrap">
<a href="/edit/<?= htmlspecialchars($a['uuid']) ?>"
class="btn btn-outline-secondary btn-sm">Modifier</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</form>
<script src="/assets/js/admin.js" defer></script>
<?php endif; ?>
<!-- ─────────────────────────── 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>
<div class="card-body">
<form method="post" action="/?action=admin_grant_role" class="row g-2 align-items-end">
<div class="col-md-5">
<label class="form-label small">Email</label>
<input type="email" name="email" class="form-control form-control-sm"
placeholder="utilisateur@exemple.fr" required>
</div>
<div class="col-md-4">
<label class="form-label small">Rôle</label>
<select name="role" class="form-select form-select-sm" required>
<?php foreach ($adminData['roles'] as $r): ?>
<option value="<?= htmlspecialchars($r['name']) ?>">
<?= htmlspecialchars($r['label']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary btn-sm w-100">Attribuer</button>
</div>
</form>
</div>
</div>
<!-- Liste des utilisateurs -->
<?php if (empty($adminData['users'])): ?>
<p class="text-muted">Aucun utilisateur.</p>
<?php else: ?>
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th>Email</th>
<th>Statut</th>
<th>Rôles</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['users'] as $u): ?>
<tr>
<td class="small"><?= htmlspecialchars($u['email']) ?></td>
<td>
<?php if ($u['is_active'] === null): ?>
<span class="badge bg-light text-muted">Pré-inscrit</span>
<?php elseif ($u['is_active']): ?>
<span class="badge bg-success">Actif</span>
<?php else: ?>
<span class="badge bg-danger">Inactif</span>
<?php endif; ?>
</td>
<td>
<?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($role['label']) ?>» à <?= htmlspecialchars($u['email']) ?> ?">
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
<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; ?>
<?php if (empty($u['roles'])): ?>
<span class="text-muted small"></span>
<?php endif; ?>
</td>
<td class="text-end">
<!-- Ajout rapide d'un rôle existant -->
<?php
$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">
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
<select name="role" class="form-select form-select-sm" style="width:auto">
<?php foreach ($missing as $r): ?>
<option value="<?= htmlspecialchars($r['name']) ?>">
<?= htmlspecialchars($r['label']) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-outline-primary btn-sm">+</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<!-- ─────────────────────────── RÔLES ─────────────────────────── -->
<?php elseif ($tab === 'roles' && isAdmin()): ?>
<div class="row g-4">
<!-- 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 elseif ($tab === 'site' && isAdmin()): ?>
<?php if (!empty($siteSettingsSaved)): ?>
<div class="alert alert-success py-2 mb-3">Paramètres enregistrés.</div>
<?php endif; ?>
<div class="card" style="max-width:540px">
<div class="card-header">Paramètres du site</div>
<div class="card-body">
<form method="post" action="/?action=admin_save_site">
<div class="mb-3">
<label for="site-title" class="form-label small fw-semibold">Titre du site</label>
<input type="text" id="site-title" name="site_title"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteTitle()) ?>"
required maxlength="80">
<div class="form-text">Affiché dans la barre de navigation et les onglets.</div>
</div>
<div class="mb-3">
<label for="site-claim" class="form-label small fw-semibold">Claim / accroche</label>
<input type="text" id="site-claim" name="site_claim"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteClaim()) ?>"
required maxlength="200">
<div class="form-text">Affiché sous le titre dans la navbar et dans le pied de page.</div>
</div>
<div class="mb-3">
<label for="site-lang" class="form-label small fw-semibold">Langue du site</label>
<input type="text" id="site-lang" name="site_lang"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteLang()) ?>"
maxlength="20" placeholder="ex : fr-FR">
<div class="form-text">Format BCP 47 (ex&nbsp;: <code>fr</code>, <code>fr-FR</code>). Utilisé dans <code>&lt;html lang&gt;</code>, og:locale, RSS et JSON-LD.</div>
</div>
<div class="mb-3">
<label for="posts-per-page" class="form-label small fw-semibold">Articles par page</label>
<input type="number" id="posts-per-page" name="posts_per_page"
class="form-control form-control-sm"
value="<?= postsPerPage() ?>"
min="1" max="100">
</div>
<div class="mb-3">
<label for="site-license-label" class="form-label small fw-semibold">Licence (libellé)</label>
<input type="text" id="site-license-label" name="site_license_label"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteLicenseLabel()) ?>"
maxlength="80" placeholder="ex : CC BY 4.0">
</div>
<div class="mb-3">
<label for="site-license-url" class="form-label small fw-semibold">Licence (URL)</label>
<input type="url" id="site-license-url" name="site_license_url"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteLicenseUrl()) ?>"
maxlength="200">
<div class="form-text">Affiché dans le footer.</div>
</div>
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
</form>
</div>
</div>
<?php endif; ?>
<!-- ─────────────────────────── COMMENTAIRES ──────────────────────── -->
<?php if ($tab === 'comments' && isAdmin()): ?>
<h5 class="mb-3">Commentaires</h5>
<?php if (empty($adminData['comments'])): ?>
<p class="text-muted">Aucun commentaire pour l'instant.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th>Article</th>
<th>Auteur</th>
<th>Commentaire</th>
<th>Date</th>
<th>État</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['comments'] as $c): ?>
<?php
$cSlug = $adminData['articleSlugs'][$c['article_uuid']] ?? null;
$cDate = date('d/m/Y H:i', strtotime((string)$c['created_at']));
$cVerif = $c['verified'] ? '<span class="badge bg-success">Vérifié</span>' : '<span class="badge bg-warning text-dark">En attente</span>';
$cPub = $c['published'] ? '<span class="badge bg-primary">Publié</span>' : '<span class="badge bg-secondary">Masqué</span>';
?>
<tr>
<td class="small">
<?php if ($cSlug): ?>
<a href="/post/<?= rawurlencode($cSlug) ?>#comments"><?= htmlspecialchars($cSlug) ?></a>
<?php else: ?>
<span class="text-muted"><?= htmlspecialchars(substr($c['article_uuid'], 0, 8)) ?>…</span>
<?php endif; ?>
</td>
<td class="small">
<div><?= htmlspecialchars($c['author_name']) ?></div>
<div class="text-muted"><?= htmlspecialchars($c['author_email']) ?></div>
</td>
<td class="small" style="max-width:30ch">
<span title="<?= htmlspecialchars($c['content']) ?>">
<?= htmlspecialchars(mb_strimwidth($c['content'], 0, 80, '…')) ?>
</span>
<?php if (!empty($c['verification_code'])): ?>
<br><span class="text-muted">Code : <?= htmlspecialchars($c['verification_code']) ?></span>
<?php endif; ?>
</td>
<td class="text-muted small text-nowrap"><?= htmlspecialchars($cDate) ?></td>
<td><?= $cVerif ?> <?= $cPub ?></td>
<td>
<?php if ($c['verified'] && $c['published']): ?>
<form method="post" action="/comment-moderate" class="d-inline">
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
<input type="hidden" name="pub" value="0">
<button type="submit" class="btn btn-sm btn-outline-danger">Masquer</button>
</form>
<?php elseif ($c['verified'] && !$c['published']): ?>
<form method="post" action="/comment-moderate" class="d-inline">
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
<input type="hidden" name="pub" value="1">
<button type="submit" class="btn btn-sm btn-outline-success">Publier</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
<?php
$content = ob_get_clean();
$title = 'Administration — ' . siteTitle();
include __DIR__ . '/layout.php';