From bd20b8fd7f47ce2f802eef427075011fc64acae9 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 8 May 2026 13:08:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Phase=205d-iii=20=E2=80=94=20?= =?UTF-8?q?settings=20Security=20tab=20+=202FA=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 --- frontend/package-lock.json | 23 +++ frontend/package.json | 3 +- frontend/src/pages/settings/SecurityTab.vue | 166 ++++++++++++++++++ frontend/src/pages/settings/SettingsPage.vue | 3 +- .../src/pages/settings/TwoFactorModal.vue | 155 ++++++++++++++++ 5 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/settings/SecurityTab.vue create mode 100644 frontend/src/pages/settings/TwoFactorModal.vue diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d13e6dc7..a019bb0e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "ant-design-vue": "^4.2.6", "axios": "^1.7.9", "moment": "^2.30.1", + "otpauth": "^9.5.1", "qrious": "^4.0.2", "qs": "^6.13.1", "vue": "^3.5.13", @@ -423,6 +424,17 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@oxc-project/types": { "version": "0.128.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", @@ -2219,6 +2231,17 @@ "node": ">= 0.8.0" } }, + "node_modules/otpauth": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.5.1.tgz", + "integrity": "sha512-fJmDAHc8wImfqqqOXIlBvT1dEKrZK0Cmb2VEgScpNTolCz0PHh6ExUZGv4sLtOsWNaHCQlD+rRqaPgnoxFoZjQ==", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9d4f6ffa..147cdd22 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,10 +11,11 @@ "lint": "eslint src --ext .js,.vue" }, "dependencies": { - "ant-design-vue": "^4.2.6", "@ant-design/icons-vue": "^7.0.1", + "ant-design-vue": "^4.2.6", "axios": "^1.7.9", "moment": "^2.30.1", + "otpauth": "^9.5.1", "qrious": "^4.0.2", "qs": "^6.13.1", "vue": "^3.5.13", diff --git a/frontend/src/pages/settings/SecurityTab.vue b/frontend/src/pages/settings/SecurityTab.vue new file mode 100644 index 00000000..562f40df --- /dev/null +++ b/frontend/src/pages/settings/SecurityTab.vue @@ -0,0 +1,166 @@ + + + diff --git a/frontend/src/pages/settings/SettingsPage.vue b/frontend/src/pages/settings/SettingsPage.vue index 26d5da84..493b6c88 100644 --- a/frontend/src/pages/settings/SettingsPage.vue +++ b/frontend/src/pages/settings/SettingsPage.vue @@ -15,6 +15,7 @@ import { useMediaQuery } from '@/composables/useMediaQuery.js'; import AppSidebar from '@/components/AppSidebar.vue'; import { useAllSetting } from './useAllSetting.js'; import GeneralTab from './GeneralTab.vue'; +import SecurityTab from './SecurityTab.vue'; const antdThemeConfig = computed(() => ({ algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm, @@ -214,7 +215,7 @@ const alertVisible = ref(true); Security - +