Files
Hotel-Booking/Frontend/src/features/auth/contexts/AntibotContext.tsx
Iliyan Angelov 39fcfff811 update
2025-11-30 22:43:09 +02:00

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