useSecureVerification.jsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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 =
  53. await SecureVerificationService.checkAvailableVerificationMethods();
  54. setVerificationMethods(methods);
  55. return methods;
  56. }, []);
  57. // 初始化时检查验证方式
  58. useEffect(() => {
  59. checkVerificationMethods();
  60. }, [checkVerificationMethods]);
  61. // 重置状态
  62. const resetState = useCallback(() => {
  63. setVerificationState({
  64. method: null,
  65. loading: false,
  66. code: '',
  67. apiCall: null,
  68. });
  69. setIsModalVisible(false);
  70. }, []);
  71. // 开始验证流程
  72. const startVerification = useCallback(
  73. async (apiCall, options = {}) => {
  74. const { preferredMethod, title, description } = options;
  75. // 检查验证方式
  76. const methods = await checkVerificationMethods();
  77. if (!methods.has2FA && !methods.hasPasskey) {
  78. const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作');
  79. showError(errorMessage);
  80. onError?.(new Error(errorMessage));
  81. return false;
  82. }
  83. // 设置默认验证方式
  84. let defaultMethod = preferredMethod;
  85. if (!defaultMethod) {
  86. if (methods.hasPasskey && methods.passkeySupported) {
  87. defaultMethod = 'passkey';
  88. } else if (methods.has2FA) {
  89. defaultMethod = '2fa';
  90. }
  91. }
  92. setVerificationState((prev) => ({
  93. ...prev,
  94. method: defaultMethod,
  95. apiCall,
  96. title,
  97. description,
  98. }));
  99. setIsModalVisible(true);
  100. return true;
  101. },
  102. [checkVerificationMethods, onError, t],
  103. );
  104. // 执行验证
  105. const executeVerification = useCallback(
  106. async (method, code = '') => {
  107. if (!verificationState.apiCall) {
  108. showError(t('验证配置错误'));
  109. return;
  110. }
  111. setVerificationState((prev) => ({ ...prev, loading: true }));
  112. try {
  113. // 先调用验证 API,成功后后端会设置 session
  114. await SecureVerificationService.verify(method, code);
  115. // 验证成功,调用业务 API(此时中间件会通过)
  116. const result = await verificationState.apiCall();
  117. // 显示成功消息
  118. if (successMessage) {
  119. showSuccess(successMessage);
  120. }
  121. // 调用成功回调
  122. onSuccess?.(result, method);
  123. // 自动重置状态
  124. if (autoReset) {
  125. resetState();
  126. }
  127. return result;
  128. } catch (error) {
  129. showError(error.message || t('验证失败,请重试'));
  130. onError?.(error);
  131. throw error;
  132. } finally {
  133. setVerificationState((prev) => ({ ...prev, loading: false }));
  134. }
  135. },
  136. [
  137. verificationState.apiCall,
  138. successMessage,
  139. onSuccess,
  140. onError,
  141. autoReset,
  142. resetState,
  143. t,
  144. ],
  145. );
  146. // 设置验证码
  147. const setVerificationCode = useCallback((code) => {
  148. setVerificationState((prev) => ({ ...prev, code }));
  149. }, []);
  150. // 切换验证方式
  151. const switchVerificationMethod = useCallback((method) => {
  152. setVerificationState((prev) => ({ ...prev, method, code: '' }));
  153. }, []);
  154. // 取消验证
  155. const cancelVerification = useCallback(() => {
  156. resetState();
  157. }, [resetState]);
  158. // 检查是否可以使用某种验证方式
  159. const canUseMethod = useCallback(
  160. (method) => {
  161. switch (method) {
  162. case '2fa':
  163. return verificationMethods.has2FA;
  164. case 'passkey':
  165. return (
  166. verificationMethods.hasPasskey &&
  167. verificationMethods.passkeySupported
  168. );
  169. default:
  170. return false;
  171. }
  172. },
  173. [verificationMethods],
  174. );
  175. // 获取推荐的验证方式
  176. const getRecommendedMethod = useCallback(() => {
  177. if (
  178. verificationMethods.hasPasskey &&
  179. verificationMethods.passkeySupported
  180. ) {
  181. return 'passkey';
  182. }
  183. if (verificationMethods.has2FA) {
  184. return '2fa';
  185. }
  186. return null;
  187. }, [verificationMethods]);
  188. /**
  189. * 包装 API 调用,自动处理验证错误
  190. * 当 API 返回需要验证的错误时,自动弹出验证模态框
  191. * @param {Function} apiCall - API 调用函数
  192. * @param {Object} options - 验证选项(同 startVerification)
  193. * @returns {Promise<any>}
  194. */
  195. const withVerification = useCallback(
  196. async (apiCall, options = {}) => {
  197. try {
  198. // 直接尝试调用 API
  199. return await apiCall();
  200. } catch (error) {
  201. // 检查是否是需要验证的错误
  202. if (isVerificationRequiredError(error)) {
  203. // 自动触发验证流程
  204. await startVerification(apiCall, options);
  205. // 不抛出错误,让验证模态框处理
  206. return null;
  207. }
  208. // 其他错误继续抛出
  209. throw error;
  210. }
  211. },
  212. [startVerification],
  213. );
  214. return {
  215. // 状态
  216. isModalVisible,
  217. verificationMethods,
  218. verificationState,
  219. // 方法
  220. startVerification,
  221. executeVerification,
  222. cancelVerification,
  223. resetState,
  224. setVerificationCode,
  225. switchVerificationMethod,
  226. checkVerificationMethods,
  227. // 辅助方法
  228. canUseMethod,
  229. getRecommendedMethod,
  230. withVerification, // 新增:自动处理验证的包装函数
  231. // 便捷属性
  232. hasAnyVerificationMethod:
  233. verificationMethods.has2FA || verificationMethods.hasPasskey,
  234. isLoading: verificationState.loading,
  235. currentMethod: verificationState.method,
  236. code: verificationState.code,
  237. };
  238. };