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,53 @@
<?php
namespace App\Services\Auth;
use App\Models\User;
use App\Support\PasswordPolicy;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
/**
* Управление сменой пароля с ведением истории (меры ИАФ.3).
*/
class PasswordManager
{
/**
* Установить новый пароль: сохранить старый в историю, обновить метку
* времени смены и подрезать историю до лимита.
*/
public function change(User $user, string $newPassword): void
{
DB::transaction(function () use ($user, $newPassword): void {
// Сохраняем текущий пароль в историю до перезаписи.
if ($user->password) {
$user->passwordHistories()->create([
'password_hash' => $user->password,
'created_at' => now(),
]);
}
$user->forceFill([
'password' => Hash::make($newPassword),
'password_changed_at' => now(),
])->save();
$this->trimHistory($user);
});
}
private function trimHistory(User $user): void
{
$limit = PasswordPolicy::historyLimit();
$ids = $user->passwordHistories()
->orderByDesc('created_at')
->skip($limit)
->take(PHP_INT_MAX)
->pluck('id');
if ($ids->isNotEmpty()) {
$user->passwordHistories()->whereIn('id', $ids)->delete();
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Services\Auth;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use PragmaRX\Google2FA\Google2FA;
/**
* Многофакторная аутентификация по TOTP (мера ИАФ.4).
*
* Секрет и резервные коды хранятся в зашифрованном виде (ЗНИ) поля помечены
* как encrypted в модели User. Резервные коды дополнительно хешируются.
*/
class TwoFactorService
{
public function __construct(private readonly Google2FA $engine) {}
public function generateSecret(): string
{
return $this->engine->generateSecretKey();
}
/**
* URI для QR-кода (otpauth://) добавляется в приложение-аутентификатор.
*/
public function provisioningUri(User $user, string $secret): string
{
return $this->engine->getQRCodeUrl(
(string) config('app.name'),
(string) $user->email,
$secret,
);
}
public function verify(string $secret, string $code): bool
{
// verifyKey возвращает позицию окна (int) при успехе либо false.
return $this->engine->verifyKey($secret, $code) !== false;
}
/**
* Сгенерировать набор одноразовых резервных кодов.
*
* @return array<int, string>
*/
public function generateRecoveryCodes(): array
{
$count = (int) config('security.mfa.recovery_codes');
return Collection::times($count, fn () => Str::upper(Str::random(5).'-'.Str::random(5)))
->all();
}
/**
* @param array<int, string> $codes
* @return array<int, string>
*/
public function hashRecoveryCodes(array $codes): array
{
return array_map(static fn (string $code) => Hash::make($code), $codes);
}
/**
* Проверить и «погасить» резервный код. Возвращает новый список хешей или
* null, если код не подошёл.
*
* @param array<int, string> $hashedCodes
* @return array<int, string>|null
*/
public function consumeRecoveryCode(array $hashedCodes, string $input): ?array
{
foreach ($hashedCodes as $index => $hash) {
if (Hash::check($input, $hash)) {
unset($hashedCodes[$index]);
return array_values($hashedCodes);
}
}
return null;
}
}