187 lines
10 KiB
PHP
187 lines
10 KiB
PHP
<div @class(['space-y-6', 'mx-auto max-w-3xl' => ! $embedded])>
|
|
{{-- The state belongs beside the title, not floating inside the card: it is
|
|
a property of the account, not of whichever step is on screen.
|
|
|
|
As a tab of the settings page the title steps down to an h2: the page
|
|
already has its own, and two <h1> elements is two page titles. The
|
|
state badge stays either way — it is the first thing somebody opening
|
|
this comes to find out. --}}
|
|
<div class="flex flex-wrap items-start justify-between gap-4 animate-rise">
|
|
<div>
|
|
@if ($embedded)
|
|
<h2 class="text-lg font-bold tracking-tight text-ink">{{ __('two_factor_setup.title') }}</h2>
|
|
@else
|
|
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('two_factor_setup.title') }}</h1>
|
|
@endif
|
|
<p class="mt-1 max-w-xl text-sm text-muted">{{ __('two_factor_setup.sub') }}</p>
|
|
</div>
|
|
|
|
<span class="inline-flex shrink-0 items-center gap-1.5 rounded-pill border px-2.5 py-1 text-xs font-medium
|
|
{{ $twoFactorOn ? 'border-success-border bg-success-bg text-success' : 'border-line-strong bg-surface-2 text-muted' }}">
|
|
<span class="size-1.5 rounded-pill bg-current" aria-hidden="true"></span>
|
|
{{ $twoFactorOn ? __('two_factor_setup.state_on') : __('two_factor_setup.state_off') }}
|
|
</span>
|
|
</div>
|
|
|
|
{{-- Every action below is re-checked server-side against a recent password
|
|
confirmation — hiding a button stops nobody who can post to
|
|
/livewire/update. --}}
|
|
|
|
@if (! $passwordConfirmed)
|
|
{{-- The gate. Being signed in is not enough to change how the account is
|
|
protected: the realistic threat is an unlocked machine. --}}
|
|
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
|
|
<div class="flex gap-4">
|
|
<span class="grid size-10 shrink-0 place-items-center rounded-lg bg-surface-2 text-muted" aria-hidden="true">
|
|
<x-ui.icon name="lock" class="size-5" />
|
|
</span>
|
|
<div class="min-w-0 flex-1">
|
|
<h2 class="font-semibold text-ink">{{ __('two_factor_setup.locked_title') }}</h2>
|
|
<p class="mt-1 max-w-lg text-sm text-muted">{{ __('two_factor_setup.confirm_first') }}</p>
|
|
|
|
<form wire:submit="confirmPassword" class="mt-4 flex flex-wrap items-start gap-2">
|
|
<div class="min-w-56 flex-1">
|
|
<x-ui.input name="confirmablePassword" type="password" autocomplete="current-password"
|
|
:label="__('admin_settings.password_current')" wire:model="confirmablePassword" />
|
|
</div>
|
|
<x-ui.button type="submit" variant="primary" class="mt-7"
|
|
wire:loading.attr="disabled" wire:target="confirmPassword">
|
|
{{ __('two_factor_setup.confirm_button') }}
|
|
</x-ui.button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@elseif ($twoFactorOn)
|
|
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
|
|
<div class="flex gap-4">
|
|
<span class="grid size-10 shrink-0 place-items-center rounded-lg bg-success-bg text-success" aria-hidden="true">
|
|
<x-ui.icon name="shield-check" class="size-5" />
|
|
</span>
|
|
<div class="min-w-0 flex-1">
|
|
<h2 class="font-semibold text-ink">{{ __('two_factor_setup.active_title') }}</h2>
|
|
<p class="mt-1 max-w-lg text-sm text-muted">{{ __('two_factor_setup.active_body') }}</p>
|
|
|
|
<div class="mt-5 flex flex-wrap gap-2">
|
|
<x-ui.button variant="secondary" wire:click="regenerateRecoveryCodes"
|
|
wire:loading.attr="disabled" wire:target="regenerateRecoveryCodes">
|
|
<x-ui.icon name="rotate-ccw" class="size-4" />{{ __('two_factor_setup.new_codes') }}
|
|
</x-ui.button>
|
|
<x-ui.button variant="danger"
|
|
x-on:click="$dispatch('openModal', { component: 'admin.confirm-disable-two-factor' })">
|
|
{{ __('two_factor_setup.disable') }}
|
|
</x-ui.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@elseif ($twoFactorPending)
|
|
{{-- The step that had no way out. The secret exists but is unconfirmed,
|
|
so "Abbrechen" genuinely clears it rather than leaving a dangling
|
|
enrolment that nothing else on the console would ever show. --}}
|
|
<div class="rounded-lg border border-line bg-surface shadow-xs animate-rise">
|
|
<div class="flex items-center justify-between gap-4 border-b border-line px-6 py-4">
|
|
<h2 class="font-semibold text-ink">{{ __('two_factor_setup.scan_title') }}</h2>
|
|
<span class="font-mono text-xs text-muted">{{ __('two_factor_setup.step_two') }}</span>
|
|
</div>
|
|
|
|
<div class="grid gap-6 p-6 sm:grid-cols-[auto_1fr] sm:items-start">
|
|
<div class="mx-auto rounded-lg border border-line-strong bg-white p-3 sm:mx-0">
|
|
{!! $twoFactorQr !!}
|
|
</div>
|
|
|
|
<div class="min-w-0">
|
|
<ol class="space-y-2 text-sm text-body">
|
|
<li class="flex gap-2">
|
|
<span class="font-mono text-muted" aria-hidden="true">1.</span>
|
|
<span>{{ __('two_factor_setup.scan_step_open') }}</span>
|
|
</li>
|
|
<li class="flex gap-2">
|
|
<span class="font-mono text-muted" aria-hidden="true">2.</span>
|
|
<span>{{ __('two_factor_setup.scan_step_scan') }}</span>
|
|
</li>
|
|
<li class="flex gap-2">
|
|
<span class="font-mono text-muted" aria-hidden="true">3.</span>
|
|
<span>{{ __('two_factor_setup.scan_step_code') }}</span>
|
|
</li>
|
|
</ol>
|
|
|
|
<form wire:submit="confirmTwoFactor" class="mt-5">
|
|
<div class="max-w-48">
|
|
<x-ui.input name="twoFactorCode" inputmode="numeric" autocomplete="one-time-code"
|
|
:label="__('two_factor_setup.code')" wire:model="twoFactorCode" />
|
|
</div>
|
|
|
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
<x-ui.button type="submit" variant="primary"
|
|
wire:loading.attr="disabled" wire:target="confirmTwoFactor">
|
|
{{ __('two_factor_setup.activate') }}
|
|
</x-ui.button>
|
|
<x-ui.button type="button" variant="ghost" wire:click="cancelSetup"
|
|
wire:loading.attr="disabled" wire:target="cancelSetup">
|
|
<x-ui.icon name="arrow-left" class="size-4" />{{ __('two_factor_setup.cancel') }}
|
|
</x-ui.button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@else
|
|
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
|
|
<div class="flex gap-4">
|
|
<span class="grid size-10 shrink-0 place-items-center rounded-lg bg-surface-2 text-muted" aria-hidden="true">
|
|
<x-ui.icon name="shield" class="size-5" />
|
|
</span>
|
|
<div class="min-w-0 flex-1">
|
|
<h2 class="font-semibold text-ink">{{ __('two_factor_setup.not_set_title') }}</h2>
|
|
<p class="mt-1 max-w-lg text-sm text-muted">{{ __('two_factor_setup.not_set_body') }}</p>
|
|
<p class="mt-4 text-xs text-muted">{{ __('two_factor_setup.need_app') }}</p>
|
|
|
|
<x-ui.button variant="primary" class="mt-5" wire:click="enableTwoFactor"
|
|
wire:loading.attr="disabled" wire:target="enableTwoFactor">
|
|
{{ __('two_factor_setup.enable') }}
|
|
</x-ui.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($recoveryCodes)
|
|
{{-- Its own card, not a footnote under another one: these are the only
|
|
way back in once the phone is gone, and they are shown exactly once. --}}
|
|
<div class="rounded-lg border border-warning-border bg-warning-bg p-6 animate-rise"
|
|
x-data="{
|
|
copied: false,
|
|
copy() {
|
|
const text = [...$refs.codes.querySelectorAll('li')].map(n => n.textContent.trim()).join('\n');
|
|
if (!navigator.clipboard) return;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
this.copied = true;
|
|
setTimeout(() => { this.copied = false; }, 2000);
|
|
});
|
|
},
|
|
}">
|
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
<div>
|
|
<h2 class="flex items-center gap-2 font-semibold text-warning">
|
|
<x-ui.icon name="alert-triangle" class="size-4" />{{ __('two_factor_setup.codes_title') }}
|
|
</h2>
|
|
<p class="mt-1 max-w-lg text-xs text-warning">{{ __('two_factor_setup.codes_hint') }}</p>
|
|
</div>
|
|
|
|
<x-ui.button type="button" variant="secondary" size="sm" x-on:click="copy()">
|
|
<x-ui.icon name="copy" class="size-4" />
|
|
<span x-text="copied ? @js(__('two_factor_setup.codes_copied')) : @js(__('two_factor_setup.copy_codes'))"></span>
|
|
</x-ui.button>
|
|
</div>
|
|
|
|
<ul x-ref="codes" class="mt-4 grid grid-cols-2 gap-x-6 gap-y-1.5 font-mono text-sm text-ink sm:grid-cols-4">
|
|
@foreach ($recoveryCodes as $code)<li class="select-all">{{ $code }}</li>@endforeach
|
|
</ul>
|
|
</div>
|
|
@endif
|
|
</div>
|