Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ReactNode } from 'react';
import Header from '@/components/Header';
import { SessionManager } from '@/components/SessionManager';

export default function MainLayout({ children }: { children: ReactNode }) {
return (
<>
<SessionManager>
<Header />
{children}
</>
</SessionManager>
);
}
9 changes: 9 additions & 0 deletions app/(main)/session-test/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SessionTimeoutDemo } from "@/components/SessionTimeoutDemo";

export default function SessionTestPage() {
return (
<div className="min-h-screen bg-gray-50">
<SessionTimeoutDemo />
</div>
);
}
184 changes: 184 additions & 0 deletions components/SessionManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"use client";

import { useEffect, useCallback } from "react";
import { useRouter } from "next/navigation";
import { useSessionTimeout } from "@/lib/auth/sessionTimeout";
import { SessionWarningModal, useSessionWarningModal } from "./SessionWarningModal";
import { useAuthStore } from "@/store/authStore";
import { authApi } from "@/lib/api/auth";

interface SessionManagerProps {
children: React.ReactNode;
}

export function SessionManager({ children }: SessionManagerProps) {
const router = useRouter();
const { token, refreshToken: storedRefreshToken, login, logout } = useAuthStore();
const {
isOpen,
timeRemaining,
isRefreshing,
showWarning,
hideWarning,
setRefreshing,
updateTimeRemaining,
} = useSessionWarningModal();

// Utility function to show logout notifications
const showLogoutNotification = useCallback((message: string, type: "success" | "error" | "warning" | "info" = "info") => {
if (typeof window !== "undefined") {
// Create a simple notification (you can replace this with your preferred notification system)
const notification = document.createElement("div");
notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm ${
type === "success" ? "bg-green-500 text-white" :
type === "error" ? "bg-red-500 text-white" :
type === "warning" ? "bg-orange-500 text-white" :
"bg-blue-500 text-white"
}`;
notification.innerHTML = `
<div class="flex items-center">
<span class="mr-2">${
type === "success" ? "✓" :
type === "error" ? "✕" :
type === "warning" ? "⚠" :
"ℹ"
}</span>
<span>${message}</span>
</div>
`;

document.body.appendChild(notification);

// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 5000);
}
}, []);

// Handle session timeout events
const handleSessionWarning = useCallback((remainingTime: number) => {
showWarning(remainingTime);
}, [showWarning]);

const handleSessionExpired = useCallback(() => {
hideWarning();

// Clear any stored session data
if (typeof window !== "undefined") {
localStorage.removeItem("auth-storage");
sessionStorage.clear();
}

// Show notification about logout reason
showLogoutNotification("Your session has expired due to inactivity. Please log in again.");

// Redirect to login page
router.push("/auth/login");
}, [hideWarning, router, showLogoutNotification]);

const handleRefreshSuccess = useCallback(() => {
hideWarning();
showLogoutNotification("Your session has been extended successfully.", "success");
}, [hideWarning, showLogoutNotification]);

const handleRefreshFailure = useCallback((error: any) => {
console.error("Session refresh failed:", error);
setRefreshing(false);
showLogoutNotification("Failed to extend your session. You will be logged out soon.");
}, [setRefreshing, showLogoutNotification]);

// Initialize session timeout monitoring
const { refreshToken, getSessionState } = useSessionTimeout({
onWarning: handleSessionWarning,
onExpired: handleSessionExpired,
onRefreshSuccess: handleRefreshSuccess,
onRefreshFailure: handleRefreshFailure,
warningThreshold: 5 * 60 * 1000, // 5 minutes
checkInterval: 30 * 1000, // Check every 30 seconds
});

// Handle "Stay Logged In" button click
const handleStayLoggedIn = useCallback(async () => {
setRefreshing(true);

try {
const success = await refreshToken();
if (!success) {
showLogoutNotification("Failed to extend session. Please try again.");
}
} catch (error) {
console.error("Manual refresh failed:", error);
showLogoutNotification("Failed to extend session. You will be logged out.");
} finally {
setRefreshing(false);
}
}, [refreshToken, setRefreshing, showLogoutNotification]);

// Handle manual logout
const handleLogout = useCallback(async () => {
try {
// Call logout API to invalidate server session
await authApi.logout();
} catch (error) {
console.error("Logout API call failed:", error);
} finally {
// Always perform client-side logout regardless of API success
hideWarning();
logout();

// Clear session data
if (typeof window !== "undefined") {
localStorage.removeItem("auth-storage");
sessionStorage.clear();
}

showLogoutNotification("You have been logged out successfully.");
router.push("/auth/login");
}
}, [hideWarning, logout, router, showLogoutNotification]);

// Update time remaining in modal
useEffect(() => {
if (isOpen) {
const interval = setInterval(() => {
const state = getSessionState();
updateTimeRemaining(state.timeRemaining);

// Auto-close if session is no longer in warning state
if (!state.isWarning) {
hideWarning();
}
}, 1000);

return () => clearInterval(interval);
}
}, [isOpen, getSessionState, updateTimeRemaining, hideWarning]);

return (
<>
{children}
<SessionWarningModal
isOpen={isOpen}
timeRemaining={timeRemaining}
onStayLoggedIn={handleStayLoggedIn}
onLogout={handleLogout}
isRefreshing={isRefreshing}
/>
</>
);
}

// Export a hook for accessing session manager functionality
export function useSessionManager() {
const { getSessionState } = useSessionTimeout({
warningThreshold: 5 * 60 * 1000,
checkInterval: 30 * 1000,
});

return {
getSessionState,
};
}
162 changes: 162 additions & 0 deletions components/SessionTimeoutDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { useSessionTimeout, sessionUtils } from "@/lib/auth/sessionTimeout";
import { useAuthStore } from "@/store/authStore";

export function SessionTimeoutDemo() {
const { token, refreshToken, login } = useAuthStore();
const { getSessionState, refreshToken: refreshSession } = useSessionTimeout();
const [demoToken, setDemoToken] = useState("");

const sessionState = getSessionState();

// Create a mock token that expires in 2 minutes for testing
const createTestToken = () => {
const now = Date.now();
const exp = now + (2 * 60 * 1000); // 2 minutes from now
const payload = {
sub: "test-user",
exp: Math.floor(exp / 1000),
iat: Math.floor(now / 1000),
};

const header = { alg: "HS256", typ: "JWT" };
const encodedHeader = btoa(JSON.stringify(header));
const encodedPayload = btoa(JSON.stringify(payload));
const signature = "test-signature"; // Mock signature

const testToken = `${encodedHeader}.${encodedPayload}.${signature}`;
setDemoToken(testToken);

// Simulate login with test token
login(
{ id: "test-user", email: "test@example.com", name: "Test User" },
testToken,
"test-refresh-token"
);
};

// Create a token that expires in 30 seconds for testing warning
const createExpiringToken = () => {
const now = Date.now();
const exp = now + (30 * 1000); // 30 seconds from now
const payload = {
sub: "test-user",
exp: Math.floor(exp / 1000),
iat: Math.floor(now / 1000),
};

const header = { alg: "HS256", typ: "JWT" };
const encodedHeader = btoa(JSON.stringify(header));
const encodedPayload = btoa(JSON.stringify(payload));
const signature = "test-signature";

const testToken = `${encodedHeader}.${encodedPayload}.${signature}`;

login(
{ id: "test-user", email: "test@example.com", name: "Test User" },
testToken,
"test-refresh-token"
);
};

const handleLogout = () => {
const { logout } = useAuthStore.getState();
logout();
setDemoToken("");
};

const formatTime = (ms: number) => {
return sessionUtils.formatTimeRemaining(ms);
};

return (
<div className="p-6 max-w-4xl mx-auto space-y-6">
<Card className="p-6">
<h2 className="text-2xl font-bold mb-4">Session Timeout Demo</h2>

{/* Current Session Status */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold mb-2">Current Session Status:</h3>
<div className="space-y-1 text-sm">
<p><strong>Token:</strong> {token ? "Present" : "None"}</p>
<p><strong>Refresh Token:</strong> {refreshToken ? "Present" : "None"}</p>
<p><strong>Is Warning:</strong> {sessionState.isWarning ? "Yes" : "No"}</p>
<p><strong>Time Remaining:</strong> {formatTime(sessionState.timeRemaining)}</p>
<p><strong>Is Refreshing:</strong> {sessionState.isRefreshing ? "Yes" : "No"}</p>
</div>
</div>

{/* Demo Controls */}
<div className="space-y-4">
<h3 className="font-semibold">Test Controls:</h3>

<div className="flex flex-wrap gap-3">
<Button
onClick={createTestToken}
className="bg-blue-600 hover:bg-blue-700"
>
Create 2-min Test Token
</Button>

<Button
onClick={createExpiringToken}
className="bg-orange-600 hover:bg-orange-700"
>
Create 30-sec Token (Test Warning)
</Button>

<Button
onClick={() => refreshSession()}
disabled={!token}
variant="outline"
>
Manual Refresh Token
</Button>

<Button
onClick={handleLogout}
disabled={!token}
variant="outline"
className="border-red-300 text-red-600 hover:bg-red-50"
>
Logout
</Button>
</div>
</div>

{/* Instructions */}
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<h3 className="font-semibold mb-2">How to Test:</h3>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Click &quot;Create 2-min Test Token&quot; to simulate a login session</li>
<li>Wait for the warning modal to appear (should show at 5 minutes, but our test token expires in 2 minutes)</li>
<li>Click &quot;Create 30-sec Token&quot; to test the warning appearing quickly</li>
<li>Test the &quot;Stay Logged In&quot; button to refresh the session</li>
<li>Wait for automatic logout when token expires</li>
<li>Verify notifications appear for different events</li>
</ol>
</div>

{/* Token Debug Info */}
{token && (
<div className="mt-6 p-4 bg-yellow-50 rounded-lg">
<h3 className="font-semibold mb-2">Token Debug Info:</h3>
<div className="text-xs space-y-1">
<p><strong>Full Token:</strong> {token.substring(0, 50)}...</p>
<p><strong>Expiration Time:</strong> {
sessionUtils.getTokenExpirationTime(token)
? new Date(sessionUtils.getTokenExpirationTime(token)!).toLocaleString()
: "Invalid"
}</p>
<p><strong>Is Expired:</strong> {sessionUtils.isTokenExpired(token) ? "Yes" : "No"}</p>
</div>
</div>
)}
</Card>
</div>
);
}
Loading
Loading