This commit is contained in:
ghassan committed 2026-08-04 11:26:25 +03:00
1 parent d0834df8df
commit 6dc35134fa
171 files changed
+19241 -49

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Actions\Auth;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginAction
{
/**
* @return array{status: 'authenticated', user: User}|array{status: 'requires_2fa', challenge_token: string}
*/
public function execute(Request $request, string $login, string $password): array
{
$throttleKey = 'login:'.$request->ip().':'.$login;
if (RateLimiter::tooManyAttempts($throttleKey, 5)) {
throw ValidationException::withMessages([
'login' => 'Too many login attempts. Please try again in a minute.',
]);
}
$user = User::where('email', $login)->orWhere('username', $login)->first();
if (! $user || ! Hash::check($password, $user->password)) {
RateLimiter::hit($throttleKey, 60);
// Generic message on purpose — never reveal whether the login
// identifier itself exists (no user enumeration).
throw ValidationException::withMessages([
'login' => 'These credentials do not match our records.',
]);
}
RateLimiter::clear($throttleKey);
if ($user->two_factor_enabled) {
$challengeToken = Str::random(40);
Cache::put("2fa_challenge:{$challengeToken}", [
'user_id' => $user->id,
'ip' => $request->ip(),
'attempts' => 0,
], now()->addMinutes(5));
return ['status' => 'requires_2fa', 'challenge_token' => $challengeToken];
}
Auth::login($user);
$request->session()->regenerate();
return ['status' => 'authenticated', 'user' => $user];
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Actions\Auth;
use App\Models\User;
use App\Services\TwoFactorService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\ValidationException;
class VerifyTwoFactorAction
{
public function __construct(private readonly TwoFactorService $twoFactor) {}
public function execute(Request $request, string $challengeToken, string $code): User
{
$cacheKey = "2fa_challenge:{$challengeToken}";
$challenge = Cache::get($cacheKey);
if (! $challenge || $challenge['ip'] !== $request->ip()) {
throw ValidationException::withMessages([
'challenge_token' => 'This challenge has expired. Please log in again.',
]);
}
if ($challenge['attempts'] >= 5) {
Cache::forget($cacheKey);
throw ValidationException::withMessages([
'code' => 'Too many failed attempts. Please log in again.',
]);
}
$user = User::findOrFail($challenge['user_id']);
$valid = $this->twoFactor->verifyCode($user->two_factor_secret, $code)
|| $this->twoFactor->redeemRecoveryCode($user, $code);
if (! $valid) {
$challenge['attempts']++;
Cache::put($cacheKey, $challenge, now()->addMinutes(5));
throw ValidationException::withMessages([
'code' => 'That code is invalid.',
]);
}
Cache::forget($cacheKey);
Auth::login($user);
$request->session()->regenerate();
return $user;
}
}