From d73e2f6112206165b362b9e4aeeddfe2b0b663fe Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 22 May 2026 19:54:53 +0200 Subject: [PATCH] feat(02-03): TOTP enrollment flow, backup codes, AccountView, ConfirmBlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TotpEnrollment.vue: three-step enrollment (setup → verify → backup-codes); emits 'enrolled' - BackupCodesDisplay.vue: 2-column grid, copy-all clipboard, acknowledgment checkbox - ConfirmBlock.vue: reusable inline confirmation block with 'confirmed'/'cancelled' emits - AccountView.vue: TOTP section (enrollment or disable), change-password with breach/wrong-pw error handling, sign-out-all with ConfirmBlock - npm run build exits 0 --- .../components/auth/BackupCodesDisplay.vue | 84 ++++++++ .../src/components/auth/TotpEnrollment.vue | 181 ++++++++++++++++++ frontend/src/components/ui/ConfirmBlock.vue | 45 +++++ frontend/src/views/AccountView.vue | 166 +++++++++++++--- 4 files changed, 446 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/auth/BackupCodesDisplay.vue create mode 100644 frontend/src/components/auth/TotpEnrollment.vue create mode 100644 frontend/src/components/ui/ConfirmBlock.vue diff --git a/frontend/src/components/auth/BackupCodesDisplay.vue b/frontend/src/components/auth/BackupCodesDisplay.vue new file mode 100644 index 0000000..e11b4d6 --- /dev/null +++ b/frontend/src/components/auth/BackupCodesDisplay.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/src/components/auth/TotpEnrollment.vue b/frontend/src/components/auth/TotpEnrollment.vue new file mode 100644 index 0000000..d7bfa72 --- /dev/null +++ b/frontend/src/components/auth/TotpEnrollment.vue @@ -0,0 +1,181 @@ + + + diff --git a/frontend/src/components/ui/ConfirmBlock.vue b/frontend/src/components/ui/ConfirmBlock.vue new file mode 100644 index 0000000..f3851af --- /dev/null +++ b/frontend/src/components/ui/ConfirmBlock.vue @@ -0,0 +1,45 @@ + + + diff --git a/frontend/src/views/AccountView.vue b/frontend/src/views/AccountView.vue index 53e059f..cf6a44d 100644 --- a/frontend/src/views/AccountView.vue +++ b/frontend/src/views/AccountView.vue @@ -3,7 +3,8 @@

Account settings

- + +

Account information

@@ -22,7 +23,50 @@
- + +
+

Two-factor authentication

+ + + + + + +
+ +

Change password

@@ -33,29 +77,49 @@ v-model="currentPassword" type="password" required - class="block w-full rounded-lg px-3 py-3 text-sm border border-gray-300 bg-white text-gray-900 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:outline-none" + autocomplete="current-password" + class="block w-full rounded-lg px-3 py-3 text-sm border bg-white text-gray-900 transition-colors focus:ring-2 focus:outline-none" + :class="passwordError && passwordError.includes('Current') + ? 'border-red-500 focus:ring-red-500 focus:border-red-500' + : 'border-gray-300 focus:ring-indigo-500 focus:border-indigo-500'" /> +

+ {{ passwordError }} +

+
-
+ +
{{ passwordError }}
+
{{ passwordSuccess }}
@@ -108,20 +174,21 @@ import { useRouter } from 'vue-router' import { useAuthStore } from '../stores/auth.js' import * as api from '../api/client.js' import PasswordStrengthBar from '../components/auth/PasswordStrengthBar.vue' +import TotpEnrollment from '../components/auth/TotpEnrollment.vue' +import ConfirmBlock from '../components/ui/ConfirmBlock.vue' import AppSpinner from '../components/ui/AppSpinner.vue' const authStore = useAuthStore() const router = useRouter() +// ── Change password ───────────────────────────────────────────────────────── + const currentPassword = ref('') const newPassword = ref('') const changingPassword = ref(false) const passwordError = ref(null) const passwordSuccess = ref(null) -const confirmSignOutAll = ref(false) -const signingOutAll = ref(false) - async function changePassword() { changingPassword.value = true passwordError.value = null @@ -135,12 +202,51 @@ async function changePassword() { currentPassword.value = '' newPassword.value = '' } catch (e) { - passwordError.value = e.message + const msg = e.message || '' + if (msg.toLowerCase().includes('current') || msg.toLowerCase().includes('incorrect')) { + // Wrong current password — show inline below the field + passwordError.value = 'Current password is incorrect' + } else if (msg.toLowerCase().includes('breach')) { + // HIBP breach detected + passwordError.value = 'This password has appeared in a data breach. Choose a different password.' + } else { + passwordError.value = msg + } } finally { changingPassword.value = false } } +// ── TOTP enrollment ───────────────────────────────────────────────────────── + +const confirmDisable2fa = ref(false) +const totpError = ref(null) + +function onTotpEnrolled() { + // Update user totp_enabled flag in store + if (authStore.user) { + authStore.user.totp_enabled = true + } +} + +async function disableTotp() { + totpError.value = null + try { + await api.totpDisable() + if (authStore.user) { + authStore.user.totp_enabled = false + } + confirmDisable2fa.value = false + } catch (e) { + totpError.value = e.message + } +} + +// ── Sign out all devices ──────────────────────────────────────────────────── + +const confirmSignOutAll = ref(false) +const signingOutAll = ref(false) + async function signOutAll() { signingOutAll.value = true try {