Files
xamxam/app/templates/admin/parametres.php

507 lines
27 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
<main id="main-content" class="admin-main--toc">
<?php include APP_ROOT . '/templates/admin/partials/admin-toc.php'; ?>
<article>
<h1>Paramètres</h1>
<!-- ══════════════════════════════════════════════════════════════
MAINTENANCE
══════════════════════════════════════════════════════════════ -->
<section aria-labelledby="settings-maintenance-title">
<h2 id="settings-maintenance-title">Maintenance</h2>
<table class="param-access-table">
<caption>Visibilité des pages selon le mode</caption>
<thead>
<tr>
<th scope="col">Page</th>
<th scope="col">Normal</th>
<th scope="col">Maintenance</th>
</tr>
</thead>
<tbody>
<tr>
<td>Accueil, recherche, répertoire, TFE, à propos, licence</td>
<td class="param-access-yes">✓ Visible</td>
<td class="param-access-no">✗ 503</td>
</tr>
<tr>
<td>Formulaire étudiant (partage)</td>
<td class="param-access-yes">✓ Visible</td>
<td class="param-access-yes">✓ Visible</td>
</tr>
<tr>
<td>Administration</td>
<td class="param-access-yes">✓ Visible</td>
<td class="param-access-yes">✓ Visible</td>
</tr>
</tbody>
</table>
<div class="param-maintenance-row">
<?php if ($maintenanceOn): ?>
<p>
<strong>⚠ Mode maintenance activé</strong> — le site public est inaccessible.
</p>
<p class="param-note">
Le formulaire étudiant (partage) et l'administration restent accessibles.
</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="btn btn--secondary">Désactiver la maintenance</button>
</form>
<?php else: ?>
<p>Site public : <strong>en ligne</strong></p>
<form method="post" action="actions/maintenance.php" id="enable-maintenance-form">
<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="button" class="btn btn--warning"
onclick="document.getElementById('enable-maintenance-dialog').showModal()">
Activer la maintenance
</button>
</form>
<?php endif; ?>
</div>
<h3 style="margin-top:var(--space-l);margin-bottom:var(--space-xs);font-size:var(--step-0)">Export de la base de données</h3>
<p style="margin-bottom:var(--space-s)">
Télécharger une copie complète de la base de données SQLite
(<code>xamxam.db</code>) pour sauvegarde manuelle.
</p>
<form method="get" action="/admin/actions/export.php" class="param-form">
<input type="hidden" name="db" value="1">
<button type="submit" class="btn btn--primary">Télécharger la base de données</button>
</form>
</section>
<!-- ══════════════════════════════════════════════════════════════
RELAY SMTP
══════════════════════════════════════════════════════════════ -->
<section aria-labelledby="settings-smtp-title">
<h2 id="settings-smtp-title">Emails</h2>
<p>
Configuration du serveur SMTP pour l'envoi d'e-mails
(notifications, partage de TFE, etc.).
</p>
<div class="param-smtp-status">
<?php if ($smtpConfigured): ?>
<span class="param-badge-ok">✓ Configuré</span>
<span><?= htmlspecialchars($smtpSettings['host']) ?>:<?= (int)$smtpSettings['port'] ?> (<?= htmlspecialchars($smtpSettings['encryption']) ?>)</span>
<?php else: ?>
<span class="param-badge-warn">✗ Non configuré</span>
<?php endif; ?>
</div>
<?php
// Inline helper: emit aria-invalid + error <small> when this field is the culprit
$smtpFieldErr = function(string $id) use ($smtpErrorField): string {
return $smtpErrorField === $id ? ' aria-invalid="true"' : '';
};
$smtpFieldMsg = function(string $id, string $msg) use ($smtpErrorField): string {
return $smtpErrorField === $id
? '<small class="param-field-error" id="' . $id . '-error">' . htmlspecialchars($msg) . '</small>'
: '';
};
// Human-readable hints per field (brief — the full message is in the toast)
$smtpHints = [
'smtp_host' => 'Vérifiez ladresse du serveur SMTP.',
'smtp_port' => 'Vérifiez le numéro de port.',
'smtp_encryption' => 'Vérifiez le mode de chiffrement.',
'smtp_username' => 'Vérifiez le nom dutilisateur.',
'smtp_password' => 'Mot de passe incorrect.',
];
?>
<form method="post" action="actions/settings.php" class="param-form"
<?= $smtpErrorField ? 'data-smtp-error-field="' . htmlspecialchars($smtpErrorField) . '"' : '' ?>>
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="section" value="smtp">
<fieldset class="param-grid">
<legend>Paramètres email</legend>
<div>
<label for="smtp_host">Hôte SMTP</label>
<input type="text" id="smtp_host" name="smtp_host"
value="<?= htmlspecialchars($smtpSettings['host']) ?>"
placeholder="smtp.example.com"
<?= $smtpFieldErr('smtp_host') ?>
<?= $smtpErrorField === 'smtp_host' ? 'aria-describedby="smtp_host-error"' : '' ?>>
<?= $smtpFieldMsg('smtp_host', $smtpHints['smtp_host']) ?>
</div>
<div>
<label for="smtp_port">Port</label>
<input type="number" id="smtp_port" name="smtp_port"
value="<?= (int)$smtpSettings['port'] ?>"
min="1" max="65535"
<?= $smtpFieldErr('smtp_port') ?>
<?= $smtpErrorField === 'smtp_port' ? 'aria-describedby="smtp_port-error"' : '' ?>>
<?= $smtpFieldMsg('smtp_port', $smtpHints['smtp_port']) ?>
</div>
<div>
<label for="smtp_encryption">Chiffrement</label>
<select id="smtp_encryption" name="smtp_encryption"
<?= $smtpFieldErr('smtp_encryption') ?>
<?= $smtpErrorField === 'smtp_encryption' ? 'aria-describedby="smtp_encryption-error"' : '' ?>>
<option value="tls" <?= $smtpSettings['encryption'] === 'tls' ? 'selected' : '' ?>>TLS (STARTTLS)</option>
<option value="ssl" <?= $smtpSettings['encryption'] === 'ssl' ? 'selected' : '' ?>>SSL (SMTPS)</option>
<option value="none" <?= $smtpSettings['encryption'] === 'none' ? 'selected' : '' ?>>Aucun</option>
</select>
<?= $smtpFieldMsg('smtp_encryption', $smtpHints['smtp_encryption']) ?>
</div>
<div>
<label for="smtp_username">Adresse e-mail</label>
<input type="email" id="smtp_username" name="smtp_username"
value="<?= htmlspecialchars($smtpSettings['username']) ?>"
placeholder="xamxam@erg.be"
<?= $smtpFieldErr('smtp_username') ?>
<?= $smtpErrorField === 'smtp_username' ? 'aria-describedby="smtp_username-error"' : '' ?>>
<small>Adresse utilisée pour l'authentification SMTP et comme expéditeur.</small>
<?= $smtpFieldMsg('smtp_username', $smtpHints['smtp_username']) ?>
</div>
<div>
<label for="smtp_password">Mot de passe</label>
<input type="password" id="smtp_password" name="smtp_password"
value=""
autocomplete="new-password"
placeholder="Laissez vide pour ne pas modifier"
<?= $smtpFieldErr('smtp_password') ?>
<?= $smtpErrorField === 'smtp_password' ? 'aria-describedby="smtp_password-error"' : '' ?>>
<?= $smtpFieldMsg('smtp_password', $smtpHints['smtp_password']) ?>
</div>
<div>
<label for="smtp_from_name">Nom d'expéditeur</label>
<input type="text" id="smtp_from_name" name="smtp_from_name"
value="<?= htmlspecialchars($smtpSettings['from_name']) ?>">
</div>
<div>
<label for="smtp_notify_email">Adresse de notification admin</label>
<input type="email" id="smtp_notify_email" name="smtp_notify_email"
value="<?= htmlspecialchars($smtpSettings['notify_email'] ?? '') ?>"
placeholder="admin@example.com">
<small>Reçoit les notifications (demandes d'accès, etc.). Si vide, utilise l'adresse d'expédition.</small>
</div>
</fieldset>
<button type="submit" class="btn btn--primary">Enregistrer</button>
</form>
<!-- Test d'envoi -->
<fieldset class="param-smtp-test">
<legend>Tester l'envoi d'un e-mail</legend>
<p>Envoie un e-mail de test via le relay SMTP configuré ci-dessus.</p>
<?php if (!$smtpConfigured): ?>
<p class="param-note">⚠ Configurez le relay SMTP avant de pouvoir tester l'envoi.</p>
<?php else: ?>
<form method="post" action="actions/smtp-test.php" class="param-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div class="param-smtp-test-row">
<div>
<label for="smtp_test_email">Adresse de destination</label>
<input type="email" id="smtp_test_email" name="test_email"
placeholder="test@example.com" required>
</div>
<button type="submit" class="btn btn--primary">Envoyer le test</button>
</div>
</form>
<?php endif; ?>
</fieldset>
</section>
<!-- ══════════════════════════════════════════════════════════════
PEERTUBE
══════════════════════════════════════════════════════════════ -->
<section aria-labelledby="settings-peertube-title">
<h2 id="settings-peertube-title">PeerTube</h2>
<p>
Intégration avec une instance PeerTube pour l'hébergement des vidéos et fichiers audio.
Les fichiers sont uploadés via l'API PeerTube et intégrés comme lecteurs embarqués sur la page du TFE.
</p>
<div class="param-smtp-status">
<?php if ($peerTubeConfigured): ?>
<span class="param-badge-ok">✓ Configuré</span>
<span><?= htmlspecialchars($peerTubeSettings['instance_url']) ?></span>
<?php else: ?>
<span class="param-badge-warn">✗ Non configuré</span>
<?php endif; ?>
</div>
<form method="post" action="actions/settings.php" class="param-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="section" value="peertube">
<fieldset>
<legend>Activation</legend>
<label class="param-checkbox">
<input type="checkbox" name="peertube_upload_enabled" value="1"
<?= $peerTubeEnabled ? 'checked' : '' ?>>
<span>
<strong>Activer l'upload PeerTube</strong><br>
<small>Lorsqu'activé, les fichiers vidéo et audio soumis via le formulaire sont uploadés
sur l'instance PeerTube plutôt que stockés localement sur le serveur.</small>
</span>
</label>
</fieldset>
<fieldset class="param-grid">
<legend>Paramètres Peertube</legend>
<p class="param-note">
L'authentification PeerTube utilise les mêmes identifiants que le
<strong>relay SMTP</strong> configuré ci-dessus.
</p>
<div>
<label for="peertube_instance_url">URL de l'instance PeerTube</label>
<input type="url" id="peertube_instance_url" name="peertube_instance_url"
value="<?= htmlspecialchars($peerTubeSettings['instance_url']) ?>"
placeholder="https://peertube.example.com">
<small>Sans slash final. Ex : <code>https://peertube.erg.be</code></small>
</div>
<div>
<label for="peertube_channel_name">Nom de la chaîne</label>
<input type="text" id="peertube_channel_name" name="peertube_channel_name"
value="<?= htmlspecialchars($peerTubeSettings['channel_name'] ?? '') ?>"
placeholder="xamxam_erg.be_channel@videos.erg.be">
<small>Identifiant complet de la chaîne (handle), ex : <code>nom_de_chaîne@hôte</code>.</small>
</div>
<div>
<label for="peertube_privacy">Visibilité des vidéos</label>
<select id="peertube_privacy" name="peertube_privacy">
<option value="1" <?= (int)$peerTubeSettings['privacy'] === 1 ? 'selected' : '' ?>>Publique</option>
<option value="2" <?= (int)$peerTubeSettings['privacy'] === 2 ? 'selected' : '' ?>>Non listée</option>
<option value="3" <?= (int)$peerTubeSettings['privacy'] === 3 ? 'selected' : '' ?>>Privée</option>
</select>
</div>
</fieldset>
<button type="submit" class="btn btn--primary">Enregistrer</button>
<button type="button" class="btn btn--secondary" id="peertube-test-btn"
hx-post="/admin/actions/peertube-test.php"
hx-target="#peertube-test-result"
hx-include="closest form"
hx-indicator="#peertube-test-spinner">
Tester la connexion
</button>
<span id="peertube-test-spinner" class="htmx-indicator" style="display:none;margin-left:var(--space-xs);">⏳</span>
<div id="peertube-test-result" style="margin-top:var(--space-xs);"></div>
</form>
</section>
<!-- ══════════════════════════════════════════════════════════════
COMPTE ADMINISTRATEUR
══════════════════════════════════════════════════════════════ -->
<section aria-labelledby="settings-account-title">
<h2 id="settings-account-title">Compte administrateur</h2>
<dl class="param-account-status">
<div>
<dt>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>
<dt>Stockage du hash</dt>
<dd>
<code>site_settings (DB)</code>
<?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Présent'; $badgeWarnLabel = 'Absent'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
</dd>
</div>
</dl>
<?php if (!$hasPassword): ?>
<p class="param-note">
Aucun mot de passe PHP configuré. Le formulaire ci-dessous stockera
un hash bcrypt dans la base de données.
</p>
<?php endif; ?>
<form method="post" action="/admin/actions/account.php" class="param-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>
<input type="password" id="current_password"
name="current_password" required autocomplete="current-password">
</div>
<?php endif; ?>
<div>
<label for="new_password">Nouveau mot de passe</label>
<input type="password" id="new_password"
name="new_password" required autocomplete="new-password"
minlength="12">
<small>Minimum 12 caractères.</small>
</div>
<div>
<label for="confirm_password">Confirmer le mot de passe</label>
<input type="password" id="confirm_password"
name="confirm_password" required autocomplete="new-password">
</div>
<button type="submit" class="btn btn--primary">
<?= $hasPassword ? 'Mettre à jour le mot de passe' : 'Définir le mot de passe' ?>
</button>
</form>
<!-- Danger zone: remove credentials -->
<?php if ($hasPassword): ?>
<fieldset class="param-danger-zone">
<legend>Supprimer la configuration du mot de passe</legend>
<p>
Supprime le hash de la base de données. L'accès admin
ne sera plus protégé (mode développement).
</p>
<?php /* TODO: replace this browser confirm() with a proper <dialog> modal like the other confirmations */ ?>
<form method="post" action="/admin/actions/account.php"
onsubmit="return confirm('Supprimer le mot de passe ? L\'accès admin ne sera plus protégé.')">
<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" value="">
<button type="submit" class="btn btn--danger">Supprimer le mot de passe</button>
</form>
</fieldset>
<?php endif; ?>
</section>
<!-- ══════════════════════════════════════════════════════════════
SYSTÈME
══════════════════════════════════════════════════════════════ -->
<section aria-labelledby="settings-system-title">
<h2 id="settings-system-title">Système</h2>
<p class="sys-refresh-note">
Affiché le <?= date('d/m/Y à H:i:s') ?> —
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&amp;n=<?= $selectedN ?>">Rafraîchir</a> —
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&amp;n=<?= $selectedN ?>&amp;refresh=1">Forcer actualisation</a>
</p>
<div class="sys-status-header">
<h3 class="srv-section-title srv-section-title--compact">Statut
<?php if ($statusCached && $statusCacheAge !== null): ?>
<span class="sys-cache-badge sys-cache-badge--hit" title="Données en cache">
⚡ Cache — il y a <?= $statusCacheAge ?>s
</span>
<?php else: ?>
<span class="sys-cache-badge sys-cache-badge--miss" title="Données fraîches">
⟳ Actualisé
</span>
<?php endif; ?>
</h3>
<button id="sys-status-toggle" class="sys-status-toggle"
aria-expanded="<?= $statusInitiallyCollapsed ? 'false' : 'true' ?>" aria-controls="sys-status-body"
type="button"
onclick="var b=document.getElementById('sys-status-body');var c=b.hidden;b.hidden=!c;this.setAttribute('aria-expanded',c);this.textContent=c?'▲ Réduire':'▼ Développer';document.cookie='sys_collapsed='+(!c)+';path=/;max-age=31536000';return false">
<?= $statusInitiallyCollapsed ? '▼ Développer' : '▲ Réduire' ?>
</button>
</div>
<div id="sys-status-body"<?= $statusInitiallyCollapsed ? ' hidden' : '' ?>>
<div class="srv-grid">
<?php foreach ($checks as $check): ?>
<?php $st = $check['status'] ?? 'unknown'; ?>
<div class="srv-card">
<div class="srv-card__header">
<span class="srv-card__name"><?= htmlspecialchars($check['label']) ?></span>
<span class="<?= SystemController::statusClass($st) ?>"><?= SystemController::statusLabel($st) ?></span>
</div>
<?php if (!empty($check['detail'])): ?>
<div class="srv-card__detail"><?= htmlspecialchars($check['detail']) ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<div class="sys-status-meta">
<div>
<h4 class="srv-section-title srv-section-title--sub">Environnement PHP</h4>
<div class="php-grid php-grid--flush">
<?php foreach ($phpInfo as $key => $val): ?>
<div class="php-item">
<div class="php-item__key"><?= htmlspecialchars($key) ?></div>
<div class="php-item__val"><?= htmlspecialchars($val) ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<div>
<h4 class="srv-section-title srv-section-title--sub">Espace disque</h4>
<div class="disk-bar-wrap">
<div class="disk-bar" style="--disk-pct:<?= $diskPct ?>%;--disk-color:<?= $diskColor ?>"></div>
</div>
<div class="disk-stats">
<span><?= SystemController::humanBytes($diskUsed) ?> utilisé (<?= $diskPct ?>%)</span>
<span><?= SystemController::humanBytes($diskFree) ?> libre / <?= SystemController::humanBytes($diskTotal) ?></span>
</div>
</div>
</div>
</div>
</section>
<!-- ══════════════════════════════════════════════════════════════
JOURNAUX
══════════════════════════════════════════════════════════════ -->
<section aria-labelledby="settings-logs-title">
<h2 id="settings-logs-title">Journaux</h2>
<nav class="sys-tabs" aria-label="Journaux et configuration">
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
<a href="?tab=<?= htmlspecialchars($key) ?>&amp;n=<?= $selectedN ?>"
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&amp;n=<?= $selectedN ?>"
hx-target="#sys-tab-panel"
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&amp;n=<?= $selectedN ?>"
hx-swap="innerHTML"
hx-indicator="#sys-tab-panel"
data-tab="<?= htmlspecialchars($key) ?>"
<?= $activeTab === $key ? 'aria-current="page"' : '' ?>>
<?= htmlspecialchars($def['label']) ?>
</a>
<?php endforeach; ?>
</nav>
<div id="sys-tab-panel">
<?php include APP_ROOT . '/templates/admin/partials/system-log-panel.php'; ?>
</div>
</section>
</article>
</main>
<script src="<?= App::assetV('/assets/js/app/smtp-error-focus.js') ?>"></script>
<script src="/assets/js/app/admin-logs.js"></script>
<!-- Enable maintenance confirm -->
<dialog id="enable-maintenance-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="enable-maint-title">
<div class="admin-dialog__header">
<h2 id="enable-maint-title">Activer la maintenance</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Mettre le site en maintenance ? Les visiteurs verront une page 503.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--warning"
onclick="this.closest('dialog').close(); document.getElementById('enable-maintenance-form').submit()">
Activer
</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>