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 @@
+
+
+
+
Save your backup codes
+
+ Store these codes somewhere safe. Each can only be used once if you lose access to your authenticator app.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ Two-factor authentication adds an extra layer of security. You will need an authenticator app (e.g. Google Authenticator, Authy) on your phone.
+
+
+
{{ error }}
+
+
+
+
+
+
+
+
+ Open your authenticator app and scan this QR code, or enter the key manually.
+
+
+
+
+
+
+
+
+
Or enter this secret key manually:
+
+
{{ secret }}
+
+
+
Copied!
+
+
+
+
+
+
+
{{ error }}
+
+
+
+ Authenticator connected.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
{{ message }}
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+ {{ totpError }}
+
+
+
+
+
+
+
+
+
+
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 {