Skip to content

Commit 21aeab4

Browse files
committed
implemented session timeout, activity reset and nextauth jwt token sync
1 parent 66dc1db commit 21aeab4

8 files changed

Lines changed: 220 additions & 22 deletions

File tree

app/api/auth/[...nextauth]/route.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,19 @@ export const authOptions: NextAuthOptions = {
9494
if (user) {
9595
token.role = user.role;
9696
token.id = user.id;
97+
token.iat = Math.floor(Date.now() / 1000); // Issued at time
9798
}
99+
100+
// Check if token is expired (15 minutes)
101+
const now = Math.floor(Date.now() / 1000);
102+
const tokenAge = now - (token.iat as number);
103+
const maxAge = 15 * 60; // 15 minutes
104+
105+
if (tokenAge > maxAge) {
106+
// Token expired, return empty object to force re-authentication
107+
return {};
108+
}
109+
98110
return token;
99111
},
100112
async session({ session, token }) {
@@ -111,10 +123,11 @@ export const authOptions: NextAuthOptions = {
111123
},
112124
session: {
113125
strategy: "jwt",
114-
maxAge: 30 * 24 * 60 * 60, // 30 days
126+
maxAge: 15 * 60, // 15 minutes in seconds
127+
updateAge: 5 * 60, // Update session every 5 minutes
115128
},
116129
jwt: {
117-
maxAge: 30 * 24 * 60 * 60, // 30 days
130+
maxAge: 15 * 60, // 15 minutes in seconds
118131
},
119132
secret: process.env.NEXTAUTH_SECRET,
120133
debug: process.env.NODE_ENV === "development",

app/layout.tsx

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import type { Metadata } from "next";
22
import { Inter } from "next/font/google";
33
import "./globals.css";
4-
import { Footer, Header } from "@/components";
5-
import SessionProvider from "@/utils/SessionProvider";
6-
import Providers from "@/Providers";
74
import { getServerSession } from "next-auth";
85
import 'svgmap/dist/svgMap.min.css';
9-
10-
11-
6+
import SessionProvider from "@/utils/SessionProvider";
7+
import Header from "@/components/Header";
8+
import Footer from "@/components/Footer";
9+
import Providers from "@/Providers";
10+
import SessionTimeoutWrapper from "@/components/SessionTimeoutWrapper";
1211

1312
const inter = Inter({ subsets: ["latin"] });
1413

@@ -22,19 +21,19 @@ export default async function RootLayout({
2221
}: Readonly<{
2322
children: React.ReactNode;
2423
}>) {
25-
2624
const session = await getServerSession();
2725
return (
2826
<html lang="en" data-theme="light">
2927
<body className={inter.className}>
30-
<SessionProvider session={session}>
31-
<Header />
32-
<Providers>
33-
{children}
34-
</Providers>
35-
<Footer />
36-
</SessionProvider>
37-
</body>
28+
<SessionProvider session={session}>
29+
<SessionTimeoutWrapper />
30+
<Header />
31+
<Providers>
32+
{children}
33+
</Providers>
34+
<Footer />
35+
</SessionProvider>
36+
</body>
3837
</html>
3938
);
4039
}

app/login/page.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,30 @@
22
import { CustomButton, SectionTitle } from "@/components";
33
import { isValidEmailAddressFormat } from "@/lib/utils";
44
import { signIn, useSession } from "next-auth/react";
5-
import { useRouter } from "next/navigation";
5+
import { useRouter, useSearchParams } from "next/navigation";
66
import React, { useEffect, useState } from "react";
77
import toast from "react-hot-toast";
88
import { FcGoogle } from "react-icons/fc";
99

1010
const LoginPage = () => {
1111
const router = useRouter();
12+
const searchParams = useSearchParams();
1213
const [error, setError] = useState("");
13-
// const session = useSession();
1414
const { data: session, status: sessionStatus } = useSession();
1515

1616
useEffect(() => {
17+
// Check if session expired
18+
const expired = searchParams.get('expired');
19+
if (expired === 'true') {
20+
setError("Your session has expired. Please log in again.");
21+
toast.error("Your session has expired. Please log in again.");
22+
}
23+
1724
// if user has already logged in redirect to home page
1825
if (sessionStatus === "authenticated") {
1926
router.replace("/");
2027
}
21-
}, [sessionStatus, router]);
28+
}, [sessionStatus, router, searchParams]);
2229

2330
const handleSubmit = async (e: any) => {
2431
e.preventDefault();

components/SessionTimeoutTest.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client";
2+
3+
import { useSession } from "next-auth/react";
4+
import { useState, useEffect } from "react";
5+
6+
export default function SessionTimeoutTest() {
7+
const { data: session, status } = useSession();
8+
const [timeLeft, setTimeLeft] = useState(0);
9+
const [isActive, setIsActive] = useState(false);
10+
11+
useEffect(() => {
12+
if (status === "authenticated" && session) {
13+
setIsActive(true);
14+
setTimeLeft(30); // 30 seconds to match the hook
15+
16+
const timer = setInterval(() => {
17+
setTimeLeft((prev) => {
18+
if (prev <= 1) {
19+
clearInterval(timer);
20+
return 0;
21+
}
22+
return prev - 1;
23+
});
24+
}, 1000);
25+
26+
return () => clearInterval(timer);
27+
} else {
28+
setIsActive(false);
29+
setTimeLeft(0);
30+
}
31+
}, [session, status]);
32+
33+
if (status === "loading") return <div>Loading...</div>;
34+
if (status === "unauthenticated") return <div>Not logged in</div>;
35+
36+
const formatTime = (seconds: number) => {
37+
const mins = Math.floor(seconds / 60);
38+
const secs = seconds % 60;
39+
return `${mins}:${secs.toString().padStart(2, '0')}`;
40+
};
41+
42+
return (
43+
<div className="fixed top-4 right-4 bg-red-100 border border-red-400 text-red-700 px-4 py-2 rounded z-50">
44+
<div className="text-sm font-bold">Session Timeout Test (30s)</div>
45+
<div className="text-xs">
46+
{isActive ? `Time left: ${formatTime(timeLeft)}` : 'Session inactive'}
47+
</div>
48+
<div className="text-xs">
49+
Status: {status}
50+
</div>
51+
</div>
52+
);
53+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"use client";
2+
3+
import { useSessionTimeout } from "@/hooks/useSessionTimeout";
4+
5+
export default function SessionTimeoutWrapper() {
6+
useSessionTimeout();
7+
return null;
8+
}

hooks/useSessionTimeout.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"use client";
2+
3+
import { useSession, signOut } from "next-auth/react";
4+
import { useEffect, useRef } from "react";
5+
6+
// Production timeout: 15 minutes
7+
const SESSION_TIMEOUT = 15 * 60 * 1000; // 15 minutes in milliseconds
8+
9+
export function useSessionTimeout() {
10+
const { data: session, status } = useSession();
11+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
12+
13+
useEffect(() => {
14+
// Only run on client side
15+
if (typeof window === 'undefined') return;
16+
17+
if (status === "authenticated" && session) {
18+
const startTimeout = () => {
19+
// Clear existing timeout
20+
if (timeoutRef.current) {
21+
clearTimeout(timeoutRef.current);
22+
}
23+
24+
// Set new timeout
25+
timeoutRef.current = setTimeout(() => {
26+
signOut({
27+
callbackUrl: "/login?expired=true",
28+
redirect: true
29+
});
30+
}, SESSION_TIMEOUT);
31+
};
32+
33+
// Start the initial timeout
34+
startTimeout();
35+
36+
// Reset timeout on user activity
37+
const resetTimeout = () => {
38+
startTimeout();
39+
};
40+
41+
// Listen for user activity
42+
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
43+
events.forEach(event => {
44+
document.addEventListener(event, resetTimeout, true);
45+
});
46+
47+
return () => {
48+
if (timeoutRef.current) {
49+
clearTimeout(timeoutRef.current);
50+
}
51+
events.forEach(event => {
52+
document.removeEventListener(event, resetTimeout, true);
53+
});
54+
};
55+
}
56+
}, [session, status]);
57+
}

hooks/useSessionTimeoutTest.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use client";
2+
3+
import { useSession, signOut } from "next-auth/react";
4+
import { useEffect, useRef } from "react";
5+
6+
// Use 30 seconds for testing
7+
const SESSION_TIMEOUT = 30 * 1000; // 30 seconds for testing
8+
9+
export function useSessionTimeoutTest() {
10+
const { data: session, status } = useSession();
11+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
12+
13+
useEffect(() => {
14+
// Only run on client side
15+
if (typeof window === 'undefined') return;
16+
17+
if (status === "authenticated" && session) {
18+
console.log('🕐 Session timeout test started - 30 seconds');
19+
20+
const startTimeout = () => {
21+
// Clear existing timeout
22+
if (timeoutRef.current) {
23+
clearTimeout(timeoutRef.current);
24+
}
25+
26+
// Set new timeout
27+
timeoutRef.current = setTimeout(() => {
28+
console.log('🚪 Session expired - signing out');
29+
signOut({
30+
callbackUrl: "/login?expired=true",
31+
redirect: true
32+
});
33+
}, SESSION_TIMEOUT);
34+
};
35+
36+
// Start the initial timeout
37+
startTimeout();
38+
39+
// Reset timeout on user activity
40+
const resetTimeout = () => {
41+
console.log('🔄 User activity detected - resetting timeout');
42+
startTimeout();
43+
};
44+
45+
// Listen for user activity
46+
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
47+
events.forEach(event => {
48+
document.addEventListener(event, resetTimeout, true);
49+
});
50+
51+
return () => {
52+
if (timeoutRef.current) {
53+
clearTimeout(timeoutRef.current);
54+
}
55+
events.forEach(event => {
56+
document.removeEventListener(event, resetTimeout, true);
57+
});
58+
};
59+
}
60+
}, [session, status]);
61+
}

middleware.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { NextResponse } from "next/server";
33

44
export default withAuth(
55
function middleware(req) {
6-
// Additional server-side check for admin routes
6+
// Check for admin routes
77
if (req.nextUrl.pathname.startsWith("/admin")) {
88
if (req.nextauth.token?.role !== "admin") {
99
return NextResponse.redirect(new URL("/", req.url));
@@ -13,7 +13,7 @@ export default withAuth(
1313
{
1414
callbacks: {
1515
authorized: ({ token, req }) => {
16-
// Allow access to admin routes only if user is authenticated and has admin role
16+
// Admin routes require admin token
1717
if (req.nextUrl.pathname.startsWith("/admin")) {
1818
return !!token && token.role === "admin";
1919
}

0 commit comments

Comments
 (0)