248 lines
6.4 KiB
TypeScript
248 lines
6.4 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
|
|
import {
|
|
FormTiming,
|
|
MouseMovement,
|
|
validateAntibotData,
|
|
checkRateLimit,
|
|
checkRateLimitStatus,
|
|
clearRateLimit,
|
|
getFingerprintHash,
|
|
type AntibotValidationResult,
|
|
} from '../../../shared/utils/antibot';
|
|
|
|
interface AntibotContextType {
|
|
timing: FormTiming;
|
|
fingerprintHash: string;
|
|
startFormTracking: () => void;
|
|
stopFormTracking: () => void;
|
|
validateForm: (
|
|
honeypotValue: string,
|
|
recaptchaToken: string | null,
|
|
options?: {
|
|
minTimeOnPage?: number;
|
|
minTimeToFill?: number;
|
|
requireRecaptcha?: boolean;
|
|
checkMouseMovements?: boolean;
|
|
}
|
|
) => AntibotValidationResult;
|
|
checkActionRateLimit: (action: string, maxAttempts?: number) => {
|
|
allowed: boolean;
|
|
remainingAttempts: number;
|
|
resetTime: number;
|
|
};
|
|
checkActionRateLimitStatus: (action: string, maxAttempts?: number) => {
|
|
allowed: boolean;
|
|
remainingAttempts: number;
|
|
resetTime: number;
|
|
};
|
|
clearActionRateLimit: (action: string) => void;
|
|
recordMouseMovement: (x: number, y: number) => void;
|
|
recordClick: () => void;
|
|
recordKeyPress: () => void;
|
|
reset: () => void;
|
|
}
|
|
|
|
const AntibotContext = createContext<AntibotContextType | undefined>(undefined);
|
|
|
|
export const useAntibot = () => {
|
|
const context = useContext(AntibotContext);
|
|
if (!context) {
|
|
throw new Error('useAntibot must be used within an AntibotProvider');
|
|
}
|
|
return context;
|
|
};
|
|
|
|
interface AntibotProviderProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const AntibotProvider: React.FC<AntibotProviderProps> = ({ children }) => {
|
|
const [timing, setTiming] = useState<FormTiming>({
|
|
pageLoadTime: Date.now(),
|
|
formStartTime: null,
|
|
formSubmitTime: null,
|
|
timeOnPage: 0,
|
|
timeToFill: null,
|
|
mouseMovements: [],
|
|
clickCount: 0,
|
|
keyPressCount: 0,
|
|
});
|
|
|
|
const [fingerprintHash] = useState<string>(() => getFingerprintHash());
|
|
const pageLoadTimeRef = useRef<number>(Date.now());
|
|
const formStartTimeRef = useRef<number | null>(null);
|
|
const mouseMovementsRef = useRef<MouseMovement[]>([]);
|
|
const clickCountRef = useRef<number>(0);
|
|
const keyPressCountRef = useRef<number>(0);
|
|
const updateIntervalRef = useRef<number | null>(null);
|
|
|
|
// Update time on page periodically
|
|
useEffect(() => {
|
|
updateIntervalRef.current = window.setInterval(() => {
|
|
setTiming((prev) => ({
|
|
...prev,
|
|
timeOnPage: Date.now() - pageLoadTimeRef.current,
|
|
}));
|
|
}, 1000);
|
|
|
|
return () => {
|
|
if (updateIntervalRef.current !== null) {
|
|
clearInterval(updateIntervalRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// Track mouse movements
|
|
useEffect(() => {
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
recordMouseMovement(e.clientX, e.clientY);
|
|
};
|
|
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
return () => window.removeEventListener('mousemove', handleMouseMove);
|
|
}, []);
|
|
|
|
// Track clicks
|
|
useEffect(() => {
|
|
const handleClick = () => {
|
|
recordClick();
|
|
};
|
|
|
|
window.addEventListener('click', handleClick);
|
|
return () => window.removeEventListener('click', handleClick);
|
|
}, []);
|
|
|
|
// Track key presses
|
|
useEffect(() => {
|
|
const handleKeyPress = () => {
|
|
recordKeyPress();
|
|
};
|
|
|
|
window.addEventListener('keypress', handleKeyPress);
|
|
return () => window.removeEventListener('keypress', handleKeyPress);
|
|
}, []);
|
|
|
|
const recordMouseMovement = useCallback((x: number, y: number) => {
|
|
const movement: MouseMovement = {
|
|
x,
|
|
y,
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
mouseMovementsRef.current.push(movement);
|
|
|
|
// Keep only last 50 movements to avoid memory issues
|
|
if (mouseMovementsRef.current.length > 50) {
|
|
mouseMovementsRef.current = mouseMovementsRef.current.slice(-50);
|
|
}
|
|
|
|
setTiming((prev) => ({
|
|
...prev,
|
|
mouseMovements: [...mouseMovementsRef.current],
|
|
}));
|
|
}, []);
|
|
|
|
const recordClick = useCallback(() => {
|
|
clickCountRef.current += 1;
|
|
setTiming((prev) => ({
|
|
...prev,
|
|
clickCount: clickCountRef.current,
|
|
}));
|
|
}, []);
|
|
|
|
const recordKeyPress = useCallback(() => {
|
|
keyPressCountRef.current += 1;
|
|
setTiming((prev) => ({
|
|
...prev,
|
|
keyPressCount: keyPressCountRef.current,
|
|
}));
|
|
}, []);
|
|
|
|
const startFormTracking = useCallback(() => {
|
|
formStartTimeRef.current = Date.now();
|
|
setTiming((prev) => ({
|
|
...prev,
|
|
formStartTime: formStartTimeRef.current,
|
|
}));
|
|
}, []);
|
|
|
|
const stopFormTracking = useCallback(() => {
|
|
if (formStartTimeRef.current !== null) {
|
|
const formSubmitTime = Date.now();
|
|
const timeToFill = formSubmitTime - formStartTimeRef.current;
|
|
|
|
setTiming((prev) => ({
|
|
...prev,
|
|
formSubmitTime,
|
|
timeToFill,
|
|
}));
|
|
}
|
|
}, []);
|
|
|
|
const validateForm = useCallback((
|
|
honeypotValue: string,
|
|
recaptchaToken: string | null,
|
|
options?: {
|
|
minTimeOnPage?: number;
|
|
minTimeToFill?: number;
|
|
requireRecaptcha?: boolean;
|
|
checkMouseMovements?: boolean;
|
|
}
|
|
): AntibotValidationResult => {
|
|
return validateAntibotData(timing, honeypotValue, recaptchaToken, options);
|
|
}, [timing]);
|
|
|
|
const checkActionRateLimit = useCallback((action: string, maxAttempts: number = 5) => {
|
|
return checkRateLimit(action, maxAttempts);
|
|
}, []);
|
|
|
|
const checkActionRateLimitStatus = useCallback((action: string, maxAttempts: number = 5) => {
|
|
return checkRateLimitStatus(action, maxAttempts);
|
|
}, []);
|
|
|
|
const clearActionRateLimit = useCallback((action: string) => {
|
|
clearRateLimit(action);
|
|
}, []);
|
|
|
|
const reset = useCallback(() => {
|
|
pageLoadTimeRef.current = Date.now();
|
|
formStartTimeRef.current = null;
|
|
mouseMovementsRef.current = [];
|
|
clickCountRef.current = 0;
|
|
keyPressCountRef.current = 0;
|
|
|
|
setTiming({
|
|
pageLoadTime: pageLoadTimeRef.current,
|
|
formStartTime: null,
|
|
formSubmitTime: null,
|
|
timeOnPage: 0,
|
|
timeToFill: null,
|
|
mouseMovements: [],
|
|
clickCount: 0,
|
|
keyPressCount: 0,
|
|
});
|
|
}, []);
|
|
|
|
const value: AntibotContextType = {
|
|
timing,
|
|
fingerprintHash,
|
|
startFormTracking,
|
|
stopFormTracking,
|
|
validateForm,
|
|
checkActionRateLimit,
|
|
checkActionRateLimitStatus,
|
|
clearActionRateLimit,
|
|
recordMouseMovement,
|
|
recordClick,
|
|
recordKeyPress,
|
|
reset,
|
|
};
|
|
|
|
return (
|
|
<AntibotContext.Provider value={value}>
|
|
{children}
|
|
</AntibotContext.Provider>
|
|
);
|
|
};
|
|
|