Make the magic-link resend cooldown configurable
EmailVerifier::RESEND_INTERVAL_SECONDS was a hardcoded class constant shared (via a copy-of-a-constant) by AuthController and EmailVerificationController. It's now a constructor param (resendIntervalSeconds, default 60, same as before) sourced from Config -- new MAGIC_LINK_RESEND_SECONDS env var, default unchanged. Docker Compose sets it to 0, so magic links resend immediately during local development instead of waiting out the throttle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,6 @@ final class AuthController extends Controller
|
||||
$lastSent = $user['verification_email_sent_at'] ?? null;
|
||||
|
||||
return $lastSent !== null
|
||||
&& (time() - (int) strtotime($lastSent)) < EmailVerifier::RESEND_INTERVAL_SECONDS;
|
||||
&& (time() - (int) strtotime($lastSent)) < $this->verifier->resendIntervalSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
final class EmailVerificationController extends Controller
|
||||
{
|
||||
private const RESEND_INTERVAL_SECONDS = EmailVerifier::RESEND_INTERVAL_SECONDS;
|
||||
private const EMAIL_MAX = 255;
|
||||
|
||||
public function __construct(
|
||||
@@ -115,7 +114,7 @@ final class EmailVerificationController extends Controller
|
||||
return $this->json($response, [
|
||||
'message' => 'Confirmation email sent to the new address.',
|
||||
'pending_email' => $newEmail,
|
||||
'retry_after' => self::RESEND_INTERVAL_SECONDS,
|
||||
'retry_after' => $this->verifier->resendIntervalSeconds,
|
||||
], 202);
|
||||
}
|
||||
|
||||
@@ -130,11 +129,11 @@ final class EmailVerificationController extends Controller
|
||||
}
|
||||
|
||||
$elapsed = time() - (int) strtotime($lastSent);
|
||||
if ($elapsed < self::RESEND_INTERVAL_SECONDS) {
|
||||
if ($elapsed < $this->verifier->resendIntervalSeconds) {
|
||||
throw new ApiException(
|
||||
'Please wait a moment before requesting another email.',
|
||||
429,
|
||||
['retry_after' => self::RESEND_INTERVAL_SECONDS - $elapsed],
|
||||
['retry_after' => $this->verifier->resendIntervalSeconds - $elapsed],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@ use App\Repository\UserRepository;
|
||||
final class EmailVerifier
|
||||
{
|
||||
public const TOKEN_TTL_SECONDS = 900; // 15 minutes
|
||||
public const RESEND_INTERVAL_SECONDS = 60;
|
||||
|
||||
public function __construct(
|
||||
private readonly EmailVerificationRepository $tokens,
|
||||
private readonly UserRepository $users,
|
||||
private readonly Mailer $mailer,
|
||||
private readonly string $appUrl,
|
||||
/** How long to wait before a magic link can be resent to the same address. */
|
||||
public readonly int $resendIntervalSeconds = 60,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ final class Config
|
||||
public readonly string $webauthnRpId,
|
||||
/** WebAuthn relying party display name, shown by the browser/OS passkey prompt. */
|
||||
public readonly string $webauthnRpName,
|
||||
/** Minimum gap between magic links sent to the same address. */
|
||||
public readonly int $resendIntervalSeconds,
|
||||
public readonly MailConfig $mail,
|
||||
) {
|
||||
}
|
||||
@@ -56,6 +58,8 @@ final class Config
|
||||
$webauthnRpId = self::env('WEBAUTHN_RP_ID') ?? (parse_url($appUrl, PHP_URL_HOST) ?: 'localhost');
|
||||
$webauthnRpName = self::env('WEBAUTHN_RP_NAME', 'Projects');
|
||||
|
||||
$resendIntervalSeconds = (int) (self::env('MAGIC_LINK_RESEND_SECONDS') ?? '60');
|
||||
|
||||
$mailLogPath = self::env('MAIL_LOG_PATH', $storagePath . '/mail.log');
|
||||
if (!self::isAbsolutePath($mailLogPath)) {
|
||||
$mailLogPath = $basePath . '/' . ltrim($mailLogPath, '/');
|
||||
@@ -82,6 +86,7 @@ final class Config
|
||||
$appUrl,
|
||||
$webauthnRpId,
|
||||
$webauthnRpName,
|
||||
$resendIntervalSeconds,
|
||||
$mail,
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ $session = new SessionPayload($jwt, $verificationTokens, $passkeys);
|
||||
$mailer = $config->mail->transport === 'log'
|
||||
? new LogMailer($config->mail->logPath)
|
||||
: new PhpMailerMailer($config->mail);
|
||||
$verifier = new EmailVerifier($verificationTokens, $users, $mailer, $config->appUrl);
|
||||
$verifier = new EmailVerifier($verificationTokens, $users, $mailer, $config->appUrl, $config->resendIntervalSeconds);
|
||||
|
||||
// 'none' attestation: verify the credential is a legitimate WebAuthn response
|
||||
// without checking authenticator provenance against a root CA -- the usual
|
||||
|
||||
Reference in New Issue
Block a user