From f32b81f3f34619600bc644973f1912c4de1e8036 Mon Sep 17 00:00:00 2001 From: smail Date: Tue, 8 Sep 2026 12:57:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=BA=E5=8C=96=E7=94=A8=E6=88=B7=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E4=BF=A1=E6=81=AF=E7=BC=96=E8=BE=91=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/recentAuthentication.js | 30 +++++++++++-------- .../UserManagement/UserManagement.js | 30 +++++++++++++++++-- .../UserManagement/UserManagement.vue | 9 +++++- test/frontend/user-management.test.mjs | 13 ++++++++ 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/frontend/src/utils/recentAuthentication.js b/frontend/src/utils/recentAuthentication.js index a6d97b5..812cc05 100644 --- a/frontend/src/utils/recentAuthentication.js +++ b/frontend/src/utils/recentAuthentication.js @@ -8,19 +8,7 @@ export function isRecentAuthenticationCancelled(error) { return error?.code === cancelledCode } -export async function requestWithRecentAuthentication(request) { - let initialResponse - let requiresRecentAuthentication = false - try { - initialResponse = await request() - } catch (error) { - if (error?.apiErrorCode !== recentAuthenticationRequired) throw error - requiresRecentAuthentication = true - } - if (!requiresRecentAuthentication && initialResponse?.data?.code !== recentAuthenticationRequired) { - return initialResponse - } - +export async function confirmRecentAuthentication() { let password = '' try { const result = await ElMessageBox.prompt( @@ -50,6 +38,22 @@ export async function requestWithRecentAuthentication(request) { } finally { password = '' } +} + +export async function requestWithRecentAuthentication(request) { + let initialResponse + let requiresRecentAuthentication = false + try { + initialResponse = await request() + } catch (error) { + if (error?.apiErrorCode !== recentAuthenticationRequired) throw error + requiresRecentAuthentication = true + } + if (!requiresRecentAuthentication && initialResponse?.data?.code !== recentAuthenticationRequired) { + return initialResponse + } + + await confirmRecentAuthentication() return request() } diff --git a/frontend/src/views/components/UserManagement/UserManagement.js b/frontend/src/views/components/UserManagement/UserManagement.js index 4f13674..bae5039 100644 --- a/frontend/src/views/components/UserManagement/UserManagement.js +++ b/frontend/src/views/components/UserManagement/UserManagement.js @@ -9,6 +9,7 @@ import {ElNotification} from "element-plus"; import {Plus} from "@element-plus/icons-vue"; import {useGlobalStatusStore} from "@/stores/useGlobalStatusStore"; +import {confirmRecentAuthentication, isRecentAuthenticationCancelled} from '@/utils/recentAuthentication' const roleOptions = { super_admin: { label: '超级管理员', type: 'danger' }, @@ -74,6 +75,8 @@ export function usePage() { const externalEmailDialogVisible = ref(false) + const securityDialogOpening = ref('') + const editingCurrentUser = computed(() => isSameUser(selectedUser.value, globalStatus.userInfos)) const recoveryCodeWebManaged = computed(() => canManageRecoveryCode(selectedUser.value)) @@ -122,13 +125,35 @@ export function usePage() { userInfoDialog.value = true } - const openRecoveryCodeDialog = function () { + const confirmSecurityDialogAccess = async function (dialogName) { + if (securityDialogOpening.value) return false + securityDialogOpening.value = dialogName + try { + await confirmRecentAuthentication() + return true + } catch (error) { + if (!isRecentAuthenticationCancelled(error)) { + ElNotification({ + title: lang.fail, + message: error?.message || '身份确认失败', + type: 'error', + }) + } + return false + } finally { + securityDialogOpening.value = '' + } + } + + const openRecoveryCodeDialog = async function () { if (!recoveryCodeWebManaged.value) return + if (!await confirmSecurityDialogAccess('recovery-code')) return recoveryCodeDialogVisible.value = true } - const openExternalEmailDialog = function () { + const openExternalEmailDialog = async function () { if (!selectedUser.value?.ID) return + if (!await confirmSecurityDialogAccess('external-email')) return externalEmailDialogVisible.value = true } @@ -200,6 +225,7 @@ export function usePage() { recoveryCodeWebManaged, recoveryCodeDialogVisible, externalEmailDialogVisible, + securityDialogOpening, openRecoveryCodeDialog, openExternalEmailDialog, handleSecurityChanged, diff --git a/frontend/src/views/components/UserManagement/UserManagement.vue b/frontend/src/views/components/UserManagement/UserManagement.vue index 1ea332c..19acaac 100644 --- a/frontend/src/views/components/UserManagement/UserManagement.vue +++ b/frontend/src/views/components/UserManagement/UserManagement.vue @@ -122,6 +122,7 @@ {{ recoveryCodeWebManaged ? '管理安全码' : '仅支持本机管理' }} @@ -137,7 +138,12 @@ ).label }} - 编辑外部邮箱 + + 编辑外部邮箱 + @@ -201,6 +207,7 @@ const { recoveryCodeWebManaged, recoveryCodeDialogVisible, externalEmailDialogVisible, + securityDialogOpening, openRecoveryCodeDialog, openExternalEmailDialog, handleSecurityChanged, diff --git a/test/frontend/user-management.test.mjs b/test/frontend/user-management.test.mjs index 47c73c8..808a6c1 100644 --- a/test/frontend/user-management.test.mjs +++ b/test/frontend/user-management.test.mjs @@ -75,6 +75,19 @@ test('用户安全信息使用独立弹窗并调用固定管理接口', () => { assert.doesNotMatch(externalSource, /\/api\/v1\/security\/external-email\/bind/) }) +test('打开安全码或外部邮箱编辑弹窗前必须确认当前登录密码', () => { + const source = read('UserManagement.js') + assert.match(source, /confirmRecentAuthentication/) + assert.match(source, /confirmSecurityDialogAccess\('recovery-code'\)/) + assert.match(source, /confirmSecurityDialogAccess\('external-email'\)/) + assert.match(source, /if \(!await confirmSecurityDialogAccess\('recovery-code'\)\) return/) + assert.match(source, /if \(!await confirmSecurityDialogAccess\('external-email'\)\) return/) + + const template = read('UserManagement.vue') + assert.match(template, /securityDialogOpening === 'recovery-code'/) + assert.match(template, /securityDialogOpening === 'external-email'/) +}) + test('当前用户判断优先使用用户 ID 并保留账号兼容回退', () => { const { isSameUser } = loadStatusHelpers() assert.equal(isSameUser({ ID: 7, Account: 'same' }, { id: 7, account: 'other' }), true)