11399a54a6
- 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>
100 lines
4.3 KiB
PHP
100 lines
4.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
if (!defined('BASE_PATH')) {
|
|
define('BASE_PATH', dirname(__DIR__, 2));
|
|
}
|
|
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
|
require_once dirname(__DIR__, 2) . '/config/config.php';
|
|
require_once dirname(__DIR__, 2) . '/bootstrap.php';
|
|
|
|
$token = (string)($_GET['token'] ?? '');
|
|
if ($token === '' || preg_match('/[^A-Za-z0-9\-\_]/', $token)) {
|
|
http_response_code(400);
|
|
exit(renderMagicPage('Lien invalide', '<p>Ce lien de connexion est invalide.</p>', null));
|
|
}
|
|
|
|
$pdo = db();
|
|
|
|
// ─── Rendu minimal standalone ────────────────────────────────────────────────
|
|
function renderMagicPage(string $title, string $body, ?string $token): string
|
|
{
|
|
$formHtml = $token !== null
|
|
? '<form method="post" action="' . htmlspecialchars($_SERVER['REQUEST_URI'] ?? '') . '">'
|
|
. '<input type="hidden" name="confirm" value="1">'
|
|
. '<button type="submit" style="display:inline-block;padding:10px 24px;background:#0d6efd;color:#fff;border:none;border-radius:4px;font-size:1rem;cursor:pointer">Se connecter</button>'
|
|
. '</form>'
|
|
: '';
|
|
return '<!doctype html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
|
|
. '<title>' . htmlspecialchars($title) . '</title>'
|
|
. '<style>body{font-family:system-ui,sans-serif;max-width:480px;margin:80px auto;padding:0 1rem;text-align:center}'
|
|
. 'h1{font-size:1.4rem;margin-bottom:1rem}</style></head>'
|
|
. '<body><h1>' . htmlspecialchars($title) . '</h1>' . $body . $formHtml . '</body></html>';
|
|
}
|
|
|
|
// ─── GET : afficher la page de confirmation ──────────────────────────────────
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$stmt = $pdo->prepare('SELECT id, expires_at, consumed_at FROM auth_magic_links WHERE token = :t');
|
|
$stmt->execute([':t' => $token]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
http_response_code(400);
|
|
exit(renderMagicPage('Lien inconnu', '<p>Ce lien de connexion est introuvable.</p>', null));
|
|
}
|
|
if ($row['consumed_at'] !== null) {
|
|
http_response_code(400);
|
|
exit(renderMagicPage('Lien déjà utilisé', '<p>Ce lien de connexion a déjà été utilisé.</p><p><a href="/login">Demander un nouveau lien</a></p>', null));
|
|
}
|
|
if (strtotime((string)$row['expires_at']) < time()) {
|
|
http_response_code(400);
|
|
exit(renderMagicPage('Lien expiré', '<p>Ce lien de connexion a expiré.</p><p><a href="/login">Demander un nouveau lien</a></p>', null));
|
|
}
|
|
|
|
exit(renderMagicPage('Connexion', '<p style="color:#555;margin-bottom:1.5rem">Cliquez sur le bouton ci-dessous pour vous connecter.</p>', $token));
|
|
}
|
|
|
|
// ─── POST : consommer le token et ouvrir la session ──────────────────────────
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$sql = 'SELECT id, email, expires_at, consumed_at, return_to
|
|
FROM auth_magic_links
|
|
WHERE token = :t
|
|
FOR UPDATE';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([':t' => $token]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
throw new RuntimeException('Lien inconnu.');
|
|
}
|
|
if ($row['consumed_at'] !== null) {
|
|
throw new RuntimeException('Lien déjà utilisé.');
|
|
}
|
|
if (strtotime((string)$row['expires_at']) < time()) {
|
|
throw new RuntimeException('Lien expiré.');
|
|
}
|
|
|
|
$pdo->prepare('UPDATE auth_magic_links SET consumed_at = NOW() WHERE id = :id')->execute([':id' => $row['id']]);
|
|
$pdo->commit();
|
|
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
session_start();
|
|
}
|
|
session_regenerate_id(true);
|
|
$_SESSION['user_email'] = strtolower(trim((string)$row['email']));
|
|
|
|
$dest = $row['return_to'] ?? '/';
|
|
if (!is_string($dest) || !str_starts_with($dest, '/')) {
|
|
$dest = '/';
|
|
}
|
|
header('Location: ' . $dest, true, 303);
|
|
exit;
|
|
} catch (\Throwable $e) {
|
|
if ($pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
http_response_code(400);
|
|
exit(renderMagicPage('Erreur', '<p>' . htmlspecialchars($e->getMessage()) . '</p><p><a href="/login">Retour à la connexion</a></p>', null));
|
|
}
|