import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import logger from '../utils/logger'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from './ui/dialog'; import { Button } from './ui/button'; import { AlertTriangle, RefreshCw } from 'lucide-react'; /** * IdleSessionWarning Component * * Monitors user activity and warns before session expiration * - Warns 1 minute before JWT expiry (at 29 minutes if JWT is 30 min) * - Auto-logout on expiration * - "Stay Logged In" extends session */ const IdleSessionWarning = () => { const { user, logout, refreshUser } = useAuth(); const navigate = useNavigate(); // Configuration const SESSION_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds const WARNING_BEFORE_EXPIRY = 1 * 60 * 1000; // Warn 1 minute before expiry const WARNING_TIME = SESSION_DURATION - WARNING_BEFORE_EXPIRY; // 29 minutes const [showWarning, setShowWarning] = useState(false); const [timeRemaining, setTimeRemaining] = useState(60); // seconds const [isExtending, setIsExtending] = useState(false); const activityTimeoutRef = useRef(null); const warningTimeoutRef = useRef(null); const countdownIntervalRef = useRef(null); const lastActivityRef = useRef(Date.now()); // Reset activity timer const resetActivityTimer = useCallback(() => { lastActivityRef.current = Date.now(); // Clear existing timers if (activityTimeoutRef.current) { clearTimeout(activityTimeoutRef.current); } if (warningTimeoutRef.current) { clearTimeout(warningTimeoutRef.current); } if (countdownIntervalRef.current) { clearInterval(countdownIntervalRef.current); } // Hide warning if showing if (showWarning) { setShowWarning(false); } // Set new warning timer warningTimeoutRef.current = setTimeout(() => { // Show warning setShowWarning(true); setTimeRemaining(60); // 60 seconds until logout // Start countdown countdownIntervalRef.current = setInterval(() => { setTimeRemaining((prev) => { if (prev <= 1) { // Time's up - logout handleSessionExpired(); return 0; } return prev - 1; }); }, 1000); // Set auto-logout timer activityTimeoutRef.current = setTimeout(() => { handleSessionExpired(); }, WARNING_BEFORE_EXPIRY); }, WARNING_TIME); }, [showWarning]); // Handle session expiration const handleSessionExpired = useCallback(() => { // Clear all timers if (activityTimeoutRef.current) clearTimeout(activityTimeoutRef.current); if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current); if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); setShowWarning(false); logout(); navigate('/login', { state: { message: 'Your session has expired due to inactivity. Please log in again.' } }); }, [logout, navigate]); // Handle "Stay Logged In" button const handleExtendSession = async () => { setIsExtending(true); try { // Refresh user data to get new token await refreshUser(); // Reset activity timer resetActivityTimer(); logger.log('[IdleSessionWarning] Session extended successfully'); } catch (error) { logger.error('[IdleSessionWarning] Failed to extend session:', error); // If refresh fails, logout handleSessionExpired(); } finally { setIsExtending(false); } }; // Track user activity useEffect(() => { if (!user) return; const activityEvents = [ 'mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click' ]; // Throttle activity detection to avoid too many resets let throttleTimeout = null; const handleActivity = () => { if (throttleTimeout) return; throttleTimeout = setTimeout(() => { resetActivityTimer(); throttleTimeout = null; }, 1000); // Throttle to once per second }; // Add event listeners activityEvents.forEach(event => { document.addEventListener(event, handleActivity, { passive: true }); }); // Initialize timer resetActivityTimer(); // Cleanup return () => { activityEvents.forEach(event => { document.removeEventListener(event, handleActivity); }); if (activityTimeoutRef.current) clearTimeout(activityTimeoutRef.current); if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current); if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); if (throttleTimeout) clearTimeout(throttleTimeout); }; }, [user, resetActivityTimer]); // Don't render if user is not logged in if (!user) return null; return ( { if (!open) { // Prevent closing dialog by clicking outside // User must click a button return; } }}> e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()} >
Session About to Expire
Your session will expire in {timeRemaining} seconds due to inactivity.

Click "Stay Logged In" to continue your session, or you will be automatically logged out.

); }; export default IdleSessionWarning;