first commit

This commit is contained in:
brusnitsyn
2026-06-24 17:20:43 +09:00
commit 43499acf1c
165 changed files with 25929 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Facades\Audit;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
/**
* Вход и выход из системы (меры ИАФ.1, ИАФ.5, ИАФ.6, РСБ.2).
*/
class AuthenticatedSessionController extends Controller
{
public function create(): Response
{
return Inertia::render('auth/Login');
}
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
// Блокировка учётной записи администратором (УПД.1).
if ($request->user()->isBlocked()) {
Auth::logout();
Audit::log('auth.login.failed', 'login', 'User:'.$request->user()->id, 'failure', ['reason' => 'blocked']);
return back()->withErrors(['email' => 'Учётная запись заблокирована.']);
}
// Регенерация идентификатора сессии после аутентификации (ИАФ.5).
$request->session()->regenerate();
// Сбрасываем отметку прохождения второго фактора — потребуется заново.
$request->session()->forget('mfa.verified');
$request->user()->forceFill(['last_login_at' => now()])->saveQuietly();
return redirect()->intended(route('dashboard', absolute: false));
}
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Facades\Audit;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\ChangePasswordRequest;
use App\Services\Auth\PasswordManager;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
/**
* Смена пароля пользователем, в т.ч. принудительная по истечении срока.
*
* Меры ФСТЭК: ИАФ.3 (управление аутентификационной информацией), РСБ.2.
*/
class PasswordController extends Controller
{
public function __construct(private readonly PasswordManager $passwords) {}
/**
* Экран принудительной смены пароля (срок истёк).
*/
public function expired(): Response
{
return Inertia::render('auth/PasswordExpired');
}
public function update(ChangePasswordRequest $request): RedirectResponse
{
$this->passwords->change($request->user(), $request->string('password'));
Audit::log('auth.password.changed', 'change', 'User:'.$request->user()->id);
return redirect()->route('dashboard')->with('status', 'Пароль обновлён.');
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Facades\Audit;
use App\Http\Controllers\Controller;
use App\Services\Auth\TwoFactorService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
/**
* Многофакторная аутентификация по TOTP (мера ИАФ.4).
*
* Поток:
* setup -> показать секрет/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();
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}