@extends("layouts.app")
@section("title", "Security")
@section("content")
@include("partials.errors")
<div class="mx-auto max-w-3xl space-y-6">
{{-- Authenticator app --}}
<div class="card">
<div class="card-header flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="font-semibold">Authenticator app</h3>
<p class="mt-0.5 text-sm text-ink-500 dark:text-ink-400">A six-digit code from an app on your phone.</p>
</div>
@if(totp_enabled)<span class="badge-success">On</span>@else<span class="badge-neutral">Off</span>@endif
</div>
<div class="card-body">
@if(totp_enabled)
<p class="text-sm text-ink-600 dark:text-ink-400">Enabled {{ totp_enabled_at }}.</p>
<form method="post" action="/settings/security/totp/disable" class="mt-4"
data-confirm="Turn off the authenticator app? Your account will be protected by a password alone.">
{!! csrf_field !!}
<div class="max-w-sm">
<label for="totp_password" class="field-label">Confirm with your password</label>
<input id="totp_password" name="password" type="password" required autocomplete="current-password" class="field-input">
</div>
<button type="submit" class="btn-danger mt-4">Turn off</button>
</form>
@elseif(totp_pending)
<div class="grid gap-6 sm:grid-cols-[auto_1fr]">
<div class="mx-auto">
{{-- The QR is rendered server-side as SVG. Loading a JavaScript
library would need a looser CSP, and a chart API would mean
posting the secret to a third party. --}}
<div class="rounded-lg border border-ink-200 bg-white p-3 dark:border-ink-700">{!! totp_qr !!}</div>
</div>
<div>
<ol class="space-y-3 text-sm text-ink-600 dark:text-ink-400">
<li><span class="font-medium text-ink-900 dark:text-ink-100">1.</span> Scan the code with your authenticator app.</li>
<li>
<span class="font-medium text-ink-900 dark:text-ink-100">2.</span> Cannot scan? Enter this key by hand:
<span class="mt-1.5 flex items-center gap-2">
<code class="select-all rounded bg-ink-100 px-2 py-1 font-mono text-xs dark:bg-ink-800">{{ totp_secret }}</code>
<button type="button" class="btn-ghost btn-sm" data-copy="{{ totp_secret }}">Copy</button>
</span>
</li>
<li><span class="font-medium text-ink-900 dark:text-ink-100">3.</span> Enter the code it shows.</li>
</ol>
<form method="post" action="/settings/security/totp/confirm" class="mt-5" novalidate>
{!! csrf_field !!}
<div class="max-w-[12rem]">
<label for="code" class="field-label">Code</label>
<input id="code" name="code" type="text" required inputmode="numeric" pattern="[0-9]*"
maxlength="6" autocomplete="one-time-code" data-otp-input
class="field-input text-center font-mono text-xl tracking-[0.3em]
@if(error_code) field-input-invalid @endif">
@if(error_code)<p class="field-error">{{ error_code }}</p>@endif
</div>
<button type="submit" class="btn-primary mt-4">Confirm and turn on</button>
</form>
</div>
</div>
@else
<form method="post" action="/settings/security/totp/start">
{!! csrf_field !!}
<button type="submit" class="btn-primary">Set up</button>
</form>
@endif
</div>
</div>
{{-- Passkeys --}}
<div class="card">
<div class="card-header flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="font-semibold">Passkeys</h3>
<p class="mt-0.5 text-sm text-ink-500 dark:text-ink-400">
Your fingerprint, face or security key. Cannot be phished, because the browser checks the site for you.
</p>
</div>
@if(passkeys_empty == false)<span class="badge-success">{{ passkey_count }} registered</span>@endif
</div>
<div class="card-body">
@if(passkeys_empty)
<p class="mb-4 text-sm text-ink-500 dark:text-ink-400">None registered.</p>
@else
<ul class="mb-4 divide-y divide-ink-200 dark:divide-ink-800">
@foreach(passkeys as key)
<li class="flex flex-wrap items-center justify-between gap-3 py-3">
<div class="min-w-0">
<p class="truncate text-sm font-medium">{{ key.label }}</p>
<p class="text-xs text-ink-500 dark:text-ink-400">Added {{ key.created_at }} · last used {{ key.last_used_at }}</p>
</div>
{{-- Removing a passkey asks for one first. The script fills
`assertion` in after the browser has proved you hold a
passkey on this account; with scripting off it stays
empty, the server refuses, and nothing is removed. --}}
<form method="post" action="/settings/security/passkeys/{{ key.id }}/delete"
data-passkey-confirm data-allow-resubmit
data-confirm-url="/settings/security/passkeys/confirm">
{!! csrf_field !!}
<input type="hidden" name="assertion" value="">
<button type="submit" class="btn-ghost btn-sm text-red-600 dark:text-red-400">Remove</button>
</form>
</li>
@endforeach
</ul>
@endif
<button type="button" class="btn-secondary" data-passkey-register
data-options-url="/settings/security/passkeys/options"
data-verify-url="/settings/security/passkeys">
Add a passkey
</button>
<p data-passkey-error class="field-error" hidden></p>
<p data-passkey-unsupported class="field-hint" hidden>This browser does not support passkeys.</p>
</div>
</div>
{{-- Recovery codes --}}
<div class="card">
<div class="card-header flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="font-semibold">Recovery codes</h3>
<p class="mt-0.5 text-sm text-ink-500 dark:text-ink-400">The way back in when the phone is gone.</p>
</div>
@if(recovery_remaining)<span class="badge-neutral">{{ recovery_remaining }} left</span>@endif
</div>
<div class="card-body">
@if(fresh_recovery_codes_empty == false)
<div class="mb-4 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm
dark:border-amber-500/30 dark:bg-amber-500/10">
<p class="font-medium text-amber-900 dark:text-amber-300">Save these now. They are not shown again.</p>
<ul class="mt-3 grid grid-cols-2 gap-2 font-mono text-sm">
@foreach(fresh_recovery_codes as code)
<li class="rounded bg-white px-2 py-1 text-center dark:bg-ink-900">{{ code }}</li>
@endforeach
</ul>
<div class="mt-3 flex gap-2">
<button type="button" class="btn-secondary btn-sm" data-copy="{{ fresh_recovery_codes_text }}">Copy all</button>
<a download="recovery-codes.txt" class="btn-secondary btn-sm"
data-download-text="{{ fresh_recovery_codes_text }}" href="#">Download</a>
</div>
</div>
@endif
<form method="post" action="/settings/security/recovery-codes"
data-confirm="Generate a new set? Every existing code stops working immediately.">
{!! csrf_field !!}
<button type="submit" class="btn-secondary">
@if(recovery_remaining)Generate a new set@else Generate codes @endif
</button>
</form>
</div>
</div>
{{-- Sessions --}}
<div class="card">
<div class="card-header"><h3 class="font-semibold">Signed-in devices</h3></div>
<div class="card-body">
<p class="mb-4 text-sm text-ink-500 dark:text-ink-400">
Signing out everywhere else keeps this device and ends the rest.
</p>
<form method="post" action="/settings/security/sessions/revoke"
data-confirm="Sign out of every other device?">
{!! csrf_field !!}
<button type="submit" class="btn-secondary">Sign out other devices</button>
</form>
</div>
</div>
</div>
@endsection