useSecureVerification.jsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import { useState, useEffect, useCallback } from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import { SecureVerificationService } from '../../services/secureVerification';
  18. import { showError, showSuccess } from '../../helpers';
  19. import { isVerificationRequiredError } from '../../helpers/secureApiCall';
  20. /**
  21. * 通用安全验证 Hook
  22. * @param {Object} options - 配置选项
  23. * @param {Function} options.onSuccess - 验证成功回调
  24. * @param {Function} options.onError - 验证失败回调
  25. * @param {string} options.successMessage - 成功提示消息
  26. * @param {boolean} options.autoReset - 验证完成后是否自动重置状态,默认为 true
  27. */
  28. export const useSecureVerification = ({
  29. onSuccess,
  30. onError,
  31. successMessage,
  32. autoReset = true
  33. } = {}) => {
  34. const { t } = useTranslation();
  35. // 验证方式可用性状态
  36. const [verificationMethods, setVerificationMethods] = useState({
  37. has2FA: false,
  38. hasPasskey: false,
  39. passkeySupported: false
  40. });
  41. // 模态框状态
  42. const [isModalVisible, setIsModalVisible] = useState(false);
  43. // 当前验证状态
  44. const [verificationState, setVerificationState] = useState({
  45. method: null, // '2fa' | 'passkey'
  46. loading: false,
  47. code: '',
  48. apiCall: null
  49. });
  50. // 检查可用的验证方式
  51. const checkVerificationMethods = useCallback(async () => {
  52. const methods = await SecureVerificationService.checkAvailableVerificationMethods();
  53. setVerificationMethods(methods);
  54. return methods;
  55. }, []);
  56. // 初始化时检查验证方式
  57. useEffect(() => {
  58. checkVerificationMethods();
  59. }, [checkVerificationMethods]);
  60. // 重置状态
  61. const resetState = useCallback(() => {
  62. setVerificationState({
  63. method: null,
  64. loading: false,
  65. code: '',
  66. apiCall: null
  67. });
  68. setIsModalVisible(false);
  69. }, []);
  70. // 开始验证流程
  71. const startVerification = useCallback(async (apiCall, options = {}) => {
  72. const { preferredMethod, title, description } = options;
  73. // 检查验证方式
  74. const methods = await checkVerificationMethods();
  75. if (!methods.has2FA && !methods.hasPasskey) {
  76. const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作');
  77. showError(errorMessage);
  78. onError?.(new Error(errorMessage));
  79. return false;
  80. }
  81. // 设置默认验证方式
  82. let defaultMethod = preferredMethod;
  83. if (!defaultMethod) {
  84. if (methods.hasPasskey && methods.passkeySupported) {
  85. defaultMethod = 'passkey';
  86. } else if (methods.has2FA) {
  87. defaultMethod = '2fa';
  88. }
  89. }
  90. setVerificationState(prev => ({
  91. ...prev,
  92. method: defaultMethod,
  93. apiCall,
  94. title,
  95. description
  96. }));
  97. setIsModalVisible(true);
  98. return true;
  99. }, [checkVerificationMethods, onError, t]);
  100. // 执行验证
  101. const executeVerification = useCallback(async (method, code = '') => {
  102. if (!verificationState.apiCall) {
  103. showError(t('验证配置错误'));
  104. return;
  105. }
  106. setVerificationState(prev => ({ ...prev, loading: true }));
  107. try {
  108. // 先调用验证 API,成功后后端会设置 session
  109. await SecureVerificationService.verify(method, code);
  110. // 验证成功,调用业务 API(此时中间件会通过)
  111. const result = await verificationState.apiCall();
  112. // 显示成功消息
  113. if (successMessage) {
  114. showSuccess(successMessage);
  115. }
  116. // 调用成功回调
  117. onSuccess?.(result, method);
  118. // 自动重置状态
  119. if (autoReset) {
  120. resetState();
  121. }
  122. return result;
  123. } catch (error) {
  124. showError(error.message || t('验证失败,请重试'));
  125. onError?.(error);
  126. throw error;
  127. } finally {
  128. setVerificationState(prev => ({ ...prev, loading: false }));
  129. }
  130. }, [verificationState.apiCall, successMessage, onSuccess, onError, autoReset, resetState, t]);
  131. // 设置验证码
  132. const setVerificationCode = useCallback((code) => {
  133. setVerificationState(prev => ({ ...prev, code }));
  134. }, []);
  135. // 切换验证方式
  136. const switchVerificationMethod = useCallback((method) => {
  137. setVerificationState(prev => ({ ...prev, method, code: '' }));
  138. }, []);
  139. // 取消验证
  140. const cancelVerification = useCallback(() => {
  141. resetState();
  142. }, [resetState]);
  143. // 检查是否可以使用某种验证方式
  144. const canUseMethod = useCallback((method) => {
  145. switch (method) {
  146. case '2fa':
  147. return verificationMethods.has2FA;
  148. case 'passkey':
  149. return verificationMethods.hasPasskey && verificationMethods.passkeySupported;
  150. default:
  151. return false;
  152. }
  153. }, [verificationMethods]);
  154. // 获取推荐的验证方式
  155. const getRecommendedMethod = useCallback(() => {
  156. if (verificationMethods.hasPasskey && verificationMethods.passkeySupported) {
  157. return 'passkey';
  158. }
  159. if (verificationMethods.has2FA) {
  160. return '2fa';
  161. }
  162. return null;
  163. }, [verificationMethods]);
  164. /**
  165. * 包装 API 调用,自动处理验证错误
  166. * 当 API 返回需要验证的错误时,自动弹出验证模态框
  167. * @param {Function} apiCall - API 调用函数
  168. * @param {Object} options - 验证选项(同 startVerification)
  169. * @returns {Promise<any>}
  170. */
  171. const withVerification = useCallback(async (apiCall, options = {}) => {
  172. try {
  173. // 直接尝试调用 API
  174. return await apiCall();
  175. } catch (error) {
  176. // 检查是否是需要验证的错误
  177. if (isVerificationRequiredError(error)) {
  178. // 自动触发验证流程
  179. await startVerification(apiCall, options);
  180. // 不抛出错误,让验证模态框处理
  181. return null;
  182. }
  183. // 其他错误继续抛出
  184. throw error;
  185. }
  186. }, [startVerification]);
  187. return {
  188. // 状态
  189. isModalVisible,
  190. verificationMethods,
  191. verificationState,
  192. // 方法
  193. startVerification,
  194. executeVerification,
  195. cancelVerification,
  196. resetState,
  197. setVerificationCode,
  198. switchVerificationMethod,
  199. checkVerificationMethods,
  200. // 辅助方法
  201. canUseMethod,
  202. getRecommendedMethod,
  203. withVerification, // 新增:自动处理验证的包装函数
  204. // 便捷属性
  205. hasAnyVerificationMethod: verificationMethods.has2FA || verificationMethods.hasPasskey,
  206. isLoading: verificationState.loading,
  207. currentMethod: verificationState.method,
  208. code: verificationState.code
  209. };
  210. };