You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
4.7 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { useSwissOIDAuth } from "swissoid-front";
import useTranslation from "@/src/hooks/useTranslation";
/**
* SessionMonitor component
*
* Monitors session expiry and shows a warning modal when session is about to expire.
* - Shows modal 5 minutes before expiry
* - User can click "Stay logged in" to refresh the session
* - If session expires (authenticated: false), redirects to homepage
*
* SwissOID polls /auth/status every 30 seconds by default.
*/
export default function SessionMonitor() {
const router = useRouter();
const { locale, t } = useTranslation();
const { authenticated, loading, user, refreshSession } = useSwissOIDAuth();
const [showModal, setShowModal] = useState(false);
const [timeRemaining, setTimeRemaining] = useState<number | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const wasAuthenticatedRef = useRef(false);
const hasRedirectedRef = useRef(false);
// Monitor session expiry and show warning modal
useEffect(() => {
if (!authenticated || !user || loading) {
setShowModal(false);
return;
}
const checkExpiry = () => {
const exp = (user as any)?.exp;
if (!exp) return;
const expiryTime = exp * 1000; // Convert to milliseconds
const now = Date.now();
const remaining = expiryTime - now;
setTimeRemaining(remaining);
// Show modal if less than 5 minutes remaining
const fiveMinutes = 5 * 60 * 1000;
if (remaining > 0 && remaining < fiveMinutes && !showModal) {
setShowModal(true);
}
// Hide modal if session was refreshed (more than 5 minutes now)
if (remaining >= fiveMinutes && showModal) {
setShowModal(false);
}
};
// Check immediately
checkExpiry();
// Check every 10 seconds
const interval = setInterval(checkExpiry, 10000);
return () => clearInterval(interval);
}, [authenticated, user, loading, showModal]);
// Handle logout when authenticated becomes false
useEffect(() => {
// Track if user was authenticated
if (authenticated && !loading) {
wasAuthenticatedRef.current = true;
}
// If user was authenticated and now is not (session expired), redirect to homepage
if (wasAuthenticatedRef.current && !authenticated && !loading && !hasRedirectedRef.current) {
console.log('[SessionMonitor] Session expired, redirecting to homepage');
hasRedirectedRef.current = true;
setShowModal(false);
// Redirect to homepage
router.push(`/${locale}`);
}
}, [authenticated, loading, router, locale]);
const handleRefresh = async () => {
setIsRefreshing(true);
try {
await refreshSession();
setShowModal(false);
} catch (error) {
console.error('[SessionMonitor] Failed to refresh session:', error);
} finally {
setIsRefreshing(false);
}
};
if (!showModal || timeRemaining === null) {
return null;
}
const minutes = Math.ceil(timeRemaining / 60000);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" />
<div className="relative bg-white rounded-xl shadow-xl p-6 w-80 text-center animate-fadeIn">
<h3 className="text-lg font-semibold text-slate-800 mb-2">
{t('Session Expiring')}
</h3>
<p className="text-slate-600 mb-4">
{minutes > 1
? t('Your session will expire in {minutes} minutes.', { minutes })
: t('Your session will expire in less than a minute.')}
</p>
<div className="flex justify-center gap-4">
<button
onClick={handleRefresh}
disabled={isRefreshing}
className={`px-4 py-2 rounded-lg text-white transition-all flex items-center gap-2 ${
isRefreshing
? 'bg-indigo-400 cursor-not-allowed'
: 'bg-indigo-600 hover:bg-indigo-700'
}`}
>
{isRefreshing && (
<svg className="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{isRefreshing ? t('Refreshing...') : t('Stay logged in')}
</button>
</div>
</div>
</div>
);
}