validate([ 'email' => ['required', 'email'], 'password' => ['required'], ]); $remember = $request->filled('remember'); if (Auth::attempt($credentials, $remember)) { $user = Auth::user(); if ($user->two_factor_enabled && $user->two_factor_secret) { Auth::logout(); $request->session()->put('2fa_user_id', $user->id); $request->session()->put('2fa_remember', $remember); return redirect()->route('2fa.challenge'); } $request->session()->regenerate(); AuditLog::record('user.login', [ 'user_id' => $user->id, 'user_name' => $user->name, 'details' => ['email' => $user->email], ]); return redirect()->intended('/videos'); } AuditLog::record('user.login.failed', [ 'user_id' => null, 'user_name' => null, 'details' => ['email' => $credentials['email']], ]); $this->detectBruteForce($request, $credentials['email']); return back()->withErrors([ 'email' => 'The provided credentials do not match our records.', ]); } /** * Fire one alert to super admins when the same email has racked up * BRUTE_FORCE_THRESHOLD failures in BRUTE_FORCE_WINDOW_MINUTES. A Cache * key with the same TTL prevents the alert from re-firing on every * subsequent failed attempt within the same burst. */ private const BRUTE_FORCE_THRESHOLD = 5; private const BRUTE_FORCE_WINDOW_MINUTES = 10; private function detectBruteForce(Request $request, string $email): void { $window = self::BRUTE_FORCE_WINDOW_MINUTES; $threshold = self::BRUTE_FORCE_THRESHOLD; // SQLite stores JSON-cast `details` as text; a LIKE-based match is // database-portable and avoids per-driver JSON operator syntax. $pattern = '%"email":"' . addslashes($email) . '"%'; $recent = AuditLog::where('action', 'user.login.failed') ->where('created_at', '>=', now()->subMinutes($window)) ->where('details', 'like', $pattern) ->count(); if ($recent < $threshold) { return; } $cacheKey = 'brute_force_alert:' . sha1($email); if (Cache::has($cacheKey)) { return; } Cache::put($cacheKey, 1, now()->addMinutes($window)); $ip = $request->header('CF-Connecting-IP') ?? $request->header('X-Forwarded-For') ?? $request->ip() ?? 'unknown'; $notification = new BruteForceLoginAlert($email, (string) $ip, $recent, $window); User::where('role', 'super_admin')->each(function (User $admin) use ($notification) { SafeNotifier::send($admin, $notification); }); } public function destroy(Request $request) { $user = Auth::user(); if ($user) { AuditLog::record('user.logout', [ 'user_id' => $user->id, 'user_name' => $user->name, ]); } Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect('/videos'); } }