mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
feat: admin audit logging across all admin actions
- AdminLogger: JSON-lines → /var/log/xamxam.log (prod) / storage/logs/admin.log (dev) + best-effort DB mirror to admin_audit_log table - DB: admin_audit_log table, share_links.is_archived column - ShareLink: archive() replaces delete(), toggleActive() returns new state, listActive()/listArchived() split, validateLink blocks archived slugs - All action handlers wired: publish, unpublish, visibility, delete, csv/db export, tfe add/edit, tags, pages, apropos, form-help, access-request, maintenance, settings (formulaire toggles, objet types, smtp update), smtp-test - acces.php: archive button replaces delete; collapsible archived links section - setup-server.sh: provision /var/log/xamxam.log (www-data:xamxam 640)
This commit is contained in:
@@ -6,8 +6,9 @@ require_once __DIR__ . '/../../src/ShareLink.php';
|
||||
App::adminGuard();
|
||||
|
||||
// ── Liens d'accès étudiant·e ──────────────────────────────────────────────────
|
||||
$shareLink = ShareLink::make();
|
||||
$links = $shareLink->listAll();
|
||||
$shareLink = ShareLink::make();
|
||||
$links = $shareLink->listActive();
|
||||
$archivedLinks = $shareLink->listArchived();
|
||||
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$baseUrl = $protocol . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Student-access link actions (create, toggle, set_password, delete).
|
||||
* Student-access link actions (create, toggle, set_password, archive).
|
||||
*/
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
require_once __DIR__ . '/../../../src/ShareLink.php';
|
||||
require_once __DIR__ . '/../../../src/AdminLogger.php';
|
||||
|
||||
App::adminGuard();
|
||||
|
||||
@@ -15,9 +16,10 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||
exit('CSRF token invalide.');
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||
$action = $_POST['action'] ?? '';
|
||||
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||
$shareLink = ShareLink::make();
|
||||
$logger = AdminLogger::make();
|
||||
|
||||
switch ($action) {
|
||||
case 'create':
|
||||
@@ -32,13 +34,20 @@ switch ($action) {
|
||||
}
|
||||
$objetRaw = $_POST['objet_restriction'] ?? '';
|
||||
$objetRestriction = in_array($objetRaw, ['tfe', 'thèse', 'frart'], true) ? $objetRaw : null;
|
||||
$shareLink->create(1, $password, $expiresAt, $objetRestriction);
|
||||
$link = $shareLink->create(1, $password, $expiresAt, $objetRestriction);
|
||||
$logger->logLinkCreate(
|
||||
$link['slug'] ?? '',
|
||||
$password !== null,
|
||||
$expiresAt,
|
||||
$objetRestriction
|
||||
);
|
||||
App::redirect('/admin/acces.php', success: 'Lien d\'accès créé.');
|
||||
break;
|
||||
|
||||
case 'toggle':
|
||||
if ($id > 0) {
|
||||
$shareLink->toggleActive($id);
|
||||
$nowActive = $shareLink->toggleActive($id);
|
||||
$logger->logLinkToggle($id, $nowActive);
|
||||
App::redirect('/admin/acces.php', success: 'Statut du lien modifié.');
|
||||
} else {
|
||||
App::redirect('/admin/acces.php', error: 'Lien introuvable.');
|
||||
@@ -49,16 +58,18 @@ switch ($action) {
|
||||
if ($id > 0) {
|
||||
$password = isset($_POST['password']) && $_POST['password'] !== '' ? trim($_POST['password']) : null;
|
||||
$shareLink->setPassword($id, $password);
|
||||
$logger->logLinkPasswordChange($id, $password === null);
|
||||
App::redirect('/admin/acces.php', success: 'Mot de passe mis à jour.');
|
||||
} else {
|
||||
App::redirect('/admin/acces.php', error: 'Lien introuvable.');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
case 'archive':
|
||||
if ($id > 0) {
|
||||
$shareLink->delete($id);
|
||||
App::redirect('/admin/acces.php', success: 'Lien supprimé.');
|
||||
$shareLink->archive($id);
|
||||
$logger->logLinkArchive($id);
|
||||
App::redirect('/admin/acces.php', success: 'Lien archivé.');
|
||||
} else {
|
||||
App::redirect('/admin/acces.php', error: 'Lien introuvable.');
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/SmtpRelay.php';
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
|
||||
$db = Database::getInstance();
|
||||
$db = Database::getInstance();
|
||||
$logger = AdminLogger::make();
|
||||
|
||||
$requestId = isset($_POST['request_id']) ? (int)$_POST['request_id'] : 0;
|
||||
$action = $_POST['action'] ?? '';
|
||||
@@ -54,9 +56,11 @@ try {
|
||||
|
||||
try {
|
||||
SmtpRelay::send($db, $request['email'], $subject, $body, $plain);
|
||||
$logger->logAccessRequest($requestId, 'approve', $request['email'], $request['title']);
|
||||
App::flash('success', "Demande approuvée. Email envoyé à {$request['email']}.");
|
||||
} catch (SmtpSendException $e) {
|
||||
error_log('[access-request] Email delivery failed after approval: ' . $e->getMessage());
|
||||
$logger->logAccessRequest($requestId, 'approve', $request['email'], $request['title']);
|
||||
$smtpMsg = $e->isRecipientRejected()
|
||||
? "Demande approuvée, mais l'email n'a pas pu être délivré : adresse inconnue ({$request['email']})."
|
||||
: "Demande approuvée, mais l'envoi de l'email a échoué (erreur SMTP). L'utilisateur devra relancer une demande.";
|
||||
@@ -65,8 +69,7 @@ try {
|
||||
|
||||
} elseif ($action === 'reject') {
|
||||
$db->rejectAccessRequest($requestId, $notes);
|
||||
|
||||
// Optionally send rejection email (not implemented for now)
|
||||
$logger->logAccessRequest($requestId, 'reject', $request['email'], $request['title']);
|
||||
|
||||
App::flash('success', "Demande rejetée.");
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ if (!in_array($aproposKey, $allowedKeys)) {
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
require_once __DIR__ . '/../../../src/AdminLogger.php';
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
@@ -66,6 +67,7 @@ try {
|
||||
}
|
||||
|
||||
$db->saveAproposContent($aproposKey, $cleaned);
|
||||
AdminLogger::make()->logAproposEdit($aproposKey);
|
||||
App::flash('success', "Contenu « $aproposKey » mis à jour avec succès.");
|
||||
} catch (Exception $e) {
|
||||
error_log("Apropos save error: " . $e->getMessage());
|
||||
|
||||
@@ -5,6 +5,7 @@ require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
require_once __DIR__ . '/../../../src/AdminLogger.php';
|
||||
|
||||
// CSRF validation
|
||||
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
@@ -20,8 +21,11 @@ $isDeleteAll = !empty($_POST['delete_all']);
|
||||
try {
|
||||
$db = new Database();
|
||||
|
||||
$logger = AdminLogger::make();
|
||||
|
||||
if ($isDeleteAll) {
|
||||
$count = $db->deleteAllTheses();
|
||||
$logger->logDeleteAllTheses($count);
|
||||
App::flash('success', "$count TFE(s) supprimé(s) avec succès.");
|
||||
|
||||
} elseif ($isBulk) {
|
||||
@@ -34,6 +38,7 @@ try {
|
||||
}
|
||||
|
||||
$db->bulkDeleteTheses($ids);
|
||||
$logger->logDelete(array_values($ids));
|
||||
$count = count($ids);
|
||||
App::flash('success', "$count TFE(s) supprimé(s) avec succès.");
|
||||
|
||||
@@ -47,6 +52,7 @@ try {
|
||||
}
|
||||
|
||||
$db->deleteThesis($thesisId);
|
||||
$logger->logDelete([$thesisId]);
|
||||
App::flash('success', 'TFE supprimé avec succès.');
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ if ($thesisId <= 0) {
|
||||
}
|
||||
|
||||
require_once APP_ROOT . '/src/Controllers/ThesisEditController.php';
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
|
||||
try {
|
||||
$ctrl = ThesisEditController::create();
|
||||
@@ -33,6 +34,8 @@ try {
|
||||
// Regenerate CSRF token after successful save
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
|
||||
AdminLogger::make()->logEdit($thesisId, $_POST['titre'] ?? $_POST['title'] ?? '');
|
||||
|
||||
App::flash('success', "TFE mis à jour avec succès!");
|
||||
header('Location: ../edit.php?id=' . $thesisId);
|
||||
exit();
|
||||
|
||||
@@ -10,8 +10,11 @@ require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once APP_ROOT . '/src/Controllers/ExportController.php';
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
$controller = ExportController::create();
|
||||
|
||||
AdminLogger::make()->logCsvExport();
|
||||
|
||||
$filename = 'xamxam-export-' . date('Y-m-d') . '.csv';
|
||||
|
||||
header('Content-Type: text/csv; charset=UTF-8');
|
||||
|
||||
@@ -9,6 +9,7 @@ require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once APP_ROOT . '/src/Controllers/ExportController.php';
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
$controller = ExportController::create();
|
||||
|
||||
$dbPath = $controller->getDatabasePath();
|
||||
@@ -25,5 +26,7 @@ header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Content-Length: ' . filesize($dbPath));
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
|
||||
AdminLogger::make()->logDbExport();
|
||||
|
||||
readfile($dbPath);
|
||||
exit;
|
||||
|
||||
@@ -18,6 +18,7 @@ $key = $_POST['form_help_key'] ?? '';
|
||||
$content = $_POST['content'] ?? '';
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
$db = new Database();
|
||||
|
||||
if (!in_array($key, Database::FORM_HELP_KEYS, true)) {
|
||||
@@ -28,6 +29,7 @@ if (!in_array($key, Database::FORM_HELP_KEYS, true)) {
|
||||
|
||||
try {
|
||||
$db->setFormHelpBlock($key, $content);
|
||||
AdminLogger::make()->logFormStructureEdit($key);
|
||||
App::flash('success', 'Bloc « ' . htmlspecialchars($key) . ' » mis à jour.');
|
||||
} catch (Exception $e) {
|
||||
error_log('form-help save error: ' . $e->getMessage());
|
||||
|
||||
@@ -24,10 +24,12 @@ error_log('FILES array: ' . print_r($_FILES, true));
|
||||
|
||||
require_once APP_ROOT . '/src/Controllers/ThesisCreateController.php';
|
||||
require_once APP_ROOT . '/src/AppLogger.php';
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
require_once APP_ROOT . '/src/DuplicateThesisException.php';
|
||||
|
||||
$logger = new AppLogger();
|
||||
$authorName = $_POST['auteurice'] ?? 'unknown';
|
||||
$logger = new AppLogger();
|
||||
$adminLogger = AdminLogger::make();
|
||||
$authorName = $_POST['auteurice'] ?? 'unknown';
|
||||
|
||||
try {
|
||||
$ctrl = ThesisCreateController::make();
|
||||
@@ -35,6 +37,7 @@ try {
|
||||
|
||||
$identifier = $ctrl->getIdentifier($thesisId);
|
||||
$logger->logSubmission('admin', $thesisId, $identifier, $authorName);
|
||||
$adminLogger->logAdd($thesisId, $identifier, $authorName);
|
||||
|
||||
unset($_SESSION['csrf_token']);
|
||||
|
||||
|
||||
@@ -10,15 +10,19 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||
die("Accès refusé.");
|
||||
}
|
||||
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'enable_maintenance') {
|
||||
file_put_contents(MAINTENANCE_FLAG, date('c'));
|
||||
AdminLogger::make()->logMaintenance(true);
|
||||
App::flash('success', "Mode maintenance activé.");
|
||||
} elseif ($action === 'disable_maintenance') {
|
||||
if (file_exists(MAINTENANCE_FLAG)) {
|
||||
unlink(MAINTENANCE_FLAG);
|
||||
}
|
||||
AdminLogger::make()->logMaintenance(false);
|
||||
App::flash('success', "Mode maintenance désactivé.");
|
||||
} else {
|
||||
App::flash('error', "Action inconnue.");
|
||||
|
||||
@@ -24,10 +24,12 @@ if (!in_array($slug, $allowedSlugs, true)) {
|
||||
}
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
$db = new Database();
|
||||
|
||||
try {
|
||||
$db->savePage($slug, $content);
|
||||
AdminLogger::make()->logPageEdit($slug);
|
||||
App::flash('success', 'Page « ' . htmlspecialchars($slug) . ' » mise à jour.');
|
||||
} catch (Exception $e) {
|
||||
error_log('page save error: ' . $e->getMessage());
|
||||
|
||||
@@ -5,6 +5,7 @@ require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
require_once __DIR__ . '/../../../src/AdminLogger.php';
|
||||
|
||||
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
@@ -27,6 +28,8 @@ $published = ($action === 'publish');
|
||||
try {
|
||||
$db = new Database();
|
||||
|
||||
$logger = AdminLogger::make();
|
||||
|
||||
if ($isBulk) {
|
||||
$ids = array_filter(array_map('intval', $_POST['selected_theses'] ?? []), fn($id) => $id > 0);
|
||||
|
||||
@@ -37,6 +40,7 @@ try {
|
||||
}
|
||||
|
||||
$db->bulkSetPublished($ids, $published);
|
||||
$logger->logPublish($published, array_values($ids));
|
||||
$count = count($ids);
|
||||
App::flash('success', $published
|
||||
? "$count TFE(s) publié(s) avec succès."
|
||||
@@ -52,6 +56,7 @@ try {
|
||||
}
|
||||
|
||||
$db->setPublished($thesisId, $published);
|
||||
$logger->logPublish($published, [$thesisId]);
|
||||
App::flash('success', $published ? 'TFE publié avec succès.' : 'TFE retiré de la publication.');
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/SmtpRelay.php';
|
||||
$db = new Database();
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
$db = new Database();
|
||||
$logger = AdminLogger::make();
|
||||
|
||||
$section = $_POST['section'] ?? '';
|
||||
|
||||
@@ -23,14 +25,22 @@ if ($section === 'formulaire') {
|
||||
'access_type_interdit_enabled',
|
||||
'restricted_files_enabled'
|
||||
];
|
||||
$newValues = [];
|
||||
foreach ($allowed as $key) {
|
||||
$value = isset($_POST[$key]) ? '1' : '0';
|
||||
$db->setSetting($key, $value);
|
||||
$newValues[$key] = $value;
|
||||
}
|
||||
$logger->logFormSettingsUpdate($newValues);
|
||||
App::flash('success', "Paramètres du formulaire mis à jour.");
|
||||
} elseif ($section === 'objet_types') {
|
||||
$db->setSetting('objet_these_enabled', isset($_POST['objet_these_enabled']) ? '1' : '0');
|
||||
$db->setSetting('objet_frart_enabled', isset($_POST['objet_frart_enabled']) ? '1' : '0');
|
||||
$newValues = [
|
||||
'objet_these_enabled' => isset($_POST['objet_these_enabled']) ? '1' : '0',
|
||||
'objet_frart_enabled' => isset($_POST['objet_frart_enabled']) ? '1' : '0',
|
||||
];
|
||||
$db->setSetting('objet_these_enabled', $newValues['objet_these_enabled']);
|
||||
$db->setSetting('objet_frart_enabled', $newValues['objet_frart_enabled']);
|
||||
$logger->logObjetTypesUpdate($newValues);
|
||||
App::flash('success', "Types de travaux mis à jour.");
|
||||
} elseif ($section === 'smtp') {
|
||||
$smtpData = [
|
||||
@@ -51,6 +61,7 @@ if ($section === 'formulaire') {
|
||||
|
||||
// Immediately probe the server to validate credentials
|
||||
$test = SmtpRelay::test($db);
|
||||
$logger->logSmtpUpdate($test['ok']);
|
||||
if ($test['ok']) {
|
||||
App::flash('success', "Paramètres SMTP mis à jour — connexion validée ✓");
|
||||
} else {
|
||||
|
||||
@@ -52,17 +52,22 @@ $body = <<<HTML
|
||||
</html>
|
||||
HTML;
|
||||
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
|
||||
try {
|
||||
$ok = SmtpRelay::send($db, $to, $subject, $body);
|
||||
if ($ok) {
|
||||
AdminLogger::make()->logSmtpTest($to, true);
|
||||
App::flash('success', "E-mail de test envoyé à « {$to} ».");
|
||||
} else {
|
||||
AdminLogger::make()->logSmtpTest($to, false, 'Send returned false');
|
||||
App::flash('error', "Échec de l'envoi. Vérifiez la configuration SMTP et les logs serveur.");
|
||||
}
|
||||
} catch (SmtpSendException $e) {
|
||||
$detail = $e->isRecipientRejected()
|
||||
? "Adresse rejetée par le serveur ({$to}) : " . $e->smtpResponse
|
||||
: "Erreur SMTP : " . $e->smtpResponse;
|
||||
AdminLogger::make()->logSmtpTest($to, false, $detail);
|
||||
App::flash('error', $detail);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
require_once __DIR__ . '/../../../src/AdminLogger.php';
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
$db = new Database();
|
||||
$logger = AdminLogger::make();
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
@@ -22,6 +24,7 @@ try {
|
||||
$newName = trim($_POST['new_name'] ?? '');
|
||||
if (!$id || $newName === '') throw new Exception("Paramètres invalides.");
|
||||
$db->renameTag($id, $newName);
|
||||
$logger->logTagAction('rename', ['tag_id' => $id, 'new_name' => $newName]);
|
||||
break;
|
||||
|
||||
case 'merge':
|
||||
@@ -29,12 +32,14 @@ try {
|
||||
$targetId = filter_var($_POST['target_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
if (!$sourceId || !$targetId) throw new Exception("Paramètres invalides.");
|
||||
$db->mergeTag($sourceId, $targetId);
|
||||
$logger->logTagAction('merge', ['source_id' => $sourceId, 'target_id' => $targetId]);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$id = filter_var($_POST['tag_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
if (!$id) throw new Exception("ID invalide.");
|
||||
$db->deleteTag($id);
|
||||
$logger->logTagAction('delete', ['tag_id' => $id]);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -11,6 +11,7 @@ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
require_once __DIR__ . '/../../../src/AdminLogger.php';
|
||||
|
||||
$action = $_POST['action'] ?? ''; // 'set_visibility'
|
||||
$accessTypeId = filter_var($_POST['access_type_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||||
@@ -26,6 +27,8 @@ if (!in_array($accessTypeId, $validAccess, true)) {
|
||||
try {
|
||||
$db = new Database();
|
||||
|
||||
$logger = AdminLogger::make();
|
||||
|
||||
if ($isBulk) {
|
||||
$ids = array_filter(array_map('intval', $_POST['selected_theses'] ?? []), fn($id) => $id > 0);
|
||||
if (empty($ids)) {
|
||||
@@ -34,6 +37,7 @@ try {
|
||||
exit;
|
||||
}
|
||||
$db->bulkSetVisibility($ids, $accessTypeId);
|
||||
$logger->logVisibility($accessTypeId, array_values($ids));
|
||||
App::flash('success', count($ids) . " TFE(s) mis à jour.");
|
||||
} else {
|
||||
$thesisId = filter_var($_POST['thesis_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
@@ -43,6 +47,7 @@ try {
|
||||
exit;
|
||||
}
|
||||
$db->setVisibility($thesisId, $accessTypeId);
|
||||
$logger->logVisibility($accessTypeId, [$thesisId]);
|
||||
App::flash('success', "Visibilité mise à jour.");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
|
||||
Reference in New Issue
Block a user