показать секрет/QR (сохраняется во временной сессии); * enable -> подтвердить кодом, включить MFA, выдать резервные коды; * challenge/verify -> ввод кода при каждом входе; * disable -> отключение (требует подтверждения паролем — см. маршрут). */ class TwoFactorController extends Controller { public function __construct(private readonly TwoFactorService $twoFactor) {} public function setup(Request $request): Response { $secret = $request->session()->get('mfa.pending_secret') ?? $this->twoFactor->generateSecret(); $request->session()->put('mfa.pending_secret', $secret); return Inertia::render('auth/TwoFactorSetup', [ 'secret' => $secret, 'qr' => $this->twoFactor->provisioningUri($request->user(), $secret), ]); } public function enable(Request $request): RedirectResponse { $request->validate(['code' => ['required', 'string']]); $secret = (string) $request->session()->get('mfa.pending_secret'); if ($secret === '' || ! $this->twoFactor->verify($secret, $request->string('code'))) { throw ValidationException::withMessages(['code' => 'Неверный код подтверждения.']); } $recoveryCodes = $this->twoFactor->generateRecoveryCodes(); $user = $request->user(); $user->mfa_secret = $secret; $user->setRecoveryCodes($this->twoFactor->hashRecoveryCodes($recoveryCodes)); $user->mfa_confirmed_at = now(); $user->save(); $request->session()->forget('mfa.pending_secret'); $request->session()->put('mfa.verified', true); Audit::log('auth.mfa.enabled', 'enable', 'User:'.$user->id); // Резервные коды показываем один раз — пользователь обязан их сохранить. return redirect()->route('dashboard')->with('recoveryCodes', $recoveryCodes); } public function challenge(): Response { return Inertia::render('auth/TwoFactorChallenge'); } public function verify(Request $request): RedirectResponse { $request->validate(['code' => ['required', 'string']]); $user = $request->user(); $code = $request->string('code'); if ($this->twoFactor->verify((string) $user->mfa_secret, $code)) { $request->session()->put('mfa.verified', true); return redirect()->intended(route('dashboard', absolute: false)); } // Попытка по резервному коду. $remaining = $this->twoFactor->consumeRecoveryCode($user->recoveryCodes(), $code); if ($remaining !== null) { $user->setRecoveryCodes($remaining); $user->save(); $request->session()->put('mfa.verified', true); return redirect()->intended(route('dashboard', absolute: false)); } Audit::log('auth.mfa.failed', 'verify', 'User:'.$user->id, 'failure'); throw ValidationException::withMessages(['code' => 'Неверный код.']); } public function disable(Request $request): RedirectResponse { $request->validate(['password' => ['required', 'current_password']]); $user = $request->user(); $user->forceFill([ 'mfa_secret' => null, 'mfa_recovery_codes' => null, 'mfa_confirmed_at' => null, ])->save(); $request->session()->forget('mfa.verified'); Audit::log('auth.mfa.disabled', 'disable', 'User:'.$user->id); return back(); } }