refactor: improve MFA section visual hierarchy in SecurityTab

Redesigns the MFA method cards and supporting sections for better
visual hierarchy and professional styling:

Method cards (organizer):
- Vertical layout with large icon (VAvatar 44px) at top
- Description text explaining each method
- Status chip with check icon when configured
- VCardActions with primary chip/button + "Opnieuw instellen"
- Primary method card highlighted with 2px primary border
- Proper h-100 for equal height side-by-side

Backup codes:
- Separate outlined VCard with key icon, progress bar, refresh button
- Cleaner spacing and visual grouping

Disable MFA:
- Replaced heavy danger-zone card with subtle text button
  (tabler-shield-off icon, error color) — less visual weight for a
  rarely-used destructive action

Portal:
- Per-method rows with VAvatar icons and stacked status chips
- Matching text-button style for disable action

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 22:51:54 +02:00
parent d5fb15e5fe
commit 0cdee1382e
2 changed files with 230 additions and 204 deletions

View File

@@ -268,7 +268,7 @@ function copyRegeneratedCodes() {
</VCardItem>
<VCardText>
<!-- MFA method cards -->
<VRow class="mb-4">
<VRow>
<!-- TOTP card -->
<VCol
cols="12"
@@ -276,82 +276,80 @@ function copyRegeneratedCodes() {
>
<VCard
variant="outlined"
class="pa-4 h-100"
:class="['mfa-method-card h-100', { 'mfa-method-card--primary': totpConfigured && preferredMethod === 'totp' }]"
>
<div class="d-flex align-center mb-3">
<VCardText>
<VAvatar
color="primary"
variant="tonal"
size="40"
size="44"
rounded
class="me-3"
class="mb-3"
>
<VIcon
icon="tabler-device-mobile"
size="24"
size="26"
/>
</VAvatar>
<div>
<h6 class="text-h6">
Authenticator app
</h6>
<VChip
:color="totpConfigured ? 'success' : 'default'"
variant="tonal"
size="small"
class="mt-1"
>
{{ totpConfigured ? 'Geconfigureerd' : 'Niet ingesteld' }}
</VChip>
</div>
</div>
<template v-if="totpConfigured">
<h6 class="text-h6 mb-1">
Authenticator app
</h6>
<p class="text-body-2 text-medium-emphasis mb-3">
Gebruik een authenticator app zoals Google Authenticator of 1Password
</p>
<VChip
v-if="preferredMethod === 'totp'"
color="primary"
:color="totpConfigured ? 'success' : 'default'"
variant="tonal"
size="small"
class="mt-1"
:prepend-icon="totpConfigured ? 'tabler-check' : undefined"
>
Primaire methode
{{ totpConfigured ? 'Geconfigureerd' : 'Niet ingesteld' }}
</VChip>
</VCardText>
<VCardActions class="px-4 pb-4 pt-0">
<template v-if="totpConfigured">
<VChip
v-if="preferredMethod === 'totp'"
color="primary"
variant="tonal"
size="small"
prepend-icon="tabler-check"
>
Primaire methode
</VChip>
<VBtn
v-else
variant="tonal"
size="small"
:loading="setPreferredMethodMutation.isPending.value"
@click="handleSetPreferred('totp')"
>
Als primair instellen
</VBtn>
<VSpacer />
<VBtn
variant="text"
size="small"
@click="showTotpSetup = true"
>
Opnieuw instellen
</VBtn>
</template>
<VBtn
v-else
variant="text"
variant="tonal"
size="small"
color="primary"
class="mt-1 ms-n2"
:loading="setPreferredMethodMutation.isPending.value"
@click="handleSetPreferred('totp')"
@click="showTotpSetup = true"
>
Als primair instellen
Instellen
</VBtn>
</template>
<p
v-else
class="text-body-2 text-medium-emphasis mb-3"
>
Gebruik Google Authenticator, Authy of een andere app.
</p>
<VBtn
v-if="totpConfigured"
variant="tonal"
size="small"
class="mt-2"
@click="showTotpSetup = true"
>
Opnieuw instellen
</VBtn>
<VBtn
v-else
variant="tonal"
size="small"
@click="showTotpSetup = true"
>
Instellen
</VBtn>
</VCardActions>
</VCard>
</VCol>
@@ -362,139 +360,139 @@ function copyRegeneratedCodes() {
>
<VCard
variant="outlined"
class="pa-4 h-100"
:class="['mfa-method-card h-100', { 'mfa-method-card--primary': emailConfigured && preferredMethod === 'email' }]"
>
<div class="d-flex align-center mb-3">
<VCardText>
<VAvatar
color="primary"
variant="tonal"
size="40"
size="44"
rounded
class="me-3"
class="mb-3"
>
<VIcon
icon="tabler-mail"
size="24"
size="26"
/>
</VAvatar>
<div>
<h6 class="text-h6">
E-mailcode
</h6>
<VChip
:color="emailConfigured ? 'success' : 'default'"
variant="tonal"
size="small"
class="mt-1"
>
{{ emailConfigured ? 'Geconfigureerd' : 'Niet ingesteld' }}
</VChip>
</div>
</div>
<template v-if="emailConfigured">
<h6 class="text-h6 mb-1">
E-mailcode
</h6>
<p class="text-body-2 text-medium-emphasis mb-3">
Ontvang een 6-cijferige code op {{ authStore.user?.email }}
</p>
<VChip
v-if="preferredMethod === 'email'"
color="primary"
:color="emailConfigured ? 'success' : 'default'"
variant="tonal"
size="small"
class="mt-1"
:prepend-icon="emailConfigured ? 'tabler-check' : undefined"
>
Primaire methode
{{ emailConfigured ? 'Geconfigureerd' : 'Niet ingesteld' }}
</VChip>
</VCardText>
<VCardActions class="px-4 pb-4 pt-0">
<template v-if="emailConfigured">
<VChip
v-if="preferredMethod === 'email'"
color="primary"
variant="tonal"
size="small"
prepend-icon="tabler-check"
>
Primaire methode
</VChip>
<VBtn
v-else
variant="tonal"
size="small"
:loading="setPreferredMethodMutation.isPending.value"
@click="handleSetPreferred('email')"
>
Als primair instellen
</VBtn>
<VSpacer />
<VBtn
variant="text"
size="small"
@click="showEmailSetup = true"
>
Opnieuw instellen
</VBtn>
</template>
<VBtn
v-else
variant="text"
variant="tonal"
size="small"
color="primary"
class="mt-1 ms-n2"
:loading="setPreferredMethodMutation.isPending.value"
@click="handleSetPreferred('email')"
@click="showEmailSetup = true"
>
Als primair instellen
Instellen
</VBtn>
</template>
<p
v-else
class="text-body-2 text-medium-emphasis mb-3"
>
Ontvang een code per e-mail bij elke login.
</p>
<VBtn
v-if="emailConfigured"
variant="tonal"
size="small"
class="mt-2"
@click="showEmailSetup = true"
>
Opnieuw instellen
</VBtn>
<VBtn
v-else
variant="tonal"
size="small"
@click="showEmailSetup = true"
>
Instellen
</VBtn>
</VCardActions>
</VCard>
</VCol>
</VRow>
<!-- Backup codes status -->
<template v-if="isEnabled">
<VDivider class="mb-4" />
<div class="d-flex align-center justify-space-between mb-2">
<div>
<span class="text-body-1 font-weight-medium">Backup codes</span>
<p class="text-body-2 text-medium-emphasis mb-0">
{{ backupCodesRemaining }} van 10 resterend
</p>
</div>
<VBtn
variant="tonal"
size="small"
@click="showRegenerateDialog = true; regeneratedCodes = []; regenerateCode = ''; regenerateError = ''"
>
Nieuwe codes genereren
</VBtn>
</div>
<VProgressLinear
:model-value="backupCodesRemaining * 10"
:color="backupCodesColor"
rounded
height="6"
class="mb-2"
/>
</template>
</VCardText>
</VCard>
<!-- Card 2b: MFA disable (danger zone) -->
<VCard
v-if="isEnabled"
class="mb-6 border-error"
variant="outlined"
>
<VCardText class="d-flex align-center justify-space-between">
<div>
<h6 class="text-h6 text-error mb-1">
Tweestapsverificatie uitschakelen
</h6>
<p class="text-body-2 text-medium-emphasis mb-0">
Dit vermindert de beveiliging van je account.
</p>
</div>
<VBtn
color="error"
variant="tonal"
@click="showDisableDialog = true"
<!-- Backup codes -->
<VCard
v-if="isEnabled"
variant="outlined"
class="mt-4"
>
Uitschakelen
</VBtn>
<VCardText>
<div class="d-flex align-center justify-space-between">
<div>
<div class="d-flex align-center gap-2 mb-1">
<VIcon
icon="tabler-key"
size="20"
class="text-medium-emphasis"
/>
<span class="text-body-1 font-weight-medium">Backup codes</span>
</div>
<span class="text-body-2 text-medium-emphasis">
{{ backupCodesRemaining }} van 10 resterend
</span>
</div>
<VBtn
variant="tonal"
size="small"
prepend-icon="tabler-refresh"
@click="showRegenerateDialog = true; regeneratedCodes = []; regenerateCode = ''; regenerateError = ''"
>
Nieuwe codes
</VBtn>
</div>
<VProgressLinear
:model-value="backupCodesRemaining * 10"
:color="backupCodesColor"
class="mt-3"
rounded
height="6"
/>
</VCardText>
</VCard>
<!-- Disable MFA -->
<div
v-if="isEnabled"
class="mt-6"
>
<VBtn
variant="text"
color="error"
size="small"
prepend-icon="tabler-shield-off"
@click="showDisableDialog = true"
>
Tweestapsverificatie uitschakelen
</VBtn>
</div>
</VCardText>
</VCard>
@@ -682,3 +680,10 @@ function copyRegeneratedCodes() {
--v-card-list-gap: 8px;
}
</style>
<style lang="scss" scoped>
.mfa-method-card--primary {
border-color: rgb(var(--v-theme-primary));
border-width: 2px;
}
</style>

View File

@@ -676,29 +676,39 @@ function viewEvent(eventId: string) {
<div class="d-flex flex-column gap-3 mb-4">
<!-- TOTP row -->
<div class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<VIcon
icon="tabler-device-mobile"
size="20"
class="me-2"
/>
<span class="text-body-2 font-weight-medium me-2">Authenticator app</span>
<VChip
:color="totpConfigured ? 'success' : 'default'"
variant="tonal"
size="x-small"
>
{{ totpConfigured ? 'Actief' : 'Niet ingesteld' }}
</VChip>
<VChip
v-if="totpConfigured && preferredMethod === 'totp'"
<div class="d-flex align-center gap-2">
<VAvatar
color="primary"
variant="tonal"
size="x-small"
class="ms-1"
size="32"
rounded
>
Primair
</VChip>
<VIcon
icon="tabler-device-mobile"
size="18"
/>
</VAvatar>
<div>
<span class="text-body-2 font-weight-medium">Authenticator app</span>
<div class="d-flex align-center gap-1 mt-1">
<VChip
:color="totpConfigured ? 'success' : 'default'"
variant="tonal"
size="x-small"
:prepend-icon="totpConfigured ? 'tabler-check' : undefined"
>
{{ totpConfigured ? 'Actief' : 'Niet ingesteld' }}
</VChip>
<VChip
v-if="totpConfigured && preferredMethod === 'totp'"
color="primary"
variant="tonal"
size="x-small"
>
Primair
</VChip>
</div>
</div>
</div>
<VBtn
v-if="totpConfigured && preferredMethod !== 'totp'"
@@ -714,29 +724,39 @@ function viewEvent(eventId: string) {
<!-- Email row -->
<div class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<VIcon
icon="tabler-mail"
size="20"
class="me-2"
/>
<span class="text-body-2 font-weight-medium me-2">E-mailcode</span>
<VChip
color="success"
variant="tonal"
size="x-small"
>
Actief
</VChip>
<VChip
v-if="preferredMethod === 'email'"
<div class="d-flex align-center gap-2">
<VAvatar
color="primary"
variant="tonal"
size="x-small"
class="ms-1"
size="32"
rounded
>
Primair
</VChip>
<VIcon
icon="tabler-mail"
size="18"
/>
</VAvatar>
<div>
<span class="text-body-2 font-weight-medium">E-mailcode</span>
<div class="d-flex align-center gap-1 mt-1">
<VChip
color="success"
variant="tonal"
size="x-small"
prepend-icon="tabler-check"
>
Actief
</VChip>
<VChip
v-if="preferredMethod === 'email'"
color="primary"
variant="tonal"
size="x-small"
>
Primair
</VChip>
</div>
</div>
</div>
<VBtn
v-if="preferredMethod !== 'email'"
@@ -752,9 +772,10 @@ function viewEvent(eventId: string) {
</div>
<VBtn
variant="text"
color="error"
variant="tonal"
size="small"
prepend-icon="tabler-shield-off"
class="mb-4"
@click="showDisableDialog = true"
>