feat(mfa): nudge users with a dashboard banner when backup codes run low

This commit is contained in:
Tanmay Deep Sharma
2026-04-20 15:05:17 +07:00
parent 0ac947e3be
commit 9a22d03375
5 changed files with 88 additions and 1 deletions
+3
View File
@@ -5,6 +5,7 @@ import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
import LowBackupCodesBanner from './components/app/LowBackupCodesBanner.vue';
import vueActionCable from './helper/actionCable';
import { useRouter } from 'vue-router';
import { useStore } from 'dashboard/composables/store';
@@ -30,6 +31,7 @@ export default {
PaymentPendingBanner,
WootSnackbarBox,
PendingEmailVerificationBanner,
LowBackupCodesBanner,
},
setup() {
const router = useRouter();
@@ -140,6 +142,7 @@ export default {
<template v-if="currentAccountId">
<PendingEmailVerificationBanner v-if="hideOnOnboardingView" />
<PaymentPendingBanner v-if="hideOnOnboardingView" />
<LowBackupCodesBanner v-if="hideOnOnboardingView" />
</template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
@@ -0,0 +1,72 @@
<script>
import { mapGetters } from 'vuex';
import { parseBoolean } from '@chatwoot/utils';
import Banner from 'dashboard/components/ui/Banner.vue';
import mfaAPI from 'dashboard/api/mfa';
const LOW_BACKUP_CODES_THRESHOLD = 3;
export default {
components: { Banner },
data() {
return {
mfaEnabled: false,
remainingBackupCodes: null,
};
},
computed: {
...mapGetters({ currentAccountId: 'getCurrentAccountId' }),
shouldShowBanner() {
if (!this.mfaEnabled) return false;
if (this.remainingBackupCodes === null) return false;
return this.remainingBackupCodes <= LOW_BACKUP_CODES_THRESHOLD;
},
bannerColorScheme() {
return this.remainingBackupCodes === 0 ? 'alert' : 'warning';
},
bannerMessage() {
if (this.remainingBackupCodes === 0) {
return this.$t('MFA_SETTINGS.LOW_BACKUP_CODES.NONE_LEFT');
}
return this.$t('MFA_SETTINGS.LOW_BACKUP_CODES.MESSAGE', {
count: this.remainingBackupCodes,
});
},
},
mounted() {
this.fetchMfaStatus();
},
methods: {
async fetchMfaStatus() {
if (!parseBoolean(window.chatwootConfig?.isMfaEnabled)) return;
try {
const { data } = await mfaAPI.get();
this.mfaEnabled = data.enabled;
this.remainingBackupCodes = data.remaining_backup_codes ?? null;
} catch {
// ignore; banner stays hidden
}
},
goToMfaSettings() {
this.$router.push({
name: 'profile_settings_mfa',
params: { accountId: this.currentAccountId },
});
},
},
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<Banner
v-if="shouldShowBanner"
:color-scheme="bannerColorScheme"
:banner-message="bannerMessage"
:action-button-label="$t('MFA_SETTINGS.LOW_BACKUP_CODES.ACTION')"
action-button-icon="i-lucide-key"
has-action-button
@primary-action="goToMfaSettings"
/>
</template>
@@ -49,6 +49,11 @@
"DISABLE_MFA_DESC": "Remove two-factor authentication from your account",
"DISABLE_BUTTON": "Disable Two-Factor Authentication"
},
"LOW_BACKUP_CODES": {
"MESSAGE": "You have {count} backup codes remaining. Generate new codes to avoid getting locked out if you lose access to your authenticator.",
"NONE_LEFT": "You have no backup codes remaining. Generate new codes now to avoid getting locked out if you lose access to your authenticator.",
"ACTION": "Generate codes"
},
"DISABLE": {
"TITLE": "Disable Two-Factor Authentication",
"DESCRIPTION": "You'll need to enter your password and either a verification code from your authenticator app or a backup code to disable two-factor authentication.",