Files
laravel-nuxt-ui-inertia-tem…/resources/js/Pages/Profile/Security.vue

389 lines
12 KiB
Vue

<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>