- TallStackUI 4 (MIT) statt Flux; Komponenten mit Präfix ts- (<x-ts-…>) - Layouts, Login-, Passwort-, 2FA- und Einstellungsseiten neu aufgebaut - Livewire-Skripte explizit in beiden Layouts, damit Alpine auch auf den Fortify-Seiten ohne Livewire-Komponente läuft - Dark Mode über tallstackui_darkTheme(), ohne Aufblitzen beim Laden - Registrierungs-View und ungenutzte Layout-Varianten entfernt - Test: Seiten laden keine Ressourcen von fremden Hosts (Vorgabe "alles lokal") Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
3.7 KiB
PHP
104 lines
3.7 KiB
PHP
@assets
|
|
@vite('resources/js/passkeys.js')
|
|
@endassets
|
|
|
|
<div
|
|
x-data="{
|
|
supported: false,
|
|
showForm: false,
|
|
name: '',
|
|
loading: false,
|
|
error: null,
|
|
updateSupport() {
|
|
this.supported = Boolean(window.Passkeys?.isSupported());
|
|
},
|
|
getDefaultPasskeyName() {
|
|
const ua = navigator.userAgent;
|
|
|
|
const browser = [
|
|
{ pattern: /Edg|Edge/, name: 'Edge' },
|
|
{ pattern: /OPR|Opera|OPiOS/, name: 'Opera' },
|
|
{ pattern: /Firefox|FxiOS/, name: 'Firefox' },
|
|
{ pattern: /Chrome|CriOS/, name: 'Chrome' },
|
|
{ pattern: /Safari/, name: 'Safari' },
|
|
].find(({ pattern }) => pattern.test(ua))?.name;
|
|
|
|
const os = [
|
|
{ pattern: /iPhone/, name: 'iPhone' },
|
|
{ pattern: /iPad|Macintosh(?=.*Mobile)/, name: 'iPad' },
|
|
{ pattern: /Android/, name: 'Android' },
|
|
{ pattern: /Mac/, name: 'Mac' },
|
|
{ pattern: /Windows/, name: 'Windows' },
|
|
].find(({ pattern }) => pattern.test(ua))?.name;
|
|
|
|
return [browser, os].filter(Boolean).join(' on ') || '';
|
|
},
|
|
init() {
|
|
this.name = this.getDefaultPasskeyName();
|
|
this.updateSupport();
|
|
|
|
window.addEventListener('passkeys:ready', () => this.updateSupport(), { once: true });
|
|
},
|
|
async register() {
|
|
if (!this.name.trim()) return;
|
|
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
await window.Passkeys.register({ name: this.name });
|
|
this.name = '';
|
|
this.showForm = false;
|
|
await $wire.loadPasskeys();
|
|
} catch (e) {
|
|
if (e.constructor?.name !== 'UserCancelledError') {
|
|
this.error = e.message;
|
|
}
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
cancel() {
|
|
this.showForm = false;
|
|
this.name = '';
|
|
this.error = null;
|
|
},
|
|
}"
|
|
>
|
|
<template x-if="!supported">
|
|
<p class="text-sm text-gray-600 dark:text-dark-300">{{ __('Passkeys are not supported in this browser.') }}</p>
|
|
</template>
|
|
|
|
<template x-if="supported && !showForm">
|
|
<div>
|
|
<x-ts-button icon="plus" x-on:click="showForm = true">
|
|
{{ __('Add passkey') }}
|
|
</x-ts-button>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="supported && showForm">
|
|
<div class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700/50">
|
|
<x-ts-input :label="__('Passkey name')"
|
|
x-model="name"
|
|
:placeholder="__('e.g., MacBook Pro, iPhone')"
|
|
x-on:keydown.enter.prevent="register()"
|
|
x-ref="passkeyNameInput"
|
|
x-init="$nextTick(() => $refs.passkeyNameInput?.focus())"
|
|
:hint="__('Give this passkey a name to help you identify it later.')" />
|
|
|
|
<p x-show="error" x-text="error" x-cloak class="text-sm text-red-600 dark:text-red-400"></p>
|
|
|
|
<div class="flex gap-2">
|
|
<x-ts-button x-on:click="register()" x-bind:disabled="loading || !name.trim()">
|
|
<span x-show="!loading">{{ __('Register passkey') }}</span>
|
|
<span x-show="loading" x-cloak>{{ __('Registering...') }}</span>
|
|
</x-ts-button>
|
|
<x-ts-button flat color="secondary" x-on:click="cancel()">
|
|
{{ __('Cancel') }}
|
|
</x-ts-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|