Files
varlog/public/login/index.php
T
Cedric Abonnel 700329f156 Initial commit
2026-05-08 12:55:46 +02:00

189 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// projet : mug.a5l.fr
// fichier : pages/login/index.php
// version : 20251011
declare(strict_types=1);
use App\Http\Csrf;
// --- Helpers AVANT tout usage ---
if (!function_exists('env')) {
function env(string $key, ?string $default = null): ?string {
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') return (string)$_ENV[$key];
$v = getenv($key);
if ($v !== false && $v !== '') return (string)$v;
return $default;
}
}
if (!function_exists('db')) {
function db(): \PDO { return \App\Infrastructure\Database::get(); }
}
if (!function_exists('url')) {
function url(string $path = '/'): string {
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
return $scheme . '://' . $host . $path;
}
}
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
require_once dirname(__DIR__, 2) . '/config/config.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');
// --- return_to ---
$defaultReturn = '/';
$sanitize = static function (string $url) use ($defaultReturn): string {
$url = trim($url);
if ($url === '' || !str_starts_with($url, '/')) return $defaultReturn;
return $url;
};
$returnTo = $sanitize((string)($_GET['return_to'] ?? ($_SERVER['HTTP_REFERER'] ?? $defaultReturn)));
// --- OIDC ---
$oidcEnabled = (bool) (env('OIDC_ISSUER') && env('OIDC_CLIENT_ID'));
$oidcLoginUrl = '/login/oidc' . ($returnTo ? ('?return_to=' . urlencode($returnTo)) : '');
$oidcAuto = (isset($_GET['sso']) && $_GET['sso'] === '1') || (env('OIDC_AUTO', '0') === '1');
if ($oidcEnabled && $oidcAuto) { header('Location: ' . $oidcLoginUrl, true, 302); exit; }
// --- form: demande de lien magique ---
$errors = [];
$okMsg = '';
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
if (!Csrf::validate($_POST['_csrf'] ?? null)) {
http_response_code(400);
$errors[] = 'Jeton CSRF invalide.';
} else {
$email = strtolower(trim((string)($_POST['email'] ?? '')));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Adresse email invalide.';
} else {
// rate limit simple par email et IP
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
if (strpos($ip, ',') !== false) $ip = trim(explode(',', $ip, 2)[0]);
$pdo = db();
$pdo->beginTransaction();
try {
// purge expirés / consommés
$pdo->prepare("DELETE FROM auth_magic_links WHERE email = :e AND (expires_at < NOW() OR consumed_at IS NOT NULL)")
->execute([':e' => $email]);
// 1) cooldown: refuser si un envoi récent < coolMin
$sql = sprintf(
"SELECT 1 FROM auth_magic_links
WHERE email = :e AND created_at >= NOW() - INTERVAL '%d minutes'
LIMIT 1",
max(0, $coolMin)
);
$stmt = $pdo->prepare($sql);
$stmt->execute([':e' => $email]);
if ($stmt->fetchColumn()) {
throw new RuntimeException(sprintf('Un lien vient d’être envoyé. Réessayez dans %d min.
Si vous ne recevez toujours rien, envisagez d\'utiliser un fournisseur de messagerie respectueux de la vie privée,
comme Proton Mail, Tuta, Posteo, Mailfence ou Infomaniak, qui garantissent un hébergement européen
et ne revendent pas vos données. -- Cédrix, le 11/10/2025', $coolMin));
}
// 2) plafond: maxPerWin liens sur winHours
$sql = sprintf(
"SELECT COUNT(*) FROM auth_magic_links
WHERE email = :e AND created_at >= NOW() - INTERVAL '%d hours'",
max(0, $winHours)
);
$stmt = $pdo->prepare($sql);
$stmt->execute([':e' => $email]);
if ((int)$stmt->fetchColumn() >= $maxPerWin) {
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), '+/', '-_'), '=');
$sql = sprintf(
"INSERT INTO auth_magic_links (id,email,token,created_at,expires_at,ip,user_agent,return_to)
VALUES (gen_random_uuid(), :email, :token, NOW(), NOW() + INTERVAL '%d minutes', :ip, :ua, :rt)
RETURNING token",
max(1, $ttlMin)
);
$stmt = $pdo->prepare($sql);
$stmt->execute([
':email' => $email,
':token' => $token,
':ip' => $ip,
':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512),
':rt' => ($returnTo !== '/' ? $returnTo : null),
]);
$pdo->commit();
// construit lURL et ENVOIE le mail ici...
$magicUrl = url('/login/magic.php') . '?token=' . urlencode($token);
/* envoyer_mail_smtp(...) ou mail(...) */
// message utilisateur
$okMsg = "Un lien vient d'être envoyé. Vérifiez votre boîte de réception et le dossier spam/indésirables.";
} catch (\Throwable $ex) {
if ($pdo->inTransaction()) $pdo->rollBack();
$errors[] = $ex->getMessage();
}
}
}
}
$csrf = Csrf::token();
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Connexion</title>
<link rel="stylesheet" href="/assets/bootstrap/bootstrap.min.css">
<style>.or-sep{display:flex;align-items:center;gap:.75rem;margin:1.25rem 0}.or-sep::before,.or-sep::after{content:"";flex:1;height:1px;background:#ddd}</style>
</head>
<body class="container py-5">
<h1 class="mb-3">Bienvenue 👋</h1>
<p class="text-muted">Vous n’êtes pas connecté. Accédez auxfonctionnalités en vous identifiant.</p>
<?php foreach ($errors as $e): ?>
<div class="alert alert-danger"><?= htmlspecialchars($e, ENT_QUOTES) ?></div>
<?php endforeach; ?>
<?php if ($okMsg): ?>
<div class="alert alert-success"><?= htmlspecialchars($okMsg, ENT_QUOTES) ?></div>
<?php endif; ?>
<?php if ($oidcEnabled): ?>
<div class="mb-3">
<a class="btn btn-primary w-100" href="<?= htmlspecialchars($oidcLoginUrl, ENT_QUOTES) ?>">Se connecter avec A5L</a>
</div>
<div class="or-sep"><span>ou</span></div>
<?php else: ?>
<div class="alert alert-warning">A5L indisponible : configurez <code>OIDC_ISSUER</code> et <code>OIDC_CLIENT_ID</code> dans <code>.env</code>.</div>
<div class="or-sep"><span>ou</span></div>
<?php endif; ?>
<form method="post" action="/login<?= $returnTo ? ('?return_to=' . urlencode($returnTo)) : '' ?>" novalidate>
<input type="hidden" name="_csrf" value="<?= htmlspecialchars($csrf, ENT_QUOTES) ?>">
<div class="mb-3">
<label class="form-label" for="email">Adresse email</label>
<input class="form-control" id="email" type="email" name="email" required autocomplete="email" inputmode="email" placeholder="vous@domaine.tld">
</div>
<button class="btn btn-primary" type="submit">Recevoir un lien magique</button>
</form>
</body>
</html>