2025-08-22 17:02:49 +09:00
|
|
|
|
"use client";
|
2025-08-20 18:02:50 +09:00
|
|
|
|
import { logger } from "@/lib/logger";
|
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
|
import { useAuthStore } from "@/lib/auth/store";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
|
|
interface SessionTimeoutWarningProps {
|
|
|
|
|
|
warningTime?: number; // Minutes before token expires to show warning
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
|
export function SessionTimeoutWarning({
|
|
|
|
|
|
warningTime = 10, // Show warning 10 minutes before expiry
|
2025-08-20 18:02:50 +09:00
|
|
|
|
}: SessionTimeoutWarningProps) {
|
|
|
|
|
|
const { isAuthenticated, token, logout, checkAuth } = useAuthStore();
|
|
|
|
|
|
const [showWarning, setShowWarning] = useState(false);
|
|
|
|
|
|
const [timeLeft, setTimeLeft] = useState<number>(0);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!isAuthenticated || !token) {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parse JWT to get expiry time
|
|
|
|
|
|
try {
|
2025-08-22 17:02:49 +09:00
|
|
|
|
const parts = token.split(".");
|
2025-08-20 18:02:50 +09:00
|
|
|
|
if (parts.length !== 3) {
|
2025-08-22 17:02:49 +09:00
|
|
|
|
throw new Error("Invalid token format");
|
2025-08-20 18:02:50 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
|
const payload = JSON.parse(atob(parts[1])) as { exp?: number };
|
2025-08-20 18:02:50 +09:00
|
|
|
|
if (!payload.exp) {
|
2025-08-22 17:02:49 +09:00
|
|
|
|
logger.warn("Token does not have expiration time");
|
2025-08-20 18:02:50 +09:00
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const expiryTime = payload.exp * 1000; // Convert to milliseconds
|
|
|
|
|
|
const currentTime = Date.now();
|
|
|
|
|
|
const warningThreshold = warningTime * 60 * 1000; // Convert to milliseconds
|
|
|
|
|
|
|
|
|
|
|
|
const timeUntilExpiry = expiryTime - currentTime;
|
|
|
|
|
|
const timeUntilWarning = timeUntilExpiry - warningThreshold;
|
|
|
|
|
|
|
|
|
|
|
|
if (timeUntilExpiry <= 0) {
|
|
|
|
|
|
// Token already expired
|
2025-08-22 17:02:49 +09:00
|
|
|
|
void logout();
|
2025-08-20 18:02:50 +09:00
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (timeUntilWarning <= 0) {
|
|
|
|
|
|
// Should show warning immediately
|
|
|
|
|
|
setShowWarning(true);
|
|
|
|
|
|
setTimeLeft(Math.ceil(timeUntilExpiry / 1000 / 60)); // Minutes left
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Set timeout to show warning
|
|
|
|
|
|
const warningTimeout = setTimeout(() => {
|
|
|
|
|
|
setShowWarning(true);
|
|
|
|
|
|
setTimeLeft(warningTime);
|
|
|
|
|
|
}, timeUntilWarning);
|
|
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(warningTimeout);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-08-30 18:22:31 +09:00
|
|
|
|
logger.error(error, "Error parsing JWT token");
|
2025-08-22 17:02:49 +09:00
|
|
|
|
void logout();
|
2025-08-20 18:02:50 +09:00
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isAuthenticated, token, warningTime, logout]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!showWarning) return undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const interval = setInterval(() => {
|
2025-08-22 17:02:49 +09:00
|
|
|
|
setTimeLeft(prev => {
|
2025-08-20 18:02:50 +09:00
|
|
|
|
if (prev <= 1) {
|
2025-08-22 17:02:49 +09:00
|
|
|
|
void logout();
|
2025-08-20 18:02:50 +09:00
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
return prev - 1;
|
|
|
|
|
|
});
|
2025-08-22 17:02:49 +09:00
|
|
|
|
}, 60000);
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
|
}, [showWarning, logout]);
|
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
|
const handleExtendSession = () => {
|
|
|
|
|
|
void (async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await checkAuth();
|
|
|
|
|
|
setShowWarning(false);
|
|
|
|
|
|
setTimeLeft(0);
|
|
|
|
|
|
} catch (error) {
|
2025-08-30 18:22:31 +09:00
|
|
|
|
logger.error(error, "Failed to extend session");
|
2025-08-22 17:02:49 +09:00
|
|
|
|
await logout();
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
2025-08-20 18:02:50 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleLogoutNow = () => {
|
2025-08-22 17:02:49 +09:00
|
|
|
|
void logout();
|
2025-08-20 18:02:50 +09:00
|
|
|
|
setShowWarning(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!showWarning) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
|
|
|
|
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-xl">
|
|
|
|
|
|
<div className="flex items-center gap-2 mb-4">
|
|
|
|
|
|
<span className="text-yellow-500 text-xl">⚠️</span>
|
|
|
|
|
|
<h2 className="text-lg font-semibold">Session Expiring Soon</h2>
|
|
|
|
|
|
</div>
|
2025-08-22 17:02:49 +09:00
|
|
|
|
|
2025-08-20 18:02:50 +09:00
|
|
|
|
<p className="text-gray-600 mb-6">
|
2025-08-22 17:02:49 +09:00
|
|
|
|
Your session will expire in{" "}
|
|
|
|
|
|
<strong>
|
|
|
|
|
|
{timeLeft} minute{timeLeft !== 1 ? "s" : ""}
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
. Would you like to extend your session?
|
2025-08-20 18:02:50 +09:00
|
|
|
|
</p>
|
2025-08-22 17:02:49 +09:00
|
|
|
|
|
2025-08-20 18:02:50 +09:00
|
|
|
|
<div className="flex gap-2 justify-end">
|
|
|
|
|
|
<Button variant="outline" onClick={handleLogoutNow}>
|
|
|
|
|
|
Logout Now
|
|
|
|
|
|
</Button>
|
2025-08-22 17:02:49 +09:00
|
|
|
|
<Button onClick={handleExtendSession}>Extend Session</Button>
|
2025-08-20 18:02:50 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|