latest
This commit is contained in:
1 parent
d0834df8df
commit
6dc35134fa
171 files changed
+19241
-49
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Users;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class AssignRoleAction
|
||||
{
|
||||
public function execute(User $user, string $role): User
|
||||
{
|
||||
$user->syncRoles([$role]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Users;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class CreateUserAction
|
||||
{
|
||||
/**
|
||||
* @param array{name: string, username: string, email: string, password: string, role: string} $data
|
||||
*/
|
||||
public function execute(array $data, User $actor): User
|
||||
{
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
'password' => $data['password'],
|
||||
'created_by' => $actor->id,
|
||||
]);
|
||||
|
||||
// The DB default (false) applies, but isn't hydrated back onto this
|
||||
// in-memory instance after insert — set it explicitly for the
|
||||
// response/event payload built from this same object.
|
||||
$user->two_factor_enabled = false;
|
||||
|
||||
$user->assignRole($data['role']);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Users;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class DeleteUserAction
|
||||
{
|
||||
public function execute(User $user): void
|
||||
{
|
||||
$user->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Users;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UpdateUserAction
|
||||
{
|
||||
/**
|
||||
* @param array{name?: string, username?: string, email?: string, password?: string} $data
|
||||
*/
|
||||
public function execute(User $user, array $data): User
|
||||
{
|
||||
$user->fill(array_filter($data, fn ($value) => $value !== null && $value !== ''));
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user