feat: set preferred MFA method from account settings
Adds the ability for users to change their preferred/primary MFA method when both TOTP and email are available. Backend: - Add PUT /auth/mfa/preferred-method endpoint with validation (method must be totp/email, MFA must be enabled, TOTP must be configured if selecting totp) - Add totp_configured and email_configured fields to MFA status endpoint (totp = has secret + enabled, email = always when enabled) - Fix setupEmail() to preserve mfa_secret so TOTP config survives when email is set up as a second method Frontend (organizer + portal): - Add useSetPreferredMethod() composable to useMfa.ts - Add totp_configured/email_configured to MfaStatus type - SecurityTab method cards now show "Primaire methode" chip on the preferred method and "Als primair instellen" button on the other - Portal security section shows per-method rows with status chips and primary switching Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
useRevokeDevice,
|
||||
useRevokeAllDevices,
|
||||
useRegenerateBackupCodes,
|
||||
useSetPreferredMethod,
|
||||
} from '@/composables/api/useMfa'
|
||||
import MfaTotpSetupDialog from '@/components/settings/MfaTotpSetupDialog.vue'
|
||||
import MfaEmailSetupDialog from '@/components/settings/MfaEmailSetupDialog.vue'
|
||||
@@ -59,6 +60,8 @@ const revokeDeviceMutation = useRevokeDevice()
|
||||
const revokeAllMutation = useRevokeAllDevices()
|
||||
const regenerateCodesMutation = useRegenerateBackupCodes()
|
||||
|
||||
const setPreferredMethodMutation = useSetPreferredMethod()
|
||||
|
||||
const showTotpSetup = ref(false)
|
||||
const showEmailSetup = ref(false)
|
||||
const showDisableDialog = ref(false)
|
||||
@@ -69,8 +72,13 @@ const regenerateError = ref('')
|
||||
|
||||
const isEnabled = computed(() => mfaStatus.value?.enabled ?? false)
|
||||
|
||||
const totpConfigured = computed(() => isEnabled.value && mfaStatus.value?.method === 'totp')
|
||||
const emailConfigured = computed(() => isEnabled.value && mfaStatus.value?.method === 'email')
|
||||
const totpConfigured = computed(() => mfaStatus.value?.totp_configured ?? false)
|
||||
const emailConfigured = computed(() => mfaStatus.value?.email_configured ?? false)
|
||||
const preferredMethod = computed(() => mfaStatus.value?.method ?? null)
|
||||
|
||||
function handleSetPreferred(method: 'totp' | 'email') {
|
||||
setPreferredMethodMutation.mutate(method)
|
||||
}
|
||||
|
||||
const backupCodesRemaining = computed(() => mfaStatus.value?.backup_codes_remaining ?? 0)
|
||||
const backupCodesColor = computed(() => {
|
||||
@@ -298,14 +306,30 @@ function copyRegeneratedCodes() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="totpConfigured">
|
||||
<VChip
|
||||
v-if="preferredMethod === 'totp'"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="mt-1"
|
||||
>
|
||||
Primaire methode
|
||||
</VChip>
|
||||
<VBtn
|
||||
v-else
|
||||
variant="text"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="mt-1 ms-n2"
|
||||
:loading="setPreferredMethodMutation.isPending.value"
|
||||
@click="handleSetPreferred('totp')"
|
||||
>
|
||||
Als primair instellen
|
||||
</VBtn>
|
||||
</template>
|
||||
<p
|
||||
v-if="totpConfigured && mfaStatus?.method === 'totp'"
|
||||
class="text-body-2 text-medium-emphasis mb-3"
|
||||
>
|
||||
Primaire methode
|
||||
</p>
|
||||
<p
|
||||
v-else-if="!totpConfigured"
|
||||
v-else
|
||||
class="text-body-2 text-medium-emphasis mb-3"
|
||||
>
|
||||
Gebruik Google Authenticator, Authy of een andere app.
|
||||
@@ -315,6 +339,7 @@ function copyRegeneratedCodes() {
|
||||
v-if="totpConfigured"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="mt-2"
|
||||
@click="showTotpSetup = true"
|
||||
>
|
||||
Opnieuw instellen
|
||||
@@ -367,14 +392,30 @@ function copyRegeneratedCodes() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="emailConfigured">
|
||||
<VChip
|
||||
v-if="preferredMethod === 'email'"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="mt-1"
|
||||
>
|
||||
Primaire methode
|
||||
</VChip>
|
||||
<VBtn
|
||||
v-else
|
||||
variant="text"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="mt-1 ms-n2"
|
||||
:loading="setPreferredMethodMutation.isPending.value"
|
||||
@click="handleSetPreferred('email')"
|
||||
>
|
||||
Als primair instellen
|
||||
</VBtn>
|
||||
</template>
|
||||
<p
|
||||
v-if="emailConfigured && mfaStatus?.method === 'email'"
|
||||
class="text-body-2 text-medium-emphasis mb-3"
|
||||
>
|
||||
Primaire methode
|
||||
</p>
|
||||
<p
|
||||
v-else-if="!emailConfigured"
|
||||
v-else
|
||||
class="text-body-2 text-medium-emphasis mb-3"
|
||||
>
|
||||
Ontvang een code per e-mail bij elke login.
|
||||
@@ -384,6 +425,7 @@ function copyRegeneratedCodes() {
|
||||
v-if="emailConfigured"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="mt-2"
|
||||
@click="showEmailSetup = true"
|
||||
>
|
||||
Opnieuw instellen
|
||||
|
||||
Reference in New Issue
Block a user