clusev/app/Livewire/Settings/WebauthnKeys.php

109 lines
3.5 KiB
PHP

<?php
namespace App\Livewire\Settings;
use App\Models\AuditEvent;
use App\Services\WebauthnService;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\On;
use Livewire\Attributes\Validate;
use Livewire\Component;
class WebauthnKeys extends Component
{
#[Validate('required|string|max:60')]
public string $newName = '';
/** JSON creation options for navigator.credentials.create (the JS posts the result to register). */
public function options(WebauthnService $webauthn): array
{
abort_unless($webauthn->available(), 404);
// Validate the label BEFORE the browser ceremony, so an invalid name never
// leaves an orphaned credential on the authenticator.
$this->validate();
return $webauthn->registrationOptions(Auth::user());
}
public function register(array $attestation, WebauthnService $webauthn): void
{
// WebAuthn must be available (domain + HTTPS). No existing-factor guard — a key may
// be the FIRST/only factor.
abort_unless($webauthn->available(), 404);
$this->validate();
$cred = $webauthn->verifyRegistration(Auth::user(), $attestation, trim($this->newName));
$this->reset('newName');
// First factor enrolled and no codes yet → generate + show the recovery modal.
if (! Auth::user()->hasRecoveryCodes()) {
Auth::user()->replaceRecoveryCodes();
$this->dispatch('openModal', component: 'modals.recovery-codes');
}
AuditEvent::create([
'user_id' => Auth::id(),
'actor' => Auth::user()->name ?? 'system',
'action' => 'webauthn.register',
'target' => $cred->name,
'ip' => request()->ip(),
]);
$this->dispatch('notify', message: __('auth.webauthn_added'));
}
/** Destructive (R5): open the confirm modal, which re-dispatches `webauthnKeyRemoved`. */
public function confirmRemove(int $id): void
{
$cred = Auth::user()->webauthnCredentials()->whereKey($id)->first();
if (! $cred) {
return;
}
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
'heading' => __('auth.webauthn_title'),
'body' => __('auth.webauthn_remove_confirm'),
'confirmLabel' => __('common.remove'),
'danger' => true,
'icon' => 'trash',
'event' => 'webauthnKeyRemoved',
'params' => ['id' => $id],
'notify' => '',
],
);
}
#[On('webauthnKeyRemoved')]
public function remove(int $id, WebauthnService $webauthn): void
{
abort_unless($webauthn->available(), 404);
$cred = Auth::user()->webauthnCredentials()->whereKey($id)->first();
if ($cred) {
$name = $cred->name;
$cred->delete();
AuditEvent::create([
'user_id' => Auth::id(),
'actor' => Auth::user()->name ?? 'system',
'action' => 'webauthn.remove',
'target' => $name,
'ip' => request()->ip(),
]);
$this->dispatch('notify', message: __('auth.webauthn_removed'));
Auth::user()->resetIfNoFactor();
}
}
public function render()
{
return view('livewire.settings.webauthn-keys', [
'available' => app(WebauthnService::class)->available(),
'keys' => Auth::user()->webauthnCredentials()->latest()->get(),
]);
}
}