feat(profile): align settings with dashboard layout
This commit is contained in:
@@ -12,7 +12,11 @@ const { user } = useAuth()
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Dashboard" />
|
||||
<UDashboardNavbar title="Dashboard">
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
|
||||
388
resources/js/Pages/Profile/Security.vue
Normal file
388
resources/js/Pages/Profile/Security.vue
Normal file
@@ -0,0 +1,388 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
import RecoveryCodesPanel from '@/components/auth/RecoveryCodesPanel.vue'
|
||||
import ProfileSettingsPanel from '@/components/profile/ProfileSettingsPanel.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import DashboardLayout from '@/layouts/DashboardLayout.vue'
|
||||
|
||||
interface TwoFactorState {
|
||||
available: boolean
|
||||
enabled: boolean
|
||||
pending: boolean
|
||||
requiresPassword: boolean
|
||||
qrCodeDataUri: string | null
|
||||
secretKey: string | null
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
layout: DashboardLayout,
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
twoFactor: TwoFactorState
|
||||
}>()
|
||||
|
||||
const { config, flash } = useAuth()
|
||||
|
||||
const enableForm = useForm({ password: '' })
|
||||
const confirmForm = useForm({ code: '' })
|
||||
const disableForm = useForm({ password: '', code: '' })
|
||||
const recoveryForm = useForm({ password: '', code: '', regenerate: false })
|
||||
const passwordForm = useForm({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const recoveryCodes = computed(() => flash.value.recoveryCodes ?? [])
|
||||
const twoFactorRequired = computed(() =>
|
||||
config.value.features.two_factor && config.value.features.two_factor_required,
|
||||
)
|
||||
|
||||
function enable() {
|
||||
enableForm.post('/profile/two-factor', {
|
||||
preserveScroll: true,
|
||||
onFinish: () => enableForm.reset(),
|
||||
})
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
confirmForm.post('/profile/two-factor/confirm', {
|
||||
preserveScroll: true,
|
||||
onFinish: () => confirmForm.reset(),
|
||||
})
|
||||
}
|
||||
|
||||
function disable() {
|
||||
disableForm.delete('/profile/two-factor', {
|
||||
preserveScroll: true,
|
||||
onFinish: () => disableForm.reset(),
|
||||
})
|
||||
}
|
||||
|
||||
function recoveryCodesAction(regenerate: boolean) {
|
||||
recoveryForm.regenerate = regenerate
|
||||
recoveryForm.post('/profile/two-factor/recovery-codes', {
|
||||
preserveScroll: true,
|
||||
onFinish: () => recoveryForm.reset(),
|
||||
})
|
||||
}
|
||||
|
||||
function updatePassword() {
|
||||
passwordForm.put('/profile/password', {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => passwordForm.reset(),
|
||||
onFinish: () => passwordForm.reset(
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProfileSettingsPanel>
|
||||
<UAlert
|
||||
v-if="flash.success"
|
||||
color="success"
|
||||
icon="i-lucide-check-circle"
|
||||
:title="flash.success"
|
||||
/>
|
||||
<UAlert
|
||||
v-if="twoFactorRequired && !twoFactor.enabled"
|
||||
color="warning"
|
||||
icon="i-lucide-shield-alert"
|
||||
title="Two-factor authentication is required"
|
||||
description="Finish setup before accessing the rest of the application."
|
||||
/>
|
||||
|
||||
<UPageCard
|
||||
:title="props.twoFactor.requiresPassword ? 'Password' : 'Set a password'"
|
||||
:description="props.twoFactor.requiresPassword
|
||||
? 'Confirm your current password before setting a new one. Other sessions will be signed out.'
|
||||
: 'Add a password so you can also sign in without your social provider.'"
|
||||
variant="subtle"
|
||||
>
|
||||
<form class="flex max-w-sm flex-col gap-4" @submit.prevent="updatePassword">
|
||||
<UFormField
|
||||
v-if="props.twoFactor.requiresPassword"
|
||||
label="Current password"
|
||||
name="current_password"
|
||||
:error="passwordForm.errors.current_password"
|
||||
>
|
||||
<UInput
|
||||
v-model="passwordForm.current_password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="New password"
|
||||
name="password"
|
||||
description="Use between 15 and 128 characters."
|
||||
:error="passwordForm.errors.password"
|
||||
>
|
||||
<UInput
|
||||
v-model="passwordForm.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Confirm new password"
|
||||
name="password_confirmation"
|
||||
:error="passwordForm.errors.password_confirmation"
|
||||
>
|
||||
<UInput
|
||||
v-model="passwordForm.password_confirmation"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton
|
||||
type="submit"
|
||||
label="Update password"
|
||||
class="w-fit"
|
||||
:loading="passwordForm.processing"
|
||||
/>
|
||||
</form>
|
||||
</UPageCard>
|
||||
|
||||
<UPageCard variant="subtle">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="font-semibold">
|
||||
Two-factor authentication
|
||||
</h2>
|
||||
<p class="text-muted mt-1 text-sm">
|
||||
Protect your account with a time-based code from an authenticator app.
|
||||
</p>
|
||||
</div>
|
||||
<UBadge :color="twoFactor.enabled ? 'success' : twoFactor.pending ? 'warning' : 'neutral'">
|
||||
{{ twoFactor.enabled ? 'Enabled' : twoFactor.pending ? 'Setup pending' : 'Disabled' }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UAlert
|
||||
v-if="!twoFactor.available"
|
||||
color="neutral"
|
||||
icon="i-lucide-shield-off"
|
||||
title="Two-factor authentication is not available"
|
||||
description="An administrator can enable this feature through the authentication configuration."
|
||||
/>
|
||||
|
||||
<form v-else-if="!twoFactor.enabled && !twoFactor.pending" class="space-y-4" @submit.prevent="enable">
|
||||
<UAlert
|
||||
v-if="!twoFactor.requiresPassword"
|
||||
color="info"
|
||||
icon="i-lucide-info"
|
||||
title="Your social login session will authorize enrollment."
|
||||
/>
|
||||
<UFormField
|
||||
v-if="twoFactor.requiresPassword"
|
||||
label="Current password"
|
||||
name="password"
|
||||
:error="enableForm.errors.password"
|
||||
>
|
||||
<UInput
|
||||
v-model="enableForm.password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UButton type="submit" :loading="enableForm.processing">
|
||||
Enable two-factor authentication
|
||||
</UButton>
|
||||
</form>
|
||||
|
||||
<div v-else-if="twoFactor.pending" class="space-y-6">
|
||||
<div class="grid gap-6 md:grid-cols-[auto_1fr]">
|
||||
<img
|
||||
v-if="twoFactor.qrCodeDataUri"
|
||||
:src="twoFactor.qrCodeDataUri"
|
||||
alt="Two-factor authenticator QR code"
|
||||
class="size-48 rounded-lg border bg-white p-2"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<h3 class="font-medium">
|
||||
Scan this QR code
|
||||
</h3>
|
||||
<p class="text-muted text-sm">
|
||||
Scan it with any TOTP-compatible authenticator app, then enter the generated code below.
|
||||
</p>
|
||||
<div>
|
||||
<p class="text-muted mb-1 text-xs">
|
||||
Manual setup key
|
||||
</p>
|
||||
<code class="block break-all rounded bg-elevated p-2 text-sm">{{ twoFactor.secretKey }}</code>
|
||||
</div>
|
||||
</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"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<UButton type="submit" :loading="confirmForm.processing">
|
||||
Confirm setup
|
||||
</UButton>
|
||||
<UButton
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:loading="disableForm.processing"
|
||||
@click="disable"
|
||||
>
|
||||
Cancel setup
|
||||
</UButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<UAlert
|
||||
color="success"
|
||||
variant="subtle"
|
||||
icon="i-lucide-shield-check"
|
||||
title="Your account requires a second factor when signing in."
|
||||
/>
|
||||
|
||||
<div class="space-y-4 border-t pt-6">
|
||||
<div>
|
||||
<h3 class="font-medium">
|
||||
Recovery codes
|
||||
</h3>
|
||||
<p class="text-muted text-sm">
|
||||
Re-authenticate to reveal your codes or replace them with a new set.
|
||||
</p>
|
||||
</div>
|
||||
<UFormField
|
||||
v-if="twoFactor.requiresPassword"
|
||||
label="Current password"
|
||||
name="password"
|
||||
:error="recoveryForm.errors.password"
|
||||
>
|
||||
<UInput
|
||||
v-model="recoveryForm.password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Authentication or recovery code"
|
||||
name="code"
|
||||
:error="recoveryForm.errors.code"
|
||||
>
|
||||
<UInput
|
||||
v-model="recoveryForm.code"
|
||||
autocomplete="one-time-code"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<UButton
|
||||
variant="outline"
|
||||
:loading="recoveryForm.processing && !recoveryForm.regenerate"
|
||||
@click="recoveryCodesAction(false)"
|
||||
>
|
||||
Reveal recovery codes
|
||||
</UButton>
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="outline"
|
||||
:loading="recoveryForm.processing && recoveryForm.regenerate"
|
||||
@click="recoveryCodesAction(true)"
|
||||
>
|
||||
Generate new codes
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="twoFactorRequired"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
icon="i-lucide-lock-keyhole"
|
||||
title="Two-factor authentication is required by your administrator."
|
||||
description="It cannot be disabled while the mandatory security policy is active."
|
||||
/>
|
||||
|
||||
<form v-else class="space-y-4 border-t pt-6" @submit.prevent="disable">
|
||||
<div>
|
||||
<h3 class="font-medium text-error">
|
||||
Disable two-factor authentication
|
||||
</h3>
|
||||
<p class="text-muted text-sm">
|
||||
This removes the additional protection from your account.
|
||||
</p>
|
||||
</div>
|
||||
<UFormField
|
||||
v-if="twoFactor.requiresPassword"
|
||||
label="Current password"
|
||||
name="password"
|
||||
:error="disableForm.errors.password"
|
||||
>
|
||||
<UInput
|
||||
v-model="disableForm.password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Authentication or recovery code"
|
||||
name="code"
|
||||
:error="disableForm.errors.code"
|
||||
>
|
||||
<UInput
|
||||
v-model="disableForm.code"
|
||||
autocomplete="one-time-code"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UButton type="submit" color="error" :loading="disableForm.processing">
|
||||
Disable two-factor authentication
|
||||
</UButton>
|
||||
</form>
|
||||
</div>
|
||||
</UPageCard>
|
||||
|
||||
<UPageCard v-if="recoveryCodes.length" variant="subtle">
|
||||
<template #header>
|
||||
<div>
|
||||
<h2 class="font-semibold">
|
||||
Save your recovery codes
|
||||
</h2>
|
||||
<p class="text-muted mt-1 text-sm">
|
||||
Store these codes somewhere safe. Each code can be used once.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<RecoveryCodesPanel :codes="recoveryCodes" />
|
||||
</UPageCard>
|
||||
</ProfileSettingsPanel>
|
||||
</template>
|
||||
@@ -1,346 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
import RecoveryCodesPanel from '@/components/auth/RecoveryCodesPanel.vue'
|
||||
import ProfileSettingsPanel from '@/components/profile/ProfileSettingsPanel.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import DashboardLayout from '@/layouts/DashboardLayout.vue'
|
||||
|
||||
interface TwoFactorState {
|
||||
available: boolean
|
||||
enabled: boolean
|
||||
pending: boolean
|
||||
requiresPassword: boolean
|
||||
qrCodeDataUri: string | null
|
||||
secretKey: string | null
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
layout: DashboardLayout,
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
twoFactor: TwoFactorState
|
||||
}>()
|
||||
const { flash, user } = useAuth()
|
||||
|
||||
const { config, flash, user } = useAuth()
|
||||
const form = useForm({
|
||||
first_name: user.value?.first_name ?? '',
|
||||
last_name: user.value?.last_name ?? '',
|
||||
username: user.value?.username ?? '',
|
||||
email: user.value?.email ?? '',
|
||||
})
|
||||
|
||||
const enableForm = useForm({ password: '' })
|
||||
const confirmForm = useForm({ code: '' })
|
||||
const disableForm = useForm({ password: '', code: '' })
|
||||
const recoveryForm = useForm({ password: '', code: '', regenerate: false })
|
||||
|
||||
const recoveryCodes = computed(() => flash.value.recoveryCodes ?? [])
|
||||
const twoFactorRequired = computed(() =>
|
||||
config.value.features.two_factor && config.value.features.two_factor_required,
|
||||
)
|
||||
|
||||
function enable() {
|
||||
enableForm.post('/profile/two-factor', {
|
||||
function updateProfile() {
|
||||
form.patch('/profile', {
|
||||
preserveScroll: true,
|
||||
onFinish: () => enableForm.reset(),
|
||||
})
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
confirmForm.post('/profile/two-factor/confirm', {
|
||||
preserveScroll: true,
|
||||
onFinish: () => confirmForm.reset(),
|
||||
})
|
||||
}
|
||||
|
||||
function disable() {
|
||||
disableForm.delete('/profile/two-factor', {
|
||||
preserveScroll: true,
|
||||
onFinish: () => disableForm.reset(),
|
||||
})
|
||||
}
|
||||
|
||||
function recoveryCodesAction(regenerate: boolean) {
|
||||
recoveryForm.regenerate = regenerate
|
||||
recoveryForm.post('/profile/two-factor/recovery-codes', {
|
||||
preserveScroll: true,
|
||||
onFinish: () => recoveryForm.reset(),
|
||||
onSuccess: () => form.defaults(),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Profile" />
|
||||
</template>
|
||||
<ProfileSettingsPanel>
|
||||
<UAlert
|
||||
v-if="flash.success"
|
||||
color="success"
|
||||
icon="i-lucide-check-circle"
|
||||
:title="flash.success"
|
||||
/>
|
||||
|
||||
<template #body>
|
||||
<div class="mx-auto w-full max-w-3xl space-y-6">
|
||||
<UAlert
|
||||
v-if="flash.success"
|
||||
color="success"
|
||||
icon="i-lucide-check-circle"
|
||||
:title="flash.success"
|
||||
/>
|
||||
<UAlert
|
||||
v-if="twoFactorRequired && !twoFactor.enabled"
|
||||
color="warning"
|
||||
icon="i-lucide-shield-alert"
|
||||
title="Two-factor authentication is required"
|
||||
description="Finish setup before accessing the rest of the application."
|
||||
<form @submit.prevent="updateProfile">
|
||||
<UPageCard
|
||||
title="Profile"
|
||||
description="Manage the personal information used for your account."
|
||||
variant="naked"
|
||||
orientation="horizontal"
|
||||
class="mb-4"
|
||||
>
|
||||
<UButton
|
||||
type="submit"
|
||||
label="Save changes"
|
||||
color="neutral"
|
||||
class="w-fit lg:ms-auto"
|
||||
:loading="form.processing"
|
||||
:disabled="!form.isDirty"
|
||||
/>
|
||||
</UPageCard>
|
||||
|
||||
<UCard>
|
||||
<div class="flex flex-col gap-5 sm:flex-row sm:items-center">
|
||||
<UAvatar :alt="user?.full_name" size="3xl" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<h1 class="truncate text-xl font-semibold">
|
||||
{{ user?.full_name }}
|
||||
</h1>
|
||||
<p class="text-muted truncate text-sm">
|
||||
@{{ user?.username }}
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<UBadge color="neutral" variant="subtle" icon="i-lucide-mail">
|
||||
{{ user?.email }}
|
||||
</UBadge>
|
||||
<UBadge
|
||||
:color="user?.email_verified_at ? 'success' : 'warning'"
|
||||
variant="subtle"
|
||||
:icon="user?.email_verified_at ? 'i-lucide-badge-check' : 'i-lucide-circle-alert'"
|
||||
>
|
||||
{{ user?.email_verified_at ? 'Email verified' : 'Email not verified' }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
<UPageCard variant="subtle">
|
||||
<UFormField
|
||||
label="Avatar"
|
||||
description="Generated from your account name until avatar uploads are configured."
|
||||
class="flex items-center justify-between gap-4"
|
||||
>
|
||||
<UAvatar :alt="user?.full_name" size="lg" />
|
||||
</UFormField>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="font-semibold">
|
||||
Two-factor authentication
|
||||
</h2>
|
||||
<p class="text-muted mt-1 text-sm">
|
||||
Protect your account with a time-based code from an authenticator app.
|
||||
</p>
|
||||
</div>
|
||||
<UBadge :color="twoFactor.enabled ? 'success' : twoFactor.pending ? 'warning' : 'neutral'">
|
||||
{{ twoFactor.enabled ? 'Enabled' : twoFactor.pending ? 'Setup pending' : 'Disabled' }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
<USeparator />
|
||||
|
||||
<UAlert
|
||||
v-if="!twoFactor.available"
|
||||
color="neutral"
|
||||
icon="i-lucide-shield-off"
|
||||
title="Two-factor authentication is not available"
|
||||
description="An administrator can enable this feature through the authentication configuration."
|
||||
<UFormField
|
||||
label="First name"
|
||||
name="first_name"
|
||||
description="Used to personalize your account."
|
||||
required
|
||||
:error="form.errors.first_name"
|
||||
class="flex max-sm:flex-col items-start justify-between gap-4"
|
||||
>
|
||||
<UInput
|
||||
v-model="form.first_name"
|
||||
autocomplete="given-name"
|
||||
class="w-full sm:w-64"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<form v-else-if="!twoFactor.enabled && !twoFactor.pending" class="space-y-4" @submit.prevent="enable">
|
||||
<UAlert
|
||||
v-if="!twoFactor.requiresPassword"
|
||||
color="info"
|
||||
icon="i-lucide-info"
|
||||
title="Your social login session will authorize enrollment."
|
||||
<USeparator />
|
||||
|
||||
<UFormField
|
||||
label="Last name"
|
||||
name="last_name"
|
||||
description="Displayed together with your first name."
|
||||
required
|
||||
:error="form.errors.last_name"
|
||||
class="flex max-sm:flex-col items-start justify-between gap-4"
|
||||
>
|
||||
<UInput
|
||||
v-model="form.last_name"
|
||||
autocomplete="family-name"
|
||||
class="w-full sm:w-64"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<UFormField
|
||||
label="Username"
|
||||
name="username"
|
||||
description="Your unique username for signing in."
|
||||
required
|
||||
:error="form.errors.username"
|
||||
class="flex max-sm:flex-col items-start justify-between gap-4"
|
||||
>
|
||||
<UInput
|
||||
v-model="form.username"
|
||||
autocomplete="username"
|
||||
class="w-full sm:w-64"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<UFormField
|
||||
label="Email"
|
||||
name="email"
|
||||
description="Changing it requires email verification again when enabled."
|
||||
required
|
||||
:error="form.errors.email"
|
||||
class="flex max-sm:flex-col items-start justify-between gap-4"
|
||||
>
|
||||
<div class="flex w-full flex-col items-end gap-2 sm:w-64">
|
||||
<UInput
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
class="w-full"
|
||||
/>
|
||||
<UFormField
|
||||
v-if="twoFactor.requiresPassword"
|
||||
label="Current password"
|
||||
name="password"
|
||||
:error="enableForm.errors.password"
|
||||
<UBadge
|
||||
:color="user?.email_verified_at ? 'success' : 'warning'"
|
||||
variant="subtle"
|
||||
:icon="user?.email_verified_at ? 'i-lucide-badge-check' : 'i-lucide-circle-alert'"
|
||||
>
|
||||
<UInput
|
||||
v-model="enableForm.password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UButton type="submit" :loading="enableForm.processing">
|
||||
Enable two-factor authentication
|
||||
</UButton>
|
||||
</form>
|
||||
|
||||
<div v-else-if="twoFactor.pending" class="space-y-6">
|
||||
<div class="grid gap-6 md:grid-cols-[auto_1fr]">
|
||||
<img
|
||||
v-if="twoFactor.qrCodeDataUri"
|
||||
:src="twoFactor.qrCodeDataUri"
|
||||
alt="Two-factor authenticator QR code"
|
||||
class="size-48 rounded-lg border bg-white p-2"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<h3 class="font-medium">
|
||||
Scan this QR code
|
||||
</h3>
|
||||
<p class="text-muted text-sm">
|
||||
Scan it with any TOTP-compatible authenticator app, then enter the generated code below.
|
||||
</p>
|
||||
<div>
|
||||
<p class="text-muted mb-1 text-xs">
|
||||
Manual setup key
|
||||
</p>
|
||||
<code class="block break-all rounded bg-elevated p-2 text-sm">{{ twoFactor.secretKey }}</code>
|
||||
</div>
|
||||
</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"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<UButton type="submit" :loading="confirmForm.processing">
|
||||
Confirm setup
|
||||
</UButton>
|
||||
<UButton
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:loading="disableForm.processing"
|
||||
@click="disable"
|
||||
>
|
||||
Cancel setup
|
||||
</UButton>
|
||||
</div>
|
||||
</form>
|
||||
{{ user?.email_verified_at ? 'Email verified' : 'Email not verified' }}
|
||||
</UBadge>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<UAlert
|
||||
color="success"
|
||||
variant="subtle"
|
||||
icon="i-lucide-shield-check"
|
||||
title="Your account requires a second factor when signing in."
|
||||
/>
|
||||
|
||||
<div class="space-y-4 border-t pt-6">
|
||||
<div>
|
||||
<h3 class="font-medium">
|
||||
Recovery codes
|
||||
</h3>
|
||||
<p class="text-muted text-sm">
|
||||
Re-authenticate to reveal your codes or replace them with a new set.
|
||||
</p>
|
||||
</div>
|
||||
<UFormField
|
||||
v-if="twoFactor.requiresPassword"
|
||||
label="Current password"
|
||||
name="password"
|
||||
:error="recoveryForm.errors.password"
|
||||
>
|
||||
<UInput
|
||||
v-model="recoveryForm.password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Authentication or recovery code"
|
||||
name="code"
|
||||
:error="recoveryForm.errors.code"
|
||||
>
|
||||
<UInput
|
||||
v-model="recoveryForm.code"
|
||||
autocomplete="one-time-code"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<UButton
|
||||
variant="outline"
|
||||
:loading="recoveryForm.processing && !recoveryForm.regenerate"
|
||||
@click="recoveryCodesAction(false)"
|
||||
>
|
||||
Reveal recovery codes
|
||||
</UButton>
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="outline"
|
||||
:loading="recoveryForm.processing && recoveryForm.regenerate"
|
||||
@click="recoveryCodesAction(true)"
|
||||
>
|
||||
Generate new codes
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="twoFactorRequired"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
icon="i-lucide-lock-keyhole"
|
||||
title="Two-factor authentication is required by your administrator."
|
||||
description="It cannot be disabled while the mandatory security policy is active."
|
||||
/>
|
||||
|
||||
<form v-else class="space-y-4 border-t pt-6" @submit.prevent="disable">
|
||||
<div>
|
||||
<h3 class="font-medium text-error">
|
||||
Disable two-factor authentication
|
||||
</h3>
|
||||
<p class="text-muted text-sm">
|
||||
This removes the additional protection from your account.
|
||||
</p>
|
||||
</div>
|
||||
<UFormField
|
||||
v-if="twoFactor.requiresPassword"
|
||||
label="Current password"
|
||||
name="password"
|
||||
:error="disableForm.errors.password"
|
||||
>
|
||||
<UInput
|
||||
v-model="disableForm.password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Authentication or recovery code"
|
||||
name="code"
|
||||
:error="disableForm.errors.code"
|
||||
>
|
||||
<UInput
|
||||
v-model="disableForm.code"
|
||||
autocomplete="one-time-code"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UButton type="submit" color="error" :loading="disableForm.processing">
|
||||
Disable two-factor authentication
|
||||
</UButton>
|
||||
</form>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard v-if="recoveryCodes.length">
|
||||
<template #header>
|
||||
<div>
|
||||
<h2 class="font-semibold">
|
||||
Save your recovery codes
|
||||
</h2>
|
||||
<p class="text-muted mt-1 text-sm">
|
||||
Store these codes somewhere safe. Each code can be used once.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<RecoveryCodesPanel :codes="recoveryCodes" />
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</UFormField>
|
||||
</UPageCard>
|
||||
</form>
|
||||
</ProfileSettingsPanel>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user