-
-
+
+
+
+
+ {t('使用 Passkey 验证')}
+
+
+ {t('点击验证按钮,使用您的生物特征或安全密钥')}
+
-
+
+
+
+
+
+
)}
diff --git a/web/src/components/settings/SystemSetting.jsx b/web/src/components/settings/SystemSetting.jsx
index abb55301a..f0c2dbc3a 100644
--- a/web/src/components/settings/SystemSetting.jsx
+++ b/web/src/components/settings/SystemSetting.jsx
@@ -1043,7 +1043,7 @@ const SystemSetting = () => {
handleCheckboxChange('passkey.enabled', e)
}
>
- {t('允许通过 Passkey 登录 & 注册')}
+ {t('允许通过 Passkey 登录 & 认证')}
diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx
index 27499f824..54b4525d6 100644
--- a/web/src/components/table/channels/modals/EditChannelModal.jsx
+++ b/web/src/components/table/channels/modals/EditChannelModal.jsx
@@ -206,7 +206,7 @@ const EditChannelModal = (props) => {
isModalVisible,
verificationMethods,
verificationState,
- startVerification,
+ withVerification,
executeVerification,
cancelVerification,
setVerificationCode,
@@ -214,12 +214,20 @@ const EditChannelModal = (props) => {
} = useSecureVerification({
onSuccess: (result) => {
// 验证成功后显示密钥
- if (result.success && result.data?.key) {
+ console.log('Verification success, result:', result);
+ if (result && result.success && result.data?.key) {
showSuccess(t('密钥获取成功'));
setKeyDisplayState({
showModal: true,
keyData: result.data.key,
});
+ } else if (result && result.key) {
+ // 直接返回了 key(没有包装在 data 中)
+ showSuccess(t('密钥获取成功'));
+ setKeyDisplayState({
+ showModal: true,
+ keyData: result.key,
+ });
}
},
});
@@ -604,19 +612,30 @@ const EditChannelModal = (props) => {
}
};
- // 显示安全验证模态框并开始验证流程
+ // 查看渠道密钥(透明验证)
const handleShow2FAModal = async () => {
try {
- const apiCall = createApiCalls.viewChannelKey(channelId);
-
- await startVerification(apiCall, {
- title: t('查看渠道密钥'),
- description: t('为了保护账户安全,请验证您的身份。'),
- preferredMethod: 'passkey', // 优先使用 Passkey
- });
+ // 使用 withVerification 包装,会自动处理需要验证的情况
+ const result = await withVerification(
+ createApiCalls.viewChannelKey(channelId),
+ {
+ title: t('查看渠道密钥'),
+ description: t('为了保护账户安全,请验证您的身份。'),
+ preferredMethod: 'passkey', // 优先使用 Passkey
+ }
+ );
+
+ // 如果直接返回了结果(已验证),显示密钥
+ if (result && result.success && result.data?.key) {
+ showSuccess(t('密钥获取成功'));
+ setKeyDisplayState({
+ showModal: true,
+ keyData: result.data.key,
+ });
+ }
} catch (error) {
- console.error('Failed to start verification:', error);
- showError(error.message || t('启动验证失败'));
+ console.error('Failed to view channel key:', error);
+ showError(error.message || t('获取密钥失败'));
}
};
diff --git a/web/src/helpers/secureApiCall.js b/web/src/helpers/secureApiCall.js
new file mode 100644
index 000000000..b82a6ae92
--- /dev/null
+++ b/web/src/helpers/secureApiCall.js
@@ -0,0 +1,62 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see
.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+/**
+ * 安全 API 调用包装器
+ * 自动处理需要验证的 403 错误,透明地触发验证流程
+ */
+
+/**
+ * 检查错误是否是需要安全验证的错误
+ * @param {Error} error - 错误对象
+ * @returns {boolean}
+ */
+export function isVerificationRequiredError(error) {
+ if (!error.response) return false;
+
+ const { status, data } = error.response;
+
+ // 检查是否是 403 错误且包含验证相关的错误码
+ if (status === 403 && data) {
+ const verificationCodes = [
+ 'VERIFICATION_REQUIRED',
+ 'VERIFICATION_EXPIRED',
+ 'VERIFICATION_INVALID'
+ ];
+
+ return verificationCodes.includes(data.code);
+ }
+
+ return false;
+}
+
+/**
+ * 从错误中提取验证需求信息
+ * @param {Error} error - 错误对象
+ * @returns {Object} 验证需求信息
+ */
+export function extractVerificationInfo(error) {
+ const data = error.response?.data || {};
+
+ return {
+ code: data.code,
+ message: data.message || '需要安全验证',
+ required: true
+ };
+}
\ No newline at end of file
diff --git a/web/src/hooks/common/useSecureVerification.jsx b/web/src/hooks/common/useSecureVerification.jsx
index 271345d1c..e60a104db 100644
--- a/web/src/hooks/common/useSecureVerification.jsx
+++ b/web/src/hooks/common/useSecureVerification.jsx
@@ -21,6 +21,7 @@ import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { SecureVerificationService } from '../../services/secureVerification';
import { showError, showSuccess } from '../../helpers';
+import { isVerificationRequiredError } from '../../helpers/secureApiCall';
/**
* 通用安全验证 Hook
@@ -82,10 +83,10 @@ export const useSecureVerification = ({
// 开始验证流程
const startVerification = useCallback(async (apiCall, options = {}) => {
const { preferredMethod, title, description } = options;
-
+
// 检查验证方式
const methods = await checkVerificationMethods();
-
+
if (!methods.has2FA && !methods.hasPasskey) {
const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作');
showError(errorMessage);
@@ -111,7 +112,7 @@ export const useSecureVerification = ({
description
}));
setIsModalVisible(true);
-
+
return true;
}, [checkVerificationMethods, onError, t]);
@@ -125,10 +126,11 @@ export const useSecureVerification = ({
setVerificationState(prev => ({ ...prev, loading: true }));
try {
- const result = await SecureVerificationService.verify(method, {
- code,
- apiCall: verificationState.apiCall
- });
+ // 先调用验证 API,成功后后端会设置 session
+ await SecureVerificationService.verify(method, code);
+
+ // 验证成功,调用业务 API(此时中间件会通过)
+ const result = await verificationState.apiCall();
// 显示成功消息
if (successMessage) {
@@ -191,12 +193,36 @@ export const useSecureVerification = ({
return null;
}, [verificationMethods]);
+ /**
+ * 包装 API 调用,自动处理验证错误
+ * 当 API 返回需要验证的错误时,自动弹出验证模态框
+ * @param {Function} apiCall - API 调用函数
+ * @param {Object} options - 验证选项(同 startVerification)
+ * @returns {Promise
}
+ */
+ const withVerification = useCallback(async (apiCall, options = {}) => {
+ try {
+ // 直接尝试调用 API
+ return await apiCall();
+ } catch (error) {
+ // 检查是否是需要验证的错误
+ if (isVerificationRequiredError(error)) {
+ // 自动触发验证流程
+ await startVerification(apiCall, options);
+ // 不抛出错误,让验证模态框处理
+ return null;
+ }
+ // 其他错误继续抛出
+ throw error;
+ }
+ }, [startVerification]);
+
return {
// 状态
isModalVisible,
verificationMethods,
verificationState,
-
+
// 方法
startVerification,
executeVerification,
@@ -205,11 +231,12 @@ export const useSecureVerification = ({
setVerificationCode,
switchVerificationMethod,
checkVerificationMethods,
-
+
// 辅助方法
canUseMethod,
getRecommendedMethod,
-
+ withVerification, // 新增:自动处理验证的包装函数
+
// 便捷属性
hasAnyVerificationMethod: verificationMethods.has2FA || verificationMethods.hasPasskey,
isLoading: verificationState.loading,
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index e221c3b28..5586e0a83 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -333,6 +333,7 @@
"通过密码注册时需要进行邮箱验证": "Email verification is required when registering via password",
"允许通过 GitHub 账户登录 & 注册": "Allow login & registration via GitHub account",
"允许通过微信登录 & 注册": "Allow login & registration via WeChat",
+ "允许通过 Passkey 登录 & 认证": "Allow login & authentication via Passkey",
"允许新用户注册(此项为否时,新用户将无法以任何方式进行注册": "Allow new user registration (if this option is off, new users will not be able to register in any way",
"启用 Turnstile 用户校验": "Enable Turnstile user verification",
"配置 SMTP": "Configure SMTP",
diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json
index 26c418205..e6dafac18 100644
--- a/web/src/i18n/locales/zh.json
+++ b/web/src/i18n/locales/zh.json
@@ -87,5 +87,6 @@
"此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。": "此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。",
"目标用户:{{username}}": "目标用户:{{username}}",
"Passkey 已重置": "Passkey 已重置",
- "二步验证已重置": "二步验证已重置"
+ "二步验证已重置": "二步验证已重置",
+ "允许通过 Passkey 登录 & 认证": "允许通过 Passkey 登录 & 认证"
}
diff --git a/web/src/services/secureVerification.js b/web/src/services/secureVerification.js
index 1af53204b..93cdd0a4d 100644
--- a/web/src/services/secureVerification.js
+++ b/web/src/services/secureVerification.js
@@ -18,14 +18,15 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { API, showError } from '../helpers';
-import {
- prepareCredentialRequestOptions,
- buildAssertionResult,
- isPasskeySupported
+import {
+ prepareCredentialRequestOptions,
+ buildAssertionResult,
+ isPasskeySupported
} from '../helpers/passkey';
/**
* 通用安全验证服务
+ * 验证状态完全由后端 Session 控制,前端不存储任何状态
*/
export class SecureVerificationService {
/**
@@ -81,36 +82,41 @@ export class SecureVerificationService {
/**
* 执行2FA验证
* @param {string} code - 验证码
- * @param {Function} apiCall - API调用函数,接收 {method: '2fa', code} 参数
- * @returns {Promise} API响应结果
+ * @returns {Promise}
*/
- static async verify2FA(code, apiCall) {
+ static async verify2FA(code) {
if (!code?.trim()) {
throw new Error('请输入验证码或备用码');
}
- return await apiCall({
+ // 调用通用验证 API,验证成功后后端会设置 session
+ const verifyResponse = await API.post('/api/verify', {
method: '2fa',
code: code.trim()
});
+
+ if (!verifyResponse.data?.success) {
+ throw new Error(verifyResponse.data?.message || '验证失败');
+ }
+
+ // 验证成功,session 已在后端设置
}
/**
* 执行Passkey验证
- * @param {Function} apiCall - API调用函数,接收 {method: 'passkey'} 参数
- * @returns {Promise} API响应结果
+ * @returns {Promise}
*/
- static async verifyPasskey(apiCall) {
+ static async verifyPasskey() {
try {
// 开始Passkey验证
const beginResponse = await API.post('/api/user/passkey/verify/begin');
- if (!beginResponse.success) {
- throw new Error(beginResponse.message);
+ if (!beginResponse.data?.success) {
+ throw new Error(beginResponse.data?.message || '开始验证失败');
}
// 准备WebAuthn选项
- const publicKey = prepareCredentialRequestOptions(beginResponse.data);
-
+ const publicKey = prepareCredentialRequestOptions(beginResponse.data.data.options);
+
// 执行WebAuthn验证
const credential = await navigator.credentials.get({ publicKey });
if (!credential) {
@@ -119,17 +125,23 @@ export class SecureVerificationService {
// 构建验证结果
const assertionResult = buildAssertionResult(credential);
-
+
// 完成验证
const finishResponse = await API.post('/api/user/passkey/verify/finish', assertionResult);
- if (!finishResponse.success) {
- throw new Error(finishResponse.message);
+ if (!finishResponse.data?.success) {
+ throw new Error(finishResponse.data?.message || '验证失败');
}
- // 调用业务API
- return await apiCall({
+ // 调用通用验证 API 设置 session(Passkey 验证已完成)
+ const verifyResponse = await API.post('/api/verify', {
method: 'passkey'
});
+
+ if (!verifyResponse.data?.success) {
+ throw new Error(verifyResponse.data?.message || '验证失败');
+ }
+
+ // 验证成功,session 已在后端设置
} catch (error) {
if (error.name === 'NotAllowedError') {
throw new Error('Passkey 验证被取消或超时');
@@ -144,17 +156,15 @@ export class SecureVerificationService {
/**
* 通用验证方法,根据验证类型执行相应的验证流程
* @param {string} method - 验证方式: '2fa' | 'passkey'
- * @param {Object} params - 参数对象
- * @param {string} params.code - 2FA验证码(当method为'2fa'时必需)
- * @param {Function} params.apiCall - API调用函数
- * @returns {Promise} API响应结果
+ * @param {string} code - 2FA验证码(当method为'2fa'时必需)
+ * @returns {Promise}
*/
- static async verify(method, { code, apiCall }) {
+ static async verify(method, code = '') {
switch (method) {
case '2fa':
- return await this.verify2FA(code, apiCall);
+ return await this.verify2FA(code);
case 'passkey':
- return await this.verifyPasskey(apiCall);
+ return await this.verifyPasskey();
default:
throw new Error(`不支持的验证方式: ${method}`);
}
@@ -169,8 +179,10 @@ export const createApiCalls = {
* 创建查看渠道密钥的API调用
* @param {number} channelId - 渠道ID
*/
- viewChannelKey: (channelId) => async (verificationData) => {
- return await API.post(`/api/channel/${channelId}/key`, verificationData);
+ viewChannelKey: (channelId) => async () => {
+ // 新系统中,验证已通过中间件处理,直接调用 API 即可
+ const response = await API.post(`/api/channel/${channelId}/key`, {});
+ return response.data;
},
/**
@@ -179,20 +191,27 @@ export const createApiCalls = {
* @param {string} method - HTTP方法,默认为 'POST'
* @param {Object} extraData - 额外的请求数据
*/
- custom: (url, method = 'POST', extraData = {}) => async (verificationData) => {
- const data = { ...extraData, ...verificationData };
-
+ custom: (url, method = 'POST', extraData = {}) => async () => {
+ // 新系统中,验证已通过中间件处理
+ const data = extraData;
+
+ let response;
switch (method.toUpperCase()) {
case 'GET':
- return await API.get(url, { params: data });
+ response = await API.get(url, { params: data });
+ break;
case 'POST':
- return await API.post(url, data);
+ response = await API.post(url, data);
+ break;
case 'PUT':
- return await API.put(url, data);
+ response = await API.put(url, data);
+ break;
case 'DELETE':
- return await API.delete(url, { data });
+ response = await API.delete(url, { data });
+ break;
default:
throw new Error(`不支持的HTTP方法: ${method}`);
}
+ return response.data;
}
};
\ No newline at end of file