mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 00:55:07 +00:00
495f3ae471
A role now has a start page: the dashboard, files, upload, groups, clients or the activity log (the last two for staff only). Anyone can override their role's choice in their profile. The administrator role takes a start page too, while everything else about it stays locked. A choice is only used if the account can open that page now. Otherwise the next one down is tried, ending at the dashboard, so a permission removed later never lands somebody on a 403. A role cannot be saved with a start page its own permissions block. A link followed before signing in still wins, and a waiting getting-started or what's-new page still goes first. Applies to password, two-factor and provider sign-ins, and to the site root for someone already signed in. StartPageTest opens every page for real, with and without its permission, so the enum cannot drift from the routes. Requested by @Zodiac1978 in #1777.
98 lines
3.0 KiB
PHP
98 lines
3.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Identity\Http\Controllers;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\User;
|
|
use App\Modules\Identity\SignIn;
|
|
use App\Modules\Identity\StartPages;
|
|
use App\Modules\Identity\TwoFactor\TwoFactorService;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
/**
|
|
* Second step of login for accounts with 2FA: the password was already
|
|
* verified, the pending user id waits in the session until a valid TOTP
|
|
* or recovery code arrives.
|
|
*/
|
|
class TwoFactorChallengeController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly TwoFactorService $twoFactor,
|
|
private readonly StartPages $startPages,
|
|
) {}
|
|
|
|
public function create(Request $request): Response|RedirectResponse
|
|
{
|
|
if ($this->pendingUser($request) === null) {
|
|
return redirect()->route('login');
|
|
}
|
|
|
|
return Inertia::render('auth/two-factor-challenge');
|
|
}
|
|
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$user = $this->pendingUser($request);
|
|
|
|
if ($user === null) {
|
|
return redirect()->route('login');
|
|
}
|
|
|
|
$request->validate([
|
|
'code' => ['nullable', 'string'],
|
|
'recovery_code' => ['nullable', 'string'],
|
|
]);
|
|
|
|
$throttleKey = 'two-factor.challenge.'.$user->id;
|
|
|
|
if (RateLimiter::tooManyAttempts($throttleKey, 5)) {
|
|
throw ValidationException::withMessages([
|
|
'code' => __('auth.throttle', ['seconds' => (string) RateLimiter::availableIn($throttleKey)]),
|
|
]);
|
|
}
|
|
|
|
$valid = $request->filled('code')
|
|
? $this->twoFactor->verify($user, (string) $request->string('code'))
|
|
: ($request->filled('recovery_code')
|
|
&& $this->twoFactor->consumeRecoveryCode($user, trim((string) $request->string('recovery_code'))));
|
|
|
|
if (! $valid) {
|
|
RateLimiter::hit($throttleKey);
|
|
|
|
throw ValidationException::withMessages([
|
|
'code' => __('The provided two-factor authentication code was invalid.'),
|
|
]);
|
|
}
|
|
|
|
RateLimiter::clear($throttleKey);
|
|
|
|
Auth::login($user, (bool) $request->session()->pull(SignIn::TWO_FACTOR_REMEMBER, false));
|
|
|
|
$request->session()->forget(SignIn::TWO_FACTOR_ID);
|
|
$request->session()->regenerate();
|
|
|
|
return redirect()->intended($this->startPages->pathFor($user));
|
|
}
|
|
|
|
private function pendingUser(Request $request): ?User
|
|
{
|
|
$id = $request->session()->get(SignIn::TWO_FACTOR_ID);
|
|
|
|
if (! is_int($id) && ! is_string($id)) {
|
|
return null;
|
|
}
|
|
|
|
$user = User::query()->find($id);
|
|
|
|
return $user instanceof User && $user->maySignIn() && $user->hasTwoFactorEnabled() ? $user : null;
|
|
}
|
|
}
|