feat: prevent duplicate TFE submissions with logging and user feedback

- Add DuplicateThesisException (typed, carries existing thesis metadata)
- Add Database::findDuplicateThesis(): matches on year + author + normalised
  title (exact, prefix, Levenshtein ≤10% of longer string)
- ThesisCreateController::submit() runs duplicate check before any DB write
  and throws DuplicateThesisException on match
- AppLogger::logDuplicate() writes status=duplicate entries to the JSON-lines
  log for audit purposes
- App::flash/consumeFlash extended to support 'warning' flash type
- admin/actions/formulaire.php: catches DuplicateThesisException, logs it,
  flashes an HTML warning toast with a clickable link to the existing thesis,
  and repopulates the form fields
- partage/index.php: same catch block; surfaces a plain-text flash-warning
  banner on the student form with identifier, title, and year of the match;
  form is repopulated via session
- toast.php: renders toast--warning variant
- admin.css: .toast--warning + link colour rules
- form.css: .flash-warning style for the partage form
This commit is contained in:
Pontoporeia
2026-05-04 16:29:31 +02:00
parent 0a05f3911c
commit a2cba6d3c0
35 changed files with 1726 additions and 1302 deletions

View File

@@ -259,12 +259,16 @@ function renderShareLinkForm(string $slug, array $link): void
<?php
// Show flash messages from error redirect
$flashError = $_SESSION['_flash_error'] ?? null;
$flashWarning = $_SESSION['_flash_warning'] ?? null;
$flashSuccess = $_SESSION['_flash_success'] ?? null;
unset($_SESSION['_flash_error'], $_SESSION['_flash_success']);
unset($_SESSION['_flash_error'], $_SESSION['_flash_warning'], $_SESSION['_flash_success']);
?>
<?php if ($flashError): ?>
<div class="flash-error" role="alert"><?= htmlspecialchars($flashError) ?></div>
<?php endif; ?>
<?php if ($flashWarning): ?>
<div class="flash-warning" role="alert"><?= htmlspecialchars($flashWarning) ?></div>
<?php endif; ?>
<?php if ($flashSuccess): ?>
<div class="flash-success" role="alert"><?= htmlspecialchars($flashSuccess) ?></div>
<?php endif; ?>
@@ -434,6 +438,7 @@ function handleShareLinkSubmission(string $slug): void
require_once APP_ROOT . '/src/SmtpRelay.php';
require_once APP_ROOT . '/src/StudentEmail.php';
require_once APP_ROOT . '/src/AppLogger.php';
require_once APP_ROOT . '/src/DuplicateThesisException.php';
$logger = new AppLogger();
$authorName = $_POST['auteurice'] ?? 'unknown';
@@ -474,6 +479,24 @@ function handleShareLinkSubmission(string $slug): void
// Redirect to thanks page
header('Location: /partage/recapitulatif?id=' . urlencode((string)$thesisId));
exit();
} catch (DuplicateThesisException $e) {
$logger->logDuplicate('partage', $authorName, $e->existingThesisId, $e->existingIdentifier, [
'share_slug' => $slug,
]);
error_log('Share link duplicate submission: ' . $e->getMessage());
// Repopulate the form and surface a clear warning to the student.
$_SESSION['_flash_warning'] = 'Votre soumission ressemble à un TFE déjà enregistré ('
. htmlspecialchars($e->existingIdentifier . ' — ' . $e->existingTitle . ', ' . $e->existingYear)
. '). Si vous pensez quil sagit dune erreur, veuillez contacter léquipe.';
$_SESSION['form_data_share_' . $slug] = $_POST;
$_SESSION[$shareCsrfKey] = bin2hex(random_bytes(32)); // Regenerate token
header('Location: /partage/' . urlencode($slug));
exit();
} catch (Exception $e) {
$logger->logError('partage', $e->getMessage(), [
'share_slug' => $slug,