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
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>
|
|
);
|
|
}
|