components([ Hidden::make('email') ->rules(fn (): array => $this->shouldShowPinLogin() ? ['required', 'email'] : []) ->visible(fn (): bool => $this->shouldShowPinLogin()) ->dehydrated(fn (): bool => $this->shouldShowPinLogin()) ->live(), Hidden::make('pin') ->rules(fn (): array => $this->shouldShowPinLogin() ? [ 'nullable', 'min:4', "max:{$this->maxPinLength}", 'regex:/^\\d+$/', ] : []) ->visible(fn (): bool => $this->shouldShowPinLogin()) ->dehydrated(fn (): bool => $this->shouldShowPinLogin()) ->live(), $this->getEmailFormComponent(), $this->getPasswordFormComponent(), $this->getRememberFormComponent(), View::make('filament.pages.auth.kiosk-login') ->viewData([ 'hasPinUsers' => $this->shouldShowPinLogin(), 'users' => $this->getKioskUsers(), 'maxPinLength' => $this->maxPinLength, ]) ->columnSpanFull(), ]); } public function authenticate(): ?LoginResponse { try { $this->rateLimit(5); } catch (TooManyRequestsException $exception) { $this->getRateLimitedNotification($exception)?->send(); return null; } $data = $this->form->getState(); if (blank($data['pin'] ?? null)) { return parent::authenticate(); } $authGuard = Filament::auth(); $credentials = [ 'email' => $data['email'] ?? null, 'password' => '', ]; $user = User::query() ->where('email', $data['email'] ?? '') ->first(); $pin = (string) ($data['pin'] ?? ''); if ( (! $user) || blank($user->admin_pin_hash) || $pin === '' || (! ctype_digit($pin)) || (strlen($pin) < 4 || strlen($pin) > $this->maxPinLength) || (! Hash::check($pin, $user->admin_pin_hash)) ) { $this->userUndertakingMultiFactorAuthentication = null; $this->fireFailedEvent($authGuard, $user, $credentials); $this->throwFailureValidationException(); } if ( filled($this->userUndertakingMultiFactorAuthentication) && (decrypt($this->userUndertakingMultiFactorAuthentication) === $user->getAuthIdentifier()) ) { $this->multiFactorChallengeForm->validate(); } else { foreach (Filament::getMultiFactorAuthenticationProviders() as $multiFactorAuthenticationProvider) { if (! $multiFactorAuthenticationProvider->isEnabled($user)) { continue; } $this->userUndertakingMultiFactorAuthentication = encrypt($user->getAuthIdentifier()); if ($multiFactorAuthenticationProvider instanceof \Filament\Auth\MultiFactor\Contracts\HasBeforeChallengeHook) { $multiFactorAuthenticationProvider->beforeChallenge($user); } break; } if (filled($this->userUndertakingMultiFactorAuthentication)) { $this->multiFactorChallengeForm->fill(); return null; } } if ($user instanceof FilamentUser) { if (! $user->canAccessPanel(Filament::getCurrentOrDefaultPanel())) { $this->fireFailedEvent($authGuard, $user, $credentials); $this->throwFailureValidationException(); } } $authGuard->login($user, $data['remember'] ?? true); session()->regenerate(); return app(LoginResponse::class); } public function selectUser(int $userId): void { $user = $this->getKioskUsers()->firstWhere('id', $userId); if (! $user) { return; } $this->data['email'] = $user->email; $this->data['pin'] = ''; } public function appendPinDigit(int $digit): void { $pin = (string) ($this->data['pin'] ?? ''); if (strlen($pin) >= $this->maxPinLength) { return; } $this->data['pin'] = $pin.$digit; } public function deletePinDigit(): void { $pin = (string) ($this->data['pin'] ?? ''); if ($pin === '') { return; } $this->data['pin'] = substr($pin, 0, -1); } public function clearPin(): void { $this->data['pin'] = ''; } /** * @return \Illuminate\Support\Collection */ protected function getKioskUsers(): Collection { return User::query() ->whereNotNull('admin_pin_hash') ->orderBy('name') ->get(['id', 'name', 'email', 'admin_pin_hash']); } protected function shouldShowPinLogin(): bool { return $this->getKioskUsers()->isNotEmpty(); } protected function getPasswordFormComponent(): Component { return parent::getPasswordFormComponent() ->required(fn (): bool => blank($this->data['pin'] ?? null)); } }