feat: account settings with Vuexy tab pattern and MFA banner fix
Restructures account/profile pages to match Vuexy's account-settings tab pattern (Account, Security, Notifications) and fixes the MFA enforcement banner that stayed visible after successful setup. Backend: - Add phone column to users table with migration - Add PUT /me/profile endpoint for profile updates - Create UpdateProfileRequest form request - Update MeResource to include phone field Organizer app: - Rewrite account-settings as tabbed page (VTabs pill style + VWindow) - Create AccountTab: avatar, profile form, email change, danger zone - Create SecurityTab: password change, MFA method cards, backup codes, trusted devices, disable MFA danger zone - Create NotificationsTab: placeholder with disabled toggles - Fix MFA banner: set authStore.mfaSetupRequired = false on setup complete - Update router guard to redirect to ?tab=security for MFA enforcement - Update UserProfile menu links to use tab query params Portal: - Restructure profiel.vue with VTabs (Mijn profiel + Beveiliging) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { useChangePassword, useChangeEmail } from '@/composables/api/useAccount'
|
||||
import AccountTab from '@/components/account-settings/AccountTab.vue'
|
||||
import SecurityTab from '@/components/account-settings/SecurityTab.vue'
|
||||
import NotificationsTab from '@/components/account-settings/NotificationsTab.vue'
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
@@ -8,214 +9,60 @@ definePage({
|
||||
},
|
||||
})
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Password change
|
||||
const passwordForm = ref({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
const tabs = [
|
||||
{ title: 'Account', icon: 'tabler-user', value: 'account' },
|
||||
{ title: 'Beveiliging', icon: 'tabler-shield-lock', value: 'security' },
|
||||
{ title: 'Meldingen', icon: 'tabler-bell', value: 'notifications' },
|
||||
]
|
||||
|
||||
const activeTab = computed({
|
||||
get: () => {
|
||||
const tab = route.query.tab as string | undefined
|
||||
return tabs.some(t => t.value === tab) ? tab! : 'account'
|
||||
},
|
||||
set: (val: string) => {
|
||||
router.replace({ query: { ...route.query, tab: val } })
|
||||
},
|
||||
})
|
||||
const passwordFieldErrors = ref<Record<string, string>>({})
|
||||
const passwordSuccess = ref('')
|
||||
const showCurrentPw = ref(false)
|
||||
const showNewPw = ref(false)
|
||||
const showConfirmPw = ref(false)
|
||||
|
||||
const changePasswordMutation = useChangePassword()
|
||||
|
||||
async function handlePasswordChange() {
|
||||
passwordFieldErrors.value = {}
|
||||
passwordSuccess.value = ''
|
||||
|
||||
changePasswordMutation.mutate(passwordForm.value, {
|
||||
onSuccess: (data) => {
|
||||
passwordSuccess.value = data.message
|
||||
passwordForm.value = {
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
}
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const ax = err as { response?: { data?: { message?: string; errors?: Record<string, string[]> } } }
|
||||
if (ax.response?.data?.errors) {
|
||||
for (const [key, messages] of Object.entries(ax.response.data.errors)) {
|
||||
passwordFieldErrors.value[key] = messages[0]
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Email change
|
||||
const emailForm = ref({
|
||||
new_email: '',
|
||||
password: '',
|
||||
})
|
||||
const emailFieldErrors = ref<Record<string, string>>({})
|
||||
const emailSuccess = ref('')
|
||||
const showEmailPw = ref(false)
|
||||
|
||||
const changeEmailMutation = useChangeEmail()
|
||||
|
||||
async function handleEmailChange() {
|
||||
emailFieldErrors.value = {}
|
||||
emailSuccess.value = ''
|
||||
|
||||
changeEmailMutation.mutate(
|
||||
{ ...emailForm.value, app: 'app' },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
emailSuccess.value = data.message
|
||||
emailForm.value = { new_email: '', password: '' }
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const ax = err as { response?: { data?: { message?: string; errors?: Record<string, string[]> } } }
|
||||
if (ax.response?.data?.errors) {
|
||||
for (const [key, messages] of Object.entries(ax.response.data.errors)) {
|
||||
emailFieldErrors.value[key] = messages[0]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow justify="center">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
lg="6"
|
||||
<div>
|
||||
<VTabs
|
||||
v-model="activeTab"
|
||||
class="v-tabs-pill mb-6"
|
||||
>
|
||||
<h4 class="text-h4 mb-6">
|
||||
Accountinstellingen
|
||||
</h4>
|
||||
<VTab
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
:value="tab.value"
|
||||
>
|
||||
<VIcon
|
||||
:icon="tab.icon"
|
||||
size="20"
|
||||
class="me-2"
|
||||
/>
|
||||
{{ tab.title }}
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<!-- Email change -->
|
||||
<VCard class="mb-6">
|
||||
<VCardTitle>E-mailadres wijzigen</VCardTitle>
|
||||
<VCardText>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Huidig e-mailadres: <strong>{{ authStore.user?.email }}</strong>
|
||||
</p>
|
||||
|
||||
<VForm @submit.prevent="handleEmailChange">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="emailForm.new_email"
|
||||
label="Nieuw e-mailadres"
|
||||
type="email"
|
||||
:error-messages="emailFieldErrors.new_email"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="emailForm.password"
|
||||
label="Huidig wachtwoord"
|
||||
:type="showEmailPw ? 'text' : 'password'"
|
||||
:append-inner-icon="showEmailPw ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
:error-messages="emailFieldErrors.password"
|
||||
hint="Ter bevestiging van je identiteit"
|
||||
persistent-hint
|
||||
@click:append-inner="showEmailPw = !showEmailPw"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="changeEmailMutation.isPending.value"
|
||||
:disabled="!emailForm.new_email || !emailForm.password"
|
||||
>
|
||||
Verificatiemail versturen
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
|
||||
<VAlert
|
||||
v-if="emailSuccess"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-4"
|
||||
>
|
||||
{{ emailSuccess }}
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Password change -->
|
||||
<VCard>
|
||||
<VCardTitle>Wachtwoord wijzigen</VCardTitle>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="handlePasswordChange">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="passwordForm.current_password"
|
||||
label="Huidig wachtwoord"
|
||||
:type="showCurrentPw ? 'text' : 'password'"
|
||||
:append-inner-icon="showCurrentPw ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
:error-messages="passwordFieldErrors.current_password"
|
||||
@click:append-inner="showCurrentPw = !showCurrentPw"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="passwordForm.password"
|
||||
label="Nieuw wachtwoord"
|
||||
:type="showNewPw ? 'text' : 'password'"
|
||||
:append-inner-icon="showNewPw ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
:error-messages="passwordFieldErrors.password"
|
||||
@click:append-inner="showNewPw = !showNewPw"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="passwordForm.password_confirmation"
|
||||
label="Bevestig nieuw wachtwoord"
|
||||
:type="showConfirmPw ? 'text' : 'password'"
|
||||
:append-inner-icon="showConfirmPw ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
:error-messages="passwordFieldErrors.password_confirmation"
|
||||
@click:append-inner="showConfirmPw = !showConfirmPw"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="changePasswordMutation.isPending.value"
|
||||
>
|
||||
Wachtwoord wijzigen
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
|
||||
<VAlert
|
||||
v-if="passwordSuccess"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-4"
|
||||
>
|
||||
{{ passwordSuccess }}
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VWindow
|
||||
v-model="activeTab"
|
||||
class="disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem value="account">
|
||||
<AccountTab />
|
||||
</VWindowItem>
|
||||
<VWindowItem value="security">
|
||||
<SecurityTab />
|
||||
</VWindowItem>
|
||||
<VWindowItem value="notifications">
|
||||
<NotificationsTab />
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import {
|
||||
useMfaStatus,
|
||||
useTrustedDevices,
|
||||
useRevokeDevice,
|
||||
useRevokeAllDevices,
|
||||
useRegenerateBackupCodes,
|
||||
} from '@/composables/api/useMfa'
|
||||
import MfaTotpSetupDialog from '@/components/settings/MfaTotpSetupDialog.vue'
|
||||
import MfaEmailSetupDialog from '@/components/settings/MfaEmailSetupDialog.vue'
|
||||
import MfaDisableDialog from '@/components/settings/MfaDisableDialog.vue'
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
navActiveLink: 'account-settings',
|
||||
},
|
||||
})
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const { data: mfaStatus, refetch: refetchMfaStatus } = useMfaStatus()
|
||||
const { data: trustedDevices, refetch: refetchDevices } = useTrustedDevices()
|
||||
const revokeDeviceMutation = useRevokeDevice()
|
||||
const revokeAllMutation = useRevokeAllDevices()
|
||||
const regenerateCodesMutation = useRegenerateBackupCodes()
|
||||
|
||||
const showTotpSetup = ref(false)
|
||||
const showEmailSetup = ref(false)
|
||||
const showDisableDialog = ref(false)
|
||||
const showRegenerateDialog = ref(false)
|
||||
const regenerateCode = ref('')
|
||||
const regeneratedCodes = ref<string[]>([])
|
||||
const regenerateError = ref('')
|
||||
|
||||
const isEnabled = computed(() => mfaStatus.value?.enabled ?? false)
|
||||
const methodLabel = computed(() => {
|
||||
if (mfaStatus.value?.method === 'totp') return 'Authenticator app'
|
||||
if (mfaStatus.value?.method === 'email') return 'E-mailcode'
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
function onSetupCompleted() {
|
||||
refetchMfaStatus()
|
||||
}
|
||||
|
||||
function onDisabled() {
|
||||
refetchMfaStatus()
|
||||
refetchDevices()
|
||||
}
|
||||
|
||||
async function handleRevokeDevice(id: string) {
|
||||
await revokeDeviceMutation.mutateAsync(id)
|
||||
refetchDevices()
|
||||
}
|
||||
|
||||
async function handleRevokeAllDevices() {
|
||||
await revokeAllMutation.mutateAsync()
|
||||
refetchDevices()
|
||||
}
|
||||
|
||||
async function handleRegenerateBackupCodes() {
|
||||
regenerateError.value = ''
|
||||
try {
|
||||
const data = await regenerateCodesMutation.mutateAsync({ code: regenerateCode.value })
|
||||
|
||||
regeneratedCodes.value = data.backup_codes
|
||||
refetchMfaStatus()
|
||||
}
|
||||
catch (err: unknown) {
|
||||
const ax = err as { response?: { data?: { message?: string } } }
|
||||
|
||||
regenerateError.value = ax.response?.data?.message ?? 'Kon codes niet genereren.'
|
||||
regenerateCode.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function copyRegeneratedCodes() {
|
||||
navigator.clipboard.writeText(regeneratedCodes.value.join('\n'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow justify="center">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
lg="6"
|
||||
>
|
||||
<h4 class="text-h4 mb-6">
|
||||
Beveiliging
|
||||
</h4>
|
||||
|
||||
<!-- Enforcement notice -->
|
||||
<VAlert
|
||||
v-if="mfaStatus?.is_required && !isEnabled"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-6"
|
||||
>
|
||||
Je organisatie vereist tweestapsverificatie. Stel het nu in om het platform te kunnen gebruiken.
|
||||
</VAlert>
|
||||
|
||||
<!-- Section 1: Tweestapsverificatie -->
|
||||
<VCard class="mb-6">
|
||||
<VCardTitle class="d-flex align-center">
|
||||
<VIcon
|
||||
icon="tabler-shield-lock"
|
||||
class="me-2"
|
||||
/>
|
||||
Tweestapsverificatie
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<!-- MFA NOT enabled -->
|
||||
<template v-if="!isEnabled">
|
||||
<p class="text-body-1 mb-4">
|
||||
Bescherm je account met een extra beveiligingslaag. Kies een methode:
|
||||
</p>
|
||||
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<VCard
|
||||
variant="outlined"
|
||||
class="cursor-pointer pa-4"
|
||||
@click="showTotpSetup = true"
|
||||
>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<VIcon
|
||||
icon="tabler-device-mobile"
|
||||
size="28"
|
||||
color="primary"
|
||||
class="me-2"
|
||||
/>
|
||||
<span class="text-subtitle-1 font-weight-medium">Authenticator app</span>
|
||||
</div>
|
||||
<p class="text-body-2 text-medium-emphasis mb-0">
|
||||
Aanbevolen. Gebruik Google Authenticator, Authy of een andere app.
|
||||
</p>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<VCard
|
||||
variant="outlined"
|
||||
class="cursor-pointer pa-4"
|
||||
@click="showEmailSetup = true"
|
||||
>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<VIcon
|
||||
icon="tabler-mail"
|
||||
size="28"
|
||||
color="primary"
|
||||
class="me-2"
|
||||
/>
|
||||
<span class="text-subtitle-1 font-weight-medium">E-mailcode</span>
|
||||
</div>
|
||||
<p class="text-body-2 text-medium-emphasis mb-0">
|
||||
Ontvang een code per e-mail bij elke login.
|
||||
</p>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<!-- MFA IS enabled -->
|
||||
<template v-else>
|
||||
<div class="d-flex align-center justify-space-between mb-4">
|
||||
<div>
|
||||
<VChip
|
||||
color="success"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="me-2"
|
||||
>
|
||||
Ingeschakeld
|
||||
</VChip>
|
||||
<span class="text-body-1">via {{ methodLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup codes -->
|
||||
<div class="d-flex align-center justify-space-between mb-3 pa-3 rounded" style="background: rgb(var(--v-theme-surface-variant));">
|
||||
<div>
|
||||
<span class="text-body-1 font-weight-medium">Backup codes</span>
|
||||
<br>
|
||||
<span class="text-body-2 text-medium-emphasis">
|
||||
{{ mfaStatus?.backup_codes_remaining ?? 0 }} van 10 resterend
|
||||
</span>
|
||||
</div>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="showRegenerateDialog = true; regeneratedCodes = []; regenerateCode = ''; regenerateError = ''"
|
||||
>
|
||||
Nieuwe codes genereren
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
class="mt-2"
|
||||
@click="showDisableDialog = true"
|
||||
>
|
||||
Uitschakelen
|
||||
</VBtn>
|
||||
</template>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Section 2: Vertrouwde apparaten -->
|
||||
<VCard
|
||||
v-if="isEnabled"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardTitle class="d-flex align-center justify-space-between">
|
||||
<div class="d-flex align-center">
|
||||
<VIcon
|
||||
icon="tabler-devices"
|
||||
class="me-2"
|
||||
/>
|
||||
Vertrouwde apparaten
|
||||
</div>
|
||||
<VBtn
|
||||
v-if="trustedDevices && trustedDevices.length > 1"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
color="error"
|
||||
:loading="revokeAllMutation.isPending.value"
|
||||
@click="handleRevokeAllDevices"
|
||||
>
|
||||
Alles intrekken
|
||||
</VBtn>
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<template v-if="trustedDevices && trustedDevices.length > 0">
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="device in trustedDevices"
|
||||
:key="device.id"
|
||||
class="px-0"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="tabler-device-desktop"
|
||||
size="24"
|
||||
class="me-3"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>{{ device.device_name ?? 'Onbekend apparaat' }}</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
IP: {{ device.ip_address }}
|
||||
<span v-if="device.last_used_at">
|
||||
· Laatst gebruikt: {{ new Date(device.last_used_at).toLocaleDateString('nl-NL') }}
|
||||
</span>
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
icon="tabler-trash"
|
||||
:loading="revokeDeviceMutation.isPending.value"
|
||||
@click="handleRevokeDevice(device.id)"
|
||||
/>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</template>
|
||||
|
||||
<p
|
||||
v-else
|
||||
class="text-body-2 text-medium-emphasis"
|
||||
>
|
||||
Geen vertrouwde apparaten. Wanneer je inlogt met MFA kun je ervoor kiezen een apparaat te onthouden.
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Setup dialogs -->
|
||||
<MfaTotpSetupDialog
|
||||
v-model="showTotpSetup"
|
||||
@completed="onSetupCompleted"
|
||||
/>
|
||||
|
||||
<MfaEmailSetupDialog
|
||||
v-model="showEmailSetup"
|
||||
:user-email="authStore.user?.email ?? ''"
|
||||
@completed="onSetupCompleted"
|
||||
/>
|
||||
|
||||
<MfaDisableDialog
|
||||
v-model="showDisableDialog"
|
||||
:current-method="mfaStatus?.method ?? null"
|
||||
@disabled="onDisabled"
|
||||
/>
|
||||
|
||||
<!-- Regenerate backup codes dialog -->
|
||||
<VDialog
|
||||
v-model="showRegenerateDialog"
|
||||
max-width="460"
|
||||
>
|
||||
<VCard>
|
||||
<VCardTitle class="pt-4">
|
||||
Nieuwe backup codes genereren
|
||||
</VCardTitle>
|
||||
|
||||
<VCardText v-if="regeneratedCodes.length === 0">
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
Dit vervangt al je huidige backup codes. Oude codes werken niet meer.
|
||||
</VAlert>
|
||||
|
||||
<VAlert
|
||||
v-if="regenerateError"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
density="comfortable"
|
||||
>
|
||||
{{ regenerateError }}
|
||||
</VAlert>
|
||||
|
||||
<p class="text-body-2 mb-2">
|
||||
Voer je authenticator code in ter bevestiging
|
||||
</p>
|
||||
<AppTextField
|
||||
v-model="regenerateCode"
|
||||
placeholder="123456"
|
||||
autofocus
|
||||
/>
|
||||
</VCardText>
|
||||
|
||||
<VCardText v-else>
|
||||
<VAlert
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
Nieuwe backup codes gegenereerd. Bewaar ze veilig.
|
||||
</VAlert>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<VChip
|
||||
v-for="code in regeneratedCodes"
|
||||
:key="code"
|
||||
variant="tonal"
|
||||
label
|
||||
class="font-weight-bold"
|
||||
style="font-family: monospace;"
|
||||
>
|
||||
{{ code }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="tabler-copy"
|
||||
@click="copyRegeneratedCodes"
|
||||
>
|
||||
Kopieer
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="showRegenerateDialog = false"
|
||||
>
|
||||
{{ regeneratedCodes.length > 0 ? 'Sluiten' : 'Annuleren' }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-if="regeneratedCodes.length === 0"
|
||||
color="primary"
|
||||
:loading="regenerateCodesMutation.isPending.value"
|
||||
:disabled="!regenerateCode"
|
||||
@click="handleRegenerateBackupCodes"
|
||||
>
|
||||
Genereren
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
@@ -84,7 +84,7 @@ async function handleLogin() {
|
||||
authStore.setUser(data.data.user)
|
||||
|
||||
if (data.mfa_setup_required) {
|
||||
router.replace('/account-settings/security')
|
||||
router.replace('/account-settings?tab=security')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user