beginTransaction(); try { // récupère lien non consommé et non expiré $sql = "SELECT id, email, token, created_at, 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é.'); // consomme le lien $pdo->prepare("UPDATE auth_magic_links SET consumed_at = NOW() WHERE id = :id")->execute([':id' => $row['id']]); $pdo->commit(); // ouvre une session applicative « anonyme authentifiée par email » if (session_status() !== PHP_SESSION_ACTIVE) session_start(); $_SESSION['auth'] = [ 'method' => 'magic', 'email' => (string)$row['email'], 'ts' => time(), ]; // Aucun create user ici, conforme à la demande $dest = $row['return_to'] ?? '/'; // sécurité: ne renvoyer que des chemins relatifs 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); echo htmlspecialchars($e->getMessage(), ENT_QUOTES); }