add password-reset flow: request endpoint + reset page + login link (shared OneTimeToken)

This commit is contained in:
Pontoporeia
2026-08-24 11:36:02 +02:00
parent d7184e4447
commit ddae5d8fef
7 changed files with 269 additions and 10 deletions
+63 -10
View File
@@ -182,27 +182,80 @@ class AdminAuth
}
/**
* Redeem a password-reset token and return the new password hash.
* Initiate a password reset: issue a one-time token and email the link
* to the admin notification address.
*
* On success, the token is consumed (one-time) and a fresh bcrypt hash
* is generated for the given new password — but NOT yet persisted; the
* caller decides when to store it (so the reset flow can validate before
* committing).
*
* @return string|null bcrypt hash, or null when the token is invalid.
* Uses the shared OneTimeToken model + SmtpRelay. Returns true when the
* email was accepted for delivery (false if no address is configured or
* sending fails).
*/
public static function redeemPasswordResetToken(string $token, string $newPassword): ?string
public static function requestPasswordReset(): bool
{
require_once APP_ROOT . '/src/SmtpRelay.php';
$db = new Database();
$to = SmtpRelay::getNotifyEmail($db);
if ($to === '') {
error_log('[AdminAuth] password reset requested but no notify email configured');
return false;
}
$token = self::issuePasswordResetToken();
$host = $_SERVER['HTTP_HOST'] ?? 'xamxam.erg.be';
$url = "https://{$host}/admin/password-reset.php?token={$token}";
$subject = 'Réinitialisation du mot de passe — XAMXAM';
$body = <<<HTML
<html><body style="font-family:sans-serif;color:#222;max-width:600px;margin:0 auto;padding:24px">
<h1 style="font-size:1.4rem;border-bottom:2px solid #c00;padding-bottom:8px">Réinitialisation du mot de passe</h1>
<p>Une demande de réinitialisation du mot de passe administrateur a été effectuée.</p>
<p>Cliquez sur le lien ci-dessous pour définir un nouveau mot de passe. Ce lien est à usage unique et expire dans 30 minutes.</p>
<p><a href="{$url}" style="display:inline-block;margin:16px 0;padding:12px 24px;background:#c00;color:#fff;text-decoration:none;border-radius:4px">Définir un nouveau mot de passe</a></p>
<p style="font-size:.85rem;color:#666">Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail.</p>
</body></html>
HTML;
try {
return SmtpRelay::send($db, $to, $subject, $body);
} catch (\Throwable $e) {
error_log('[AdminAuth] password-reset email failed: ' . $e->getMessage());
return false;
}
}
/**
* Redeem a password-reset token and install a new password.
*
* Validates the new password length, consumes the one-time token, stores
* a fresh bcrypt hash (cost 12), and invalidates the session so the admin
* must log in with the new password.
*
* @return bool True on success; false when the token is invalid or the
* password is too short (< 12 chars).
*/
public static function redeemPasswordResetToken(string $token, string $newPassword): bool
{
if (strlen($newPassword) < 12) {
return false;
}
require_once APP_ROOT . '/src/OneTimeToken.php';
$db = new Database();
$ot = new OneTimeToken($db->getPDO());
$context = $ot->redeem('password_reset', $token);
if ($context === null) {
return null;
return false;
}
return password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 12]);
$hash = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 12]);
if ($hash === false) {
return false;
}
self::setPasswordHash($hash);
self::logout(); // invalidate any existing admin session
return true;
}
/**