CluPilotCloud/app/Livewire/Auth/OperatorLogin.php

104 lines
3.7 KiB
PHP

<?php
namespace App\Livewire\Auth;
use App\Models\Operator;
use App\Support\AdminArea;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Component;
/**
* The console's own sign-in.
*
* Not Fortify: Fortify binds to exactly one guard, and hanging both the portal
* and the console off it would be the shared login this separation exists to
* end. The two-factor MECHANICS are still Fortify's — the same
* TwoFactorAuthenticatable trait and the same TOTP provider — only the flow is
* ours.
*/
#[Layout('layouts.portal')]
class OperatorLogin extends Component
{
public string $email = '';
public string $password = '';
public bool $remember = false;
public function authenticate(): void
{
// Rules on the action: a #[Validate] attribute on a property applies
// class-wide and would be dragged into any other action added later.
//
// password is NOT 'required': a failed attempt resets it below, and a
// retry (or a brute-force loop) submitting on top of that empty value
// must still count against the rate limiter instead of stopping short
// at a validation error keyed 'password' — which would never accumulate
// toward the throttle at all, five wrong-password submissions in a row
// would never actually throttle. An empty password fails Hash::check()
// like any other wrong one and gets the same generic message.
$this->validate([
'email' => ['required', 'email'],
'password' => ['string'],
]);
$key = 'operator-login:'.mb_strtolower($this->email).'|'.request()->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
throw ValidationException::withMessages([
'email' => __('auth.throttle', ['seconds' => RateLimiter::availableIn($key)]),
]);
}
$operator = Operator::where('email', $this->email)->first();
// One message for every failure — a different message for "no such
// account" tells a stranger which addresses are operators.
if ($operator === null
|| ! Hash::check($this->password, $operator->password)
|| ! $operator->isActive()
|| ! $operator->isOperator()) {
RateLimiter::hit($key, 60);
$this->reset('password');
throw ValidationException::withMessages(['email' => __('auth.failed')]);
}
RateLimiter::clear($key);
if ($operator->two_factor_confirmed_at !== null) {
// Not signed in yet. The id is parked in the session and the guard
// stays untouched until the code checks out.
session(['operator.2fa.id' => $operator->id, 'operator.2fa.remember' => $this->remember]);
$this->redirectRoute('admin.two-factor', navigate: false);
return;
}
$this->completeLoginAndRedirect($operator, $this->remember);
}
/** Shared with the two-factor challenge, so both exits behave identically. */
public static function completeLogin(Operator $operator, bool $remember): void
{
Auth::guard('operator')->login($operator, $remember);
session()->regenerate();
$operator->forceFill(['last_login_at' => now()])->save();
}
private function completeLoginAndRedirect(Operator $operator, bool $remember): void
{
self::completeLogin($operator, $remember);
$this->redirect(AdminArea::home(), navigate: false);
}
public function render()
{
return view('livewire.auth.operator-login');
}
}