Add Paramètres page: consolidate maintenance + account settings

This commit is contained in:
Pontoporeia
2026-04-08 15:06:51 +02:00
parent ba135f0cb5
commit 603af07b68
8 changed files with 262 additions and 35 deletions

37
SPECS.md Normal file
View File

@@ -0,0 +1,37 @@
- l'ordre des TFE sur la page d'accueil ; est-ce que ce serait possible de les faire dérouler par année avec les plus récents tout en haut , mais en rendant l'ordre de chaque année aléatoire
- On a les pdf et notes d'intention des dernières années, est-ce que vous voulez déjà y avoir accès ? Est-ce qu'il y a une nomenclature particulière qui vous fait plaisir ?
## admin
- Il a été décidé de pour linstant ne pas rendre les TFE visibles vers lextérieur.
- option douverture “interne” qui sera à priori le défaut appliqué sur une majorité des TFE
pas disponnible:
- le pdf ainsi que la note dintention ne soient que disponibles quand les personnes se trouvent physiquement à lerg (via adresse IP) ou via login (à voir ce qui est le plus simple à intégrer techniquement).
## le formulaire de dépôt
- On demandera aux étudiant·es de préparer une image au bon format pour le dépôt du TFE. Est-ce que vous pouvez nous donner la taille qu'il faudrait ?
- la page de formulaire:celle que les étudiant·es doivent remplir lors du dépôt du TFE, ajouter:
- l'explication;
- le contexte des différents choix soient visibles.
- Quand un·é étudiant·e dépose son TFE, il ne doit pas être publié directement. Il doit arriver dans la base de donnée, et quelqu'un viendrait juste clicker sur “publier” dans le backoffice une fois la défense orale terminée (et en fonction du retour du jury).
- loption “libre” ne doit donc pas encore exister cette année.
- créer un système de toggle pour quelles sont les options actives dans le formulaire
- Il ny a pour linstant que loption “interdit” et “interne”. Loption “libre” ne sera activée que à partir de lannée académique prochaine.
- la case “contact” soit accompagnée dune case à cocher/décocher ; « Je veux que mon contact soit accessible à toustes depuis la plateforme xamxam ». En fonction de cette réponse, le contact apparaîtrait ou non sur la page du TFE.
## la base de donnée
- rajouter une catégorie “objet” pour que, dans un futur éventuel, on puisse différencier les TFE des FRART et des thèses. Pour linstant cest juste un tag qui doit apparaître en back-office.
- quelle(s) fonte(s) est-ce que vous utilisez sur le site ?
- est-ce que vous pouvez menvoyez un export de la maquette du site ? (en .jpg cest ok, cest juste pour rafraîchir nos mémoires afin qu'on puisse produire les textes en adéquation avec ce qui existe)
- les questions du dernier mail :)

View File

@@ -1,6 +1,12 @@
# TODO
## Done
- [x] Create Paramètres page consolidating maintenance toggle and account settings into two sections
- [x] New `public/admin/parametres.php` with Maintenance + Compte administrateur sections
- [x] Nav updated: “Compte” replaced by “Paramètres” linking to `parametres.php`
- [x] Maintenance bar removed from `index.php`
- [x] `actions/maintenance.php` and `actions/account.php` redirect to `parametres.php` via POST `redirect` param
- [x] CSS added for `.admin-settings-section` and `.admin-maintenance-status`
- [x] Fix nav logo: revert to "Xamxam", apply display font (Combined), step-2 size, letter-spacing, text-shadow via .nav-logo class
- [x] Bump all font sizes ~10% across all CSS files (admin, system, search, main, apropos, common, tfe)
- [x] Migrate to utopia fluid type scale (--step--2 → --step-5) and space scale (--space-3xs → --space-3xl) across all CSS files

View File

@@ -23,15 +23,18 @@ $action = $_POST['action'] ?? 'change_password';
// ── Remove credentials ────────────────────────────────────────────────────────
if ($action === 'remove_credentials') {
$backUrl = $_POST['redirect'] ?? '/admin/parametres.php';
if (!preg_match('#^/admin/#', $backUrl)) { $backUrl = '/admin/parametres.php'; }
if (!$hasPassword) {
App::flash('error', 'Aucun fichier de mot de passe à supprimer.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;
}
if (!is_writable($credentialsFile) && !is_writable(dirname($credentialsFile))) {
App::flash('error', 'Le fichier de configuration n\'est pas accessible en écriture.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;
}
@@ -42,7 +45,7 @@ if ($action === 'remove_credentials') {
exit;
} else {
App::flash('error', 'Impossible de supprimer le fichier de configuration.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;
}
}
@@ -50,11 +53,14 @@ if ($action === 'remove_credentials') {
// ── Change / set password ─────────────────────────────────────────────────────
// 1. If a password is already set, verify the current one.
$backUrl = $_POST['redirect'] ?? '/admin/parametres.php';
if (!preg_match('#^/admin/#', $backUrl)) { $backUrl = '/admin/parametres.php'; }
if ($hasPassword) {
$currentPassword = $_POST['current_password'] ?? '';
if (!AdminAuth::login($currentPassword)) {
App::flash('error', 'Mot de passe actuel incorrect.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;
}
}
@@ -65,13 +71,13 @@ $confirmPassword = $_POST['confirm_password'] ?? '';
if (strlen($newPassword) < 12) {
App::flash('error', 'Le nouveau mot de passe doit contenir au moins 12 caractères.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;
}
if ($newPassword !== $confirmPassword) {
App::flash('error', 'Les mots de passe ne correspondent pas.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;
}
@@ -79,7 +85,7 @@ if ($newPassword !== $confirmPassword) {
$hash = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 12]);
if ($hash === false) {
App::flash('error', 'Erreur lors du hachage du mot de passe.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;
}
@@ -100,13 +106,13 @@ $tmpFile = $credentialsFile . '.tmp.' . bin2hex(random_bytes(6));
if (file_put_contents($tmpFile, $configContent, LOCK_EX) === false) {
@unlink($tmpFile);
App::flash('error', 'Impossible d\'écrire le fichier de configuration. Vérifiez les permissions sur config/.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;
}
if (!rename($tmpFile, $credentialsFile)) {
@unlink($tmpFile);
App::flash('error', 'Impossible de mettre à jour le fichier de configuration.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;
}
@@ -118,5 +124,5 @@ App::flash('success', $hasPassword
? 'Mot de passe mis à jour avec succès.'
: 'Mot de passe défini avec succès. L\'authentification PHP est maintenant active.');
header('Location: /admin/account.php');
header('Location: ' . $backUrl);
exit;

View File

@@ -24,5 +24,10 @@ if ($action === 'enable_maintenance') {
App::flash('error', "Action inconnue.");
}
header('Location: /admin/');
$redirect = isset($_POST['redirect']) ? $_POST['redirect'] : '/admin/';
// Allow only internal admin redirects for safety
if (!preg_match('#^/admin/#', $redirect)) {
$redirect = '/admin/';
}
header('Location: ' . $redirect);
exit();

View File

@@ -76,29 +76,6 @@ document.addEventListener('DOMContentLoaded', () => {
<?php include APP_ROOT . '/templates/partials/flash-messages.php'; ?>
<!-- Maintenance mode toggle -->
<?php $maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag'); ?>
<aside role="status" class="admin-maintenance-bar <?= $maintenanceOn ? 'admin-maintenance-bar--active' : '' ?>" aria-label="Statut du site">
<?php if ($maintenanceOn): ?>
<span>⚠ Mode maintenance <strong>activé</strong> — le site public est inaccessible.</span>
<form method="post" action="actions/maintenance.php">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="disable_maintenance">
<button type="submit" class="admin-btn admin-btn--sm">Désactiver la maintenance</button>
</form>
<?php else: ?>
<span>Site public : <strong>en ligne</strong></span>
<form method="post" action="actions/maintenance.php">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="enable_maintenance">
<button type="submit" class="admin-btn admin-btn--sm admin-btn--warning"
onclick="return confirm('Mettre le site en maintenance ? Les visiteurs verront une page 503.')">
Activer la maintenance
</button>
</form>
<?php endif; ?>
</aside>
<!-- Stats (always reflects full DB, independent of active filters) -->
<dl class="admin-stats">
<div class="admin-stat">

151
public/admin/parametres.php Normal file
View File

@@ -0,0 +1,151 @@
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
$pageTitle = "Paramètres";
$credentialsFile = APP_ROOT . '/config/admin_credentials.php';
$hasPassword = defined('ADMIN_PASSWORD_HASH');
$maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag');
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<main id="main-content">
<h1>Paramètres</h1>
<?php include APP_ROOT . '/templates/partials/flash-messages.php'; ?>
<!-- ══════════════════════════════════════════════════════════════
SECTION 1 — Maintenance
══════════════════════════════════════════════════════════════ -->
<section class="admin-settings-section" aria-labelledby="settings-maintenance-title">
<h2 class="admin-settings-section__title" id="settings-maintenance-title">Maintenance</h2>
<div class="admin-maintenance-status <?= $maintenanceOn ? 'admin-maintenance-status--active' : '' ?>">
<?php if ($maintenanceOn): ?>
<p class="admin-maintenance-status__msg">
<strong>⚠ Mode maintenance activé</strong> — le site public est inaccessible.
</p>
<form method="post" action="actions/maintenance.php">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="disable_maintenance">
<input type="hidden" name="redirect" value="/admin/parametres.php">
<button type="submit" class="admin-btn admin-btn--sm">Désactiver la maintenance</button>
</form>
<?php else: ?>
<p class="admin-maintenance-status__msg">
Site public : <strong>en ligne</strong>
</p>
<form method="post" action="actions/maintenance.php">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="enable_maintenance">
<input type="hidden" name="redirect" value="/admin/parametres.php">
<button type="submit" class="admin-btn admin-btn--sm admin-btn--warning"
onclick="return confirm('Mettre le site en maintenance ? Les visiteurs verront une page 503.')">
Activer la maintenance
</button>
</form>
<?php endif; ?>
</div>
</section>
<!-- ══════════════════════════════════════════════════════════════
SECTION 2 — Compte administrateur
══════════════════════════════════════════════════════════════ -->
<section class="admin-settings-section" aria-labelledby="settings-account-title">
<h2 class="admin-settings-section__title" id="settings-account-title">Compte administrateur</h2>
<!-- Status info -->
<dl class="admin-account-status">
<div class="admin-account-status__row">
<dt class="admin-account-status__label">Authentification PHP</dt>
<dd><?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Active'; $badgeWarnLabel = 'Non configurée'; include APP_ROOT . '/templates/partials/status-badge.php'; ?></dd>
</div>
<div class="admin-account-status__row">
<dt class="admin-account-status__label">Fichier de configuration</dt>
<dd>
<code class="admin-account-status__code">config/admin_credentials.php</code>
<?php $badgeType = 'ok'; $badgeValue = file_exists($credentialsFile); $badgeOkLabel = 'Présent'; $badgeWarnLabel = 'Absent'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
</dd>
</div>
<?php if (!$hasPassword): ?>
<p class="admin-account-status__note">
Aucun mot de passe PHP configuré. Le formulaire ci-dessous créera
<code>config/admin_credentials.php</code> avec un hash bcrypt.
</p>
<?php endif; ?>
</dl>
<!-- Password change form -->
<h3 class="admin-section-title"><?= $hasPassword ? 'Changer le mot de passe' : 'Définir le mot de passe' ?></h3>
<form method="post" action="/admin/actions/account.php" class="admin-form" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="redirect" value="/admin/parametres.php">
<?php if ($hasPassword): ?>
<div>
<label for="current_password">Mot de passe actuel</label>
<div>
<input type="password" id="current_password"
name="current_password" required autocomplete="current-password">
</div>
</div>
<?php endif; ?>
<div>
<label for="new_password">Nouveau mot de passe</label>
<div>
<input type="password" id="new_password"
name="new_password" required autocomplete="new-password"
minlength="12">
<small>Minimum 12 caractères.</small>
</div>
</div>
<div>
<label for="confirm_password">Confirmer le mot de passe</label>
<div>
<input type="password" id="confirm_password"
name="confirm_password" required autocomplete="new-password">
</div>
</div>
<div class="admin-form-footer">
<button type="submit" class="admin-btn">
<?= $hasPassword ? 'Mettre à jour le mot de passe' : 'Définir le mot de passe' ?>
</button>
</div>
</form>
<?php if ($hasPassword): ?>
<!-- Danger zone: remove password -->
<h3 class="admin-section-title admin-section-title--danger">Zone de danger</h3>
<div class="admin-danger-zone">
<p class="admin-danger-zone__description">
<strong>Supprimer la configuration du mot de passe PHP</strong><br>
<small>
Supprime <code>config/admin_credentials.php</code>. L'accès admin
dépendra uniquement de l'authentification nginx Basic Auth si elle est configurée.
</small>
</p>
<form method="post" action="/admin/actions/account.php"
onsubmit="return confirm('Supprimer le fichier de mot de passe PHP ? L\'accès admin ne sera protégé que par nginx Basic Auth.')">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="remove_credentials">
<input type="hidden" name="redirect" value="/admin/parametres.php">
<input type="hidden" name="current_password_remove" id="current_password_remove" value="">
<button type="submit" class="admin-btn admin-btn--danger">Supprimer le fichier</button>
</form>
</div>
<?php endif; ?>
</section>
</main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>

View File

@@ -905,6 +905,51 @@
margin-top: var(--space-2xs);
}
/* ── Settings page sections ─────────────────────────────────────────────── */
.admin-settings-section {
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: var(--space-m) var(--space-l);
margin-bottom: var(--space-l);
}
.admin-settings-section__title {
font-size: var(--step-0);
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--text-secondary);
margin: 0 0 var(--space-m);
padding-bottom: var(--space-2xs);
border-bottom: 1px solid var(--border-primary);
}
.admin-maintenance-status {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-s);
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: var(--space-xs) var(--space-m);
font-size: var(--step--1);
}
.admin-maintenance-status--active {
background: var(--warning-muted-bg);
border-color: var(--warning);
}
.admin-maintenance-status__msg {
margin: 0;
}
.admin-maintenance-status form {
display: inline;
flex-shrink: 0;
}
/* ── Cancel link ────────────────────────────────────────────────────────── */
.admin-cancel-link {
font-size: var(--step--1);

View File

@@ -22,7 +22,7 @@ $_thesisId = $_GET['id'] ?? null;
<li><a href="/admin/pages.php" <?= in_array($_currentPage, ['pages.php', 'pages-edit.php']) ? 'aria-current="page"' : '' ?>>Pages statiques</a></li>
<li><a href="/admin/tags.php" <?= $_currentPage === 'tags.php' ? 'aria-current="page"' : '' ?>>Mots-clés</a></li>
<li><a href="/admin/system.php" <?= in_array($_currentPage, ['system.php', 'status.php', 'logs.php']) ? 'aria-current="page"' : '' ?>>Système</a></li>
<li><a href="/admin/account.php" <?= $_currentPage === 'account.php' ? 'aria-current="page"' : '' ?>>Compte</a></li>
<li><a href="/admin/parametres.php" <?= $_currentPage === 'parametres.php' ? 'aria-current="page"' : '' ?>>Paramètres</a></li>
<?php if ($_thesisId && in_array($_currentPage, ['edit.php', 'thanks.php'])): ?>
<li><a href="/admin/edit.php?id=<?= intval($_thesisId) ?>" <?= $_currentPage === 'edit.php' ? 'aria-current="page"' : '' ?>>Modifier</a></li>
<?php endif; ?>