feat : magic link confirm, notif auteur, rate-limit IP, duplicate, cache MD, lazy img (v1.6.18)

- magic.php : GET=confirmation page, POST=consommation (protège vs scanners) (#27)
- verify_comment : email de notification à l'auteur de l'article (#44)
- login/index.php : rate limit par IP (MAGIC_MAX_PER_IP_HOUR=10) (#23)
- ArticleManager::duplicate() + route POST /duplicate/{uuid} + bouton ⧉ admin/articles (#7)
- post_view.php : cache JSON du rendu Markdown (invalidé sur mtime index.md) (#17)
- post_view.php : loading="lazy" sur toutes les <img> du contenu (#21)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 10:30:55 +02:00
parent 51055b7321
commit 11399a54a6
9 changed files with 201 additions and 43 deletions
+14 -4
View File
@@ -16,10 +16,11 @@ require_once dirname(__DIR__, 2) . '/src/SiteSettings.php';
require_once dirname(__DIR__, 2) . '/src/mailer.php';
// Paramètres (env)
$ttlMin = (int) env('MAGIC_LINK_TTL_MINUTES', '30');
$coolMin = (int) env('MAGIC_COOLDOWN_MINUTES', '5');
$winHours = (int) env('MAGIC_WINDOW_HOURS', '12');
$maxPerWin = (int) env('MAGIC_MAX_PER_WINDOW', '5');
$ttlMin = (int) env('MAGIC_LINK_TTL_MINUTES', '30');
$coolMin = (int) env('MAGIC_COOLDOWN_MINUTES', '5');
$winHours = (int) env('MAGIC_WINDOW_HOURS', '12');
$maxPerWin = (int) env('MAGIC_MAX_PER_WINDOW', '5');
$maxPerIpHour = (int) env('MAGIC_MAX_PER_IP_HOUR', '10');
// --- return_to ---
$defaultReturn = '/';
@@ -94,6 +95,15 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
throw new RuntimeException('Quota atteint. Réessayez plus tard.');
}
// 3) rate limit par IP
$stmt = $pdo->prepare(
"SELECT COUNT(*) FROM auth_magic_links WHERE ip = :ip AND created_at >= NOW() - INTERVAL '1 hour'"
);
$stmt->execute([':ip' => $ip]);
if ((int)$stmt->fetchColumn() >= $maxPerIpHour) {
throw new RuntimeException('Quota atteint. Réessayez plus tard.');
}
// Génère et enregistre le lien avec TTL ttlMin
$raw = random_bytes(32);
$token = rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');