| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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'; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export const useSecureVerification = ({ |
| | onSuccess, |
| | onError, |
| | successMessage, |
| | autoReset = true, |
| | } = {}) => { |
| | const { t } = useTranslation(); |
| |
|
| | |
| | const [verificationMethods, setVerificationMethods] = useState({ |
| | has2FA: false, |
| | hasPasskey: false, |
| | passkeySupported: false, |
| | }); |
| |
|
| | |
| | const [isModalVisible, setIsModalVisible] = useState(false); |
| |
|
| | |
| | const [verificationState, setVerificationState] = useState({ |
| | method: null, |
| | loading: false, |
| | code: '', |
| | apiCall: null, |
| | }); |
| |
|
| | |
| | const checkVerificationMethods = useCallback(async () => { |
| | const methods = |
| | await SecureVerificationService.checkAvailableVerificationMethods(); |
| | setVerificationMethods(methods); |
| | return methods; |
| | }, []); |
| |
|
| | |
| | useEffect(() => { |
| | checkVerificationMethods(); |
| | }, [checkVerificationMethods]); |
| |
|
| | |
| | const resetState = useCallback(() => { |
| | setVerificationState({ |
| | method: null, |
| | loading: false, |
| | code: '', |
| | apiCall: null, |
| | }); |
| | setIsModalVisible(false); |
| | }, []); |
| |
|
| | |
| | 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); |
| | onError?.(new Error(errorMessage)); |
| | return false; |
| | } |
| |
|
| | |
| | let defaultMethod = preferredMethod; |
| | if (!defaultMethod) { |
| | if (methods.hasPasskey && methods.passkeySupported) { |
| | defaultMethod = 'passkey'; |
| | } else if (methods.has2FA) { |
| | defaultMethod = '2fa'; |
| | } |
| | } |
| |
|
| | setVerificationState((prev) => ({ |
| | ...prev, |
| | method: defaultMethod, |
| | apiCall, |
| | title, |
| | description, |
| | })); |
| | setIsModalVisible(true); |
| |
|
| | return true; |
| | }, |
| | [checkVerificationMethods, onError, t], |
| | ); |
| |
|
| | |
| | const executeVerification = useCallback( |
| | async (method, code = '') => { |
| | if (!verificationState.apiCall) { |
| | showError(t('验证配置错误')); |
| | return; |
| | } |
| |
|
| | setVerificationState((prev) => ({ ...prev, loading: true })); |
| |
|
| | try { |
| | |
| | await SecureVerificationService.verify(method, code); |
| |
|
| | |
| | const result = await verificationState.apiCall(); |
| |
|
| | |
| | if (successMessage) { |
| | showSuccess(successMessage); |
| | } |
| |
|
| | |
| | onSuccess?.(result, method); |
| |
|
| | |
| | if (autoReset) { |
| | resetState(); |
| | } |
| |
|
| | return result; |
| | } catch (error) { |
| | showError(error.message || t('验证失败,请重试')); |
| | onError?.(error); |
| | throw error; |
| | } finally { |
| | setVerificationState((prev) => ({ ...prev, loading: false })); |
| | } |
| | }, |
| | [ |
| | verificationState.apiCall, |
| | successMessage, |
| | onSuccess, |
| | onError, |
| | autoReset, |
| | resetState, |
| | t, |
| | ], |
| | ); |
| |
|
| | |
| | const setVerificationCode = useCallback((code) => { |
| | setVerificationState((prev) => ({ ...prev, code })); |
| | }, []); |
| |
|
| | |
| | const switchVerificationMethod = useCallback((method) => { |
| | setVerificationState((prev) => ({ ...prev, method, code: '' })); |
| | }, []); |
| |
|
| | |
| | const cancelVerification = useCallback(() => { |
| | resetState(); |
| | }, [resetState]); |
| |
|
| | |
| | const canUseMethod = useCallback( |
| | (method) => { |
| | switch (method) { |
| | case '2fa': |
| | return verificationMethods.has2FA; |
| | case 'passkey': |
| | return ( |
| | verificationMethods.hasPasskey && |
| | verificationMethods.passkeySupported |
| | ); |
| | default: |
| | return false; |
| | } |
| | }, |
| | [verificationMethods], |
| | ); |
| |
|
| | |
| | const getRecommendedMethod = useCallback(() => { |
| | if ( |
| | verificationMethods.hasPasskey && |
| | verificationMethods.passkeySupported |
| | ) { |
| | return 'passkey'; |
| | } |
| | if (verificationMethods.has2FA) { |
| | return '2fa'; |
| | } |
| | return null; |
| | }, [verificationMethods]); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const withVerification = useCallback( |
| | async (apiCall, options = {}) => { |
| | try { |
| | |
| | return await apiCall(); |
| | } catch (error) { |
| | |
| | if (isVerificationRequiredError(error)) { |
| | |
| | await startVerification(apiCall, options); |
| | |
| | return null; |
| | } |
| | |
| | throw error; |
| | } |
| | }, |
| | [startVerification], |
| | ); |
| |
|
| | return { |
| | |
| | isModalVisible, |
| | verificationMethods, |
| | verificationState, |
| |
|
| | |
| | startVerification, |
| | executeVerification, |
| | cancelVerification, |
| | resetState, |
| | setVerificationCode, |
| | switchVerificationMethod, |
| | checkVerificationMethods, |
| |
|
| | |
| | canUseMethod, |
| | getRecommendedMethod, |
| | withVerification, |
| |
|
| | |
| | hasAnyVerificationMethod: |
| | verificationMethods.has2FA || verificationMethods.hasPasskey, |
| | isLoading: verificationState.loading, |
| | currentMethod: verificationState.method, |
| | code: verificationState.code, |
| | }; |
| | }; |
| |
|