Files
Hotel-Booking/Frontend/src/features/auth/hooks/useAntibotForm.ts
Iliyan Angelov 86e78247c3 updates
2025-12-01 23:30:28 +02:00

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,
};
};