233 lines
6.9 KiB
JavaScript
233 lines
6.9 KiB
JavaScript
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 (
|
|
<Dialog open={showWarning} onOpenChange={(open) => {
|
|
if (!open) {
|
|
// Prevent closing dialog by clicking outside
|
|
// User must click a button
|
|
return;
|
|
}
|
|
}}>
|
|
<DialogContent
|
|
className="max-w-md"
|
|
onPointerDownOutside={(e) => e.preventDefault()}
|
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
|
>
|
|
<DialogHeader>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="bg-[#ff9e77]/10 p-3 rounded-full">
|
|
<AlertTriangle className="h-6 w-6 text-[#ff9e77]" />
|
|
</div>
|
|
<DialogTitle className="text-[#422268]">
|
|
Session About to Expire
|
|
</DialogTitle>
|
|
</div>
|
|
<DialogDescription className="text-[#664fa3]">
|
|
Your session will expire in <strong className="text-[#422268] text-lg">{timeRemaining}</strong> seconds due to inactivity.
|
|
|
|
<div className="mt-4 p-4 bg-[#f1eef9] rounded-lg border border-[#ddd8eb]">
|
|
<p className="text-sm text-[#422268]">
|
|
Click <strong>"Stay Logged In"</strong> to continue your session, or you will be automatically logged out.
|
|
</p>
|
|
</div>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<DialogFooter className="flex-col sm:flex-row gap-3 mt-4">
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleSessionExpired}
|
|
className="border-[#ddd8eb] text-[#664fa3] hover:bg-[#f1eef9]"
|
|
>
|
|
Log Out Now
|
|
</Button>
|
|
<Button
|
|
onClick={handleExtendSession}
|
|
disabled={isExtending}
|
|
className="bg-[#664fa3] hover:bg-[#422268] text-white"
|
|
>
|
|
{isExtending ? (
|
|
<>
|
|
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
|
Extending...
|
|
</>
|
|
) : (
|
|
'Stay Logged In'
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default IdleSessionWarning;
|