223 lines
6.2 KiB
Vue
223 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
import { useForm } from '@inertiajs/vue3'
|
|
import { computed, ref } from 'vue'
|
|
import RecoveryCodesPanel from '@/components/auth/RecoveryCodesPanel.vue'
|
|
import { useAuth } from '@/composables/useAuth'
|
|
import AuthLayout from '@/layouts/AuthLayout.vue'
|
|
|
|
interface TwoFactorState {
|
|
available: boolean
|
|
enabled: boolean
|
|
pending: boolean
|
|
requiresPassword: boolean
|
|
qrCodeDataUri: string | null
|
|
secretKey: string | null
|
|
}
|
|
|
|
defineProps<{
|
|
twoFactor: TwoFactorState
|
|
}>()
|
|
|
|
const { flash } = useAuth()
|
|
const enableForm = useForm({ password: '' })
|
|
const confirmForm = useForm({ code: '' })
|
|
const logoutForm = useForm({})
|
|
const recoveryCodes = computed(() => flash.value.recoveryCodes ?? [])
|
|
const manualKeyCopied = ref(false)
|
|
|
|
function enable() {
|
|
enableForm.post('/two-factor-setup', {
|
|
onFinish: () => enableForm.reset(),
|
|
})
|
|
}
|
|
|
|
function confirm() {
|
|
confirmForm.post('/two-factor-setup/confirm', {
|
|
onFinish: () => confirmForm.reset(),
|
|
})
|
|
}
|
|
|
|
function logout() {
|
|
logoutForm.post('/logout')
|
|
}
|
|
|
|
async function copyManualKey(secretKey: string | null) {
|
|
if (!secretKey)
|
|
return
|
|
|
|
await navigator.clipboard.writeText(secretKey)
|
|
manualKeyCopied.value = true
|
|
|
|
window.setTimeout(() => {
|
|
manualKeyCopied.value = false
|
|
}, 2000)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<AuthLayout>
|
|
<div class="space-y-6">
|
|
<div class="space-y-2 text-center">
|
|
<span class="mx-auto flex size-12 items-center justify-center rounded-full bg-primary/10">
|
|
<UIcon
|
|
:name="twoFactor.enabled ? 'i-lucide-shield-check' : 'i-lucide-shield-keyhole'"
|
|
class="size-6 text-primary"
|
|
/>
|
|
</span>
|
|
<h1 class="text-xl font-semibold">
|
|
{{ twoFactor.enabled ? 'Setup complete' : 'Secure your account' }}
|
|
</h1>
|
|
<p class="text-muted text-sm">
|
|
{{ twoFactor.enabled
|
|
? 'Two-factor authentication is ready. Save your recovery codes before continuing.'
|
|
: 'Two-factor authentication is required before you can continue.' }}
|
|
</p>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="flash.error"
|
|
color="error"
|
|
icon="i-lucide-alert-circle"
|
|
:title="flash.error"
|
|
/>
|
|
|
|
<div v-if="twoFactor.enabled" class="space-y-5">
|
|
<UAlert
|
|
color="success"
|
|
variant="subtle"
|
|
icon="i-lucide-check-circle"
|
|
title="Your authenticator has been verified."
|
|
/>
|
|
|
|
<div>
|
|
<p class="mb-3 text-sm font-medium">
|
|
Recovery codes
|
|
</p>
|
|
<RecoveryCodesPanel :codes="recoveryCodes" />
|
|
<p class="mt-3 text-xs text-muted">
|
|
Store these somewhere safe. Each recovery code can only be used once.
|
|
</p>
|
|
</div>
|
|
|
|
<UButton to="/two-factor-setup/complete" block trailing-icon="i-lucide-arrow-right">
|
|
Continue
|
|
</UButton>
|
|
</div>
|
|
|
|
<form
|
|
v-else-if="!twoFactor.pending"
|
|
class="space-y-4"
|
|
@submit.prevent="enable"
|
|
>
|
|
<p class="text-sm text-muted">
|
|
Start by confirming your identity. You will then connect an authenticator app.
|
|
</p>
|
|
|
|
<UFormField
|
|
v-if="twoFactor.requiresPassword"
|
|
label="Current password"
|
|
name="password"
|
|
:error="enableForm.errors.password"
|
|
>
|
|
<UInput
|
|
v-model="enableForm.password"
|
|
type="password"
|
|
autocomplete="current-password"
|
|
autofocus
|
|
class="w-full"
|
|
/>
|
|
</UFormField>
|
|
|
|
<UAlert
|
|
v-else
|
|
color="info"
|
|
variant="subtle"
|
|
icon="i-lucide-info"
|
|
title="Your social login has confirmed your identity."
|
|
/>
|
|
|
|
<UButton type="submit" block :loading="enableForm.processing">
|
|
Set up authenticator
|
|
</UButton>
|
|
</form>
|
|
|
|
<div v-else class="space-y-5">
|
|
<div class="space-y-3 text-center">
|
|
<img
|
|
v-if="twoFactor.qrCodeDataUri"
|
|
:src="twoFactor.qrCodeDataUri"
|
|
alt="Two-factor authenticator QR code"
|
|
class="mx-auto size-48 rounded-lg border bg-white p-2"
|
|
>
|
|
<div>
|
|
<h2 class="font-medium">
|
|
Scan the QR code
|
|
</h2>
|
|
<p class="mt-1 text-sm text-muted">
|
|
Use any TOTP-compatible authenticator app.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="mb-1 text-xs text-muted">
|
|
Manual setup key
|
|
</p>
|
|
<div class="flex items-center gap-2 rounded-lg bg-elevated p-2">
|
|
<code class="min-w-0 flex-1 break-all px-1 text-sm">
|
|
{{ twoFactor.secretKey }}
|
|
</code>
|
|
<UButton
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
:icon="manualKeyCopied ? 'i-lucide-check' : 'i-lucide-copy'"
|
|
:color="manualKeyCopied ? 'success' : 'neutral'"
|
|
:aria-label="manualKeyCopied ? 'Manual setup key copied' : 'Copy manual setup key'"
|
|
@click="copyManualKey(twoFactor.secretKey)"
|
|
>
|
|
{{ manualKeyCopied ? 'Copied' : 'Copy' }}
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
|
|
<form class="space-y-4" @submit.prevent="confirm">
|
|
<UFormField
|
|
label="Six-digit authentication code"
|
|
name="code"
|
|
:error="confirmForm.errors.code"
|
|
>
|
|
<UInput
|
|
v-model="confirmForm.code"
|
|
inputmode="numeric"
|
|
autocomplete="one-time-code"
|
|
maxlength="6"
|
|
placeholder="123456"
|
|
autofocus
|
|
class="w-full"
|
|
:ui="{ base: 'text-center tracking-[0.35em]' }"
|
|
/>
|
|
</UFormField>
|
|
<UButton type="submit" block :loading="confirmForm.processing">
|
|
Verify authenticator
|
|
</UButton>
|
|
</form>
|
|
</div>
|
|
|
|
<div v-if="!twoFactor.enabled" class="border-t pt-4">
|
|
<UButton
|
|
type="button"
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-log-out"
|
|
block
|
|
:loading="logoutForm.processing"
|
|
@click="logout"
|
|
>
|
|
Sign out and finish later
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</AuthLayout>
|
|
</template>
|