[ "Too many magic link requests. Please try again in {$seconds} seconds.", ], ]); } RateLimiter::hit($key, 3600); if (!User::findByEmail($email)) { User::create([ 'email_hash' => User::hashEmail($email), ]); } return MagicLoginToken::generate($email, $ip, $ua); } /** * Verify a magic link token and log the user in. */ public function verifyMagicLink(string $token): bool { $magicToken = MagicLoginToken::valid() ->where('token_hash', hash('sha256', $token)) ->first(); if (!$magicToken) { return false; } $user = User::where('email_hash', $magicToken->email_hash)->first(); if (!$user) { return false; } $magicToken->markAsUsed(); if (is_null($user->email_verified_at)) { $user->email_verified_at = now(); $user->save(); } Auth::login($user, true); return true; } /** * Verify a magic code and log the user in. * * @throws ValidationException */ public function verifyCode(string $email, string $code): bool { $emailHash = User::hashEmail($email); $key = 'magic-code:' . $emailHash; if (RateLimiter::tooManyAttempts($key, 5)) { $seconds = RateLimiter::availableIn($key); throw ValidationException::withMessages([ 'code' => [ "Too many attempts. Please try again in {$seconds} seconds.", ], ]); } $magicToken = MagicLoginToken::valid() ->where('email_hash', $emailHash) ->latest() ->first(); if (!$magicToken || !$magicToken->verifyCode($code)) { RateLimiter::hit($key, 300); return false; } $user = User::where('email_hash', $emailHash)->first(); if (!$user) { return false; } $magicToken->markAsUsed(); RateLimiter::clear($key); if (is_null($user->email_verified_at)) { $user->email_verified_at = now(); $user->save(); } Auth::login($user, true); return true; } /** * Clean up expired and used tokens older than 7 days. */ public function cleanupExpiredTokens(): int { return MagicLoginToken::expiredOrUsed() ->where('created_at', '<', now()->subDays(7)) ->delete(); } }