Login and Session Fixes
This commit is contained in:
231
src/components/IdleSessionWarning.js
Normal file
231
src/components/IdleSessionWarning.js
Normal file
@@ -0,0 +1,231 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
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();
|
||||
|
||||
console.log('[IdleSessionWarning] Session extended successfully');
|
||||
} catch (error) {
|
||||
console.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;
|
||||
Reference in New Issue
Block a user