161 lines
4.2 KiB
TypeScript
161 lines
4.2 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { useAntibot } from '../contexts/AntibotContext';
|
|
|
|
interface UseAntibotFormOptions {
|
|
formId: string;
|
|
minTimeOnPage?: number;
|
|
minTimeToFill?: number;
|
|
requireRecaptcha?: boolean;
|
|
checkMouseMovements?: boolean;
|
|
maxAttempts?: number;
|
|
onValidationError?: (errors: string[]) => void;
|
|
onValidationWarning?: (warnings: string[]) => void;
|
|
}
|
|
|
|
interface UseAntibotFormReturn {
|
|
honeypotValue: string;
|
|
setHoneypotValue: (value: string) => void;
|
|
recaptchaToken: string | null;
|
|
setRecaptchaToken: (token: string | null) => void;
|
|
isValidating: boolean;
|
|
validate: () => Promise<boolean>;
|
|
reset: () => void;
|
|
refreshRateLimit: () => void;
|
|
rateLimitInfo: {
|
|
allowed: boolean;
|
|
remainingAttempts: number;
|
|
resetTime: number;
|
|
} | null;
|
|
}
|
|
|
|
/**
|
|
* Hook for form-level antibot protection
|
|
*/
|
|
export const useAntibotForm = (options: UseAntibotFormOptions): UseAntibotFormReturn => {
|
|
const {
|
|
formId,
|
|
minTimeOnPage = 5000,
|
|
minTimeToFill = 3000,
|
|
requireRecaptcha = false,
|
|
checkMouseMovements = true,
|
|
maxAttempts = 5,
|
|
onValidationError,
|
|
onValidationWarning,
|
|
} = options;
|
|
|
|
const antibot = useAntibot();
|
|
const [honeypotValue, setHoneypotValue] = useState('');
|
|
const [recaptchaToken, setRecaptchaToken] = useState<string | null>(null);
|
|
const [isValidating, setIsValidating] = useState(false);
|
|
const [rateLimitInfo, setRateLimitInfo] = useState<{
|
|
allowed: boolean;
|
|
remainingAttempts: number;
|
|
resetTime: number;
|
|
} | null>(null);
|
|
const formStartedRef = useRef(false);
|
|
|
|
// Start tracking when form is mounted
|
|
useEffect(() => {
|
|
if (!formStartedRef.current) {
|
|
antibot.startFormTracking();
|
|
formStartedRef.current = true;
|
|
}
|
|
}, [antibot]);
|
|
|
|
// Check rate limit on mount (read-only, doesn't record attempt)
|
|
useEffect(() => {
|
|
const rateLimit = antibot.checkActionRateLimitStatus(formId, maxAttempts);
|
|
setRateLimitInfo(rateLimit);
|
|
}, [antibot, formId, maxAttempts]);
|
|
|
|
const validate = useCallback(async (): Promise<boolean> => {
|
|
setIsValidating(true);
|
|
|
|
try {
|
|
// Check rate limit
|
|
const rateLimit = antibot.checkActionRateLimit(formId, maxAttempts);
|
|
setRateLimitInfo(rateLimit);
|
|
|
|
if (!rateLimit.allowed) {
|
|
const resetDate = new Date(rateLimit.resetTime);
|
|
const errorMsg = `Too many attempts. Please try again after ${resetDate.toLocaleTimeString()}`;
|
|
if (onValidationError) {
|
|
onValidationError([errorMsg]);
|
|
}
|
|
setIsValidating(false);
|
|
return false;
|
|
}
|
|
|
|
// Stop form tracking to calculate time to fill
|
|
antibot.stopFormTracking();
|
|
|
|
// Validate antibot data
|
|
const validation = antibot.validateForm(honeypotValue, recaptchaToken, {
|
|
minTimeOnPage,
|
|
minTimeToFill,
|
|
requireRecaptcha,
|
|
checkMouseMovements,
|
|
});
|
|
|
|
if (!validation.isValid) {
|
|
if (onValidationError) {
|
|
onValidationError(validation.errors);
|
|
}
|
|
setIsValidating(false);
|
|
return false;
|
|
}
|
|
|
|
if (validation.warnings.length > 0 && onValidationWarning) {
|
|
onValidationWarning(validation.warnings);
|
|
}
|
|
|
|
setIsValidating(false);
|
|
return true;
|
|
} catch (error) {
|
|
if (import.meta.env.DEV) {
|
|
console.error('Antibot validation error:', error);
|
|
}
|
|
setIsValidating(false);
|
|
return false;
|
|
}
|
|
}, [
|
|
antibot,
|
|
formId,
|
|
maxAttempts,
|
|
honeypotValue,
|
|
recaptchaToken,
|
|
minTimeOnPage,
|
|
minTimeToFill,
|
|
requireRecaptcha,
|
|
checkMouseMovements,
|
|
onValidationError,
|
|
onValidationWarning,
|
|
]);
|
|
|
|
const reset = useCallback(() => {
|
|
setHoneypotValue('');
|
|
setRecaptchaToken(null);
|
|
formStartedRef.current = false;
|
|
antibot.startFormTracking();
|
|
}, [antibot]);
|
|
|
|
const refreshRateLimit = useCallback(() => {
|
|
// Use status check (read-only) to avoid recording an attempt
|
|
const rateLimit = antibot.checkActionRateLimitStatus(formId, maxAttempts);
|
|
setRateLimitInfo(rateLimit);
|
|
}, [antibot, formId, maxAttempts]);
|
|
|
|
return {
|
|
honeypotValue,
|
|
setHoneypotValue,
|
|
recaptchaToken,
|
|
setRecaptchaToken,
|
|
isValidating,
|
|
validate,
|
|
reset,
|
|
refreshRateLimit,
|
|
rateLimitInfo,
|
|
};
|
|
};
|
|
|