CluPilotCloud/resources/views/livewire/admin/two-factor-setup.blade.php

178 lines
9.8 KiB
PHP

<div class="mx-auto max-w-3xl space-y-6">
{{-- 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. --}}
<div class="flex flex-wrap items-start justify-between gap-4 animate-rise">
<div>
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('two_factor_setup.title') }}</h1>
<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>