@@ -8,6 +8,8 @@ import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | |||
import Stack from "@mui/material/Stack"; | |||
import Breadcrumb from "@/components/Breadcrumb"; | |||
import { I18nProvider } from "@/i18n"; | |||
import RefreshTokenProvider from "@/components/RefreshTokenProvider"; | |||
import SessionProvider from "@/components/SessionProvider" | |||
export default async function MainLayout({ | |||
children, | |||
@@ -33,13 +35,16 @@ export default async function MainLayout({ | |||
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | |||
}} | |||
> | |||
<I18nProvider namespaces={["common"]}> | |||
<AutoLogoutProvider/> | |||
<Stack spacing={2}> | |||
<Breadcrumb /> | |||
{children} | |||
</Stack> | |||
</I18nProvider> | |||
<SessionProvider session={session}> | |||
<I18nProvider namespaces={["common"]}> | |||
<AutoLogoutProvider/> | |||
<RefreshTokenProvider/> | |||
<Stack spacing={2}> | |||
<Breadcrumb /> | |||
{children} | |||
</Stack> | |||
</I18nProvider> | |||
</SessionProvider> | |||
</Box> | |||
</> | |||
); | |||
@@ -0,0 +1,24 @@ | |||
"use server"; | |||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
import { BASE_API_URL } from "@/config/api"; | |||
import { cache } from "react"; | |||
export interface TokenResponse { | |||
accessToken: string; | |||
refreshToken: string; | |||
} | |||
export interface TokenRequest { | |||
refreshToken: string; | |||
} | |||
export const fetchTokenDetails = cache(async (data: TokenRequest) => { | |||
return serverFetchJson<TokenResponse>(`${BASE_API_URL}/refresh-token`, | |||
{ | |||
method: "POST", | |||
body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}, | |||
); | |||
}); |
@@ -22,6 +22,7 @@ export const serverFetch: typeof fetch = async (input, init) => { | |||
const session = await getServerSession<any, SessionWithTokens>(authOptions); | |||
const accessToken = session?.accessToken; | |||
// console.log("Access Token: ", accessToken) | |||
// console.log(accessToken); | |||
return fetch(input, { | |||
...init, | |||
@@ -49,6 +50,8 @@ export async function serverFetchJson<T>(...args: FetchParams) { | |||
switch (response.status) { | |||
case 401: | |||
signOutUser(); | |||
case 417: | |||
signOutUser(); | |||
case 422: | |||
throw new ServerFetchError( | |||
JSON.parse(errorText).error, | |||
@@ -3,35 +3,36 @@ import React, { createContext, useState, useEffect, ReactNode } from 'react'; | |||
import { useIdleTimer } from "react-idle-timer"; | |||
import { signOut } from "next-auth/react"; | |||
import { useTranslation } from 'react-i18next'; | |||
import { useSession } from "next-auth/react"; | |||
interface TimerContextProps { | |||
lastRequestTime: number; | |||
setLastRequestTime: React.Dispatch<React.SetStateAction<number>>; | |||
} | |||
export const TimerContext = createContext<TimerContextProps | undefined>(undefined); | |||
interface AutoLogoutProviderProps { | |||
children?: ReactNode; | |||
isUserLoggedIn: boolean; | |||
} | |||
const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children, isUserLoggedIn }) => { | |||
const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children }) => { | |||
const { t } = useTranslation("common") | |||
const [lastRequestTime, setLastRequestTime] = useState(Date.now()); | |||
const [logoutInterval, setLogoutInterval] = useState(1); // minute | |||
const [state, setState] = useState('Active'); | |||
const { data: session } = useSession(); | |||
const onIdle = () => { | |||
setLastRequestTime(Date.now()); | |||
setState('Idle') | |||
} | |||
const onActive = () => { | |||
setLastRequestTime(Date.now()); | |||
setState('Active') | |||
} | |||
const { | |||
isLastActiveTab, | |||
} = useIdleTimer({ | |||
@@ -42,21 +43,21 @@ const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children, isUse | |||
crossTab: true, | |||
syncTimers: 200, | |||
}) | |||
const lastActiveTab = isLastActiveTab() === null ? 'loading' : isLastActiveTab() | |||
const getLogoutInterval = () => { | |||
if (isUserLoggedIn && logoutInterval === 1) { | |||
if (session && logoutInterval === 1) { | |||
//TODO: get auto logout time here | |||
setLogoutInterval(60); | |||
} | |||
else { | |||
if (!isUserLoggedIn && logoutInterval > 1) { | |||
if (!session && logoutInterval > 1) { | |||
setLogoutInterval(1); | |||
} | |||
} | |||
} | |||
useEffect(() => { | |||
getLogoutInterval() | |||
const interval = setInterval(async () => { | |||
@@ -64,6 +65,8 @@ const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children, isUse | |||
// if (isPasswordExpiry()) { | |||
// navigate('/user/changePassword'); | |||
// } | |||
// console.log(state) | |||
// console.log(lastActiveTab) | |||
if (state !== "Active" && lastActiveTab) { | |||
const timeElapsed = currentTime - lastRequestTime; | |||
if (timeElapsed >= logoutInterval * 60 * 1000) { | |||
@@ -75,18 +78,18 @@ const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children, isUse | |||
} | |||
} | |||
}, 1000); // Check every second | |||
return () => { | |||
clearInterval(interval); | |||
}; | |||
}, [lastRequestTime, logoutInterval]); | |||
return ( | |||
<TimerContext.Provider value={{ lastRequestTime, setLastRequestTime }}> | |||
{children} | |||
</TimerContext.Provider> | |||
); | |||
}; | |||
export default AutoLogoutProvider; |
@@ -1,14 +1,8 @@ | |||
import React from "react"; | |||
import AutoLogoutProvider from "./AutoLogoutProvider"; | |||
import { getServerSession } from "next-auth"; | |||
import { authOptions } from "@/config/authConfig"; | |||
const AutoLogoutProviderWrapper: React.FC = async () => { | |||
const session = await getServerSession(authOptions) | |||
const isUserLoggedIn = session?.user !== null | |||
return <AutoLogoutProvider isUserLoggedIn={isUserLoggedIn}/>; | |||
return <AutoLogoutProvider/>; | |||
}; | |||
export default AutoLogoutProviderWrapper; |
@@ -0,0 +1,90 @@ | |||
'use client' | |||
import { createContext, useEffect, useRef, useCallback, ReactNode } from 'react'; | |||
import { useSession } from "next-auth/react"; | |||
// import { useTranslation } from 'react-i18next'; | |||
import { SessionWithTokens } from '@/config/authConfig'; | |||
import { fetchTokenDetails } from '@/app/api/token/actions'; | |||
interface RefreshTokenContextProps { | |||
} | |||
export const RefreshTokenContext = createContext<RefreshTokenContextProps | undefined>(undefined); | |||
interface RefreshTokenProviderProps { | |||
children?: ReactNode; | |||
} | |||
const RefreshTokenProvider: React.FC<RefreshTokenProviderProps> = ({ children }) => { | |||
const { data, update } = useSession(); | |||
const session = useRef(data as SessionWithTokens) | |||
// const token = useRef<string | null>(session.current?.accessToken ?? null) | |||
const isRefresh = useRef<boolean>(false) | |||
const handleRefreshToken = useCallback(async () => { | |||
if (!isRefresh.current) { | |||
const refreshToken = session.current.refreshToken ?? "" | |||
isRefresh.current = true | |||
try { | |||
const response = await fetchTokenDetails({ refreshToken: refreshToken }) | |||
if (response) { | |||
const newAccessToken = response.accessToken | |||
const newRefreshToken = response.refreshToken | |||
await update({ | |||
accessToken: newAccessToken, | |||
refreshToken: newRefreshToken | |||
}) | |||
// console.log("New Access Token: ", newAccessToken) | |||
// console.log("%c [ Old Refresh Token ]:", 'font-size:13px; background:pink; color:#bf2c9f;', `${refreshToken}`) | |||
// console.log("%c [ New Refresh Token ]:", 'font-size:13px; background:pink; color:#bf2c9f;', `${newRefreshToken}`) | |||
} | |||
isRefresh.current = false | |||
} catch (refreshError) { | |||
console.log('Failed to refresh token'); | |||
console.log(refreshError); | |||
isRefresh.current = false; | |||
}; | |||
} | |||
}, [session.current, update]) | |||
const checkTokenExpiry = useCallback(() => { | |||
const accessToken = session.current.accessToken | |||
if (accessToken) { | |||
const tokenExp = JSON.parse(atob(accessToken.split('.')[1])).exp; | |||
const currentTime = Math.floor(Date.now() / 1000); | |||
const expiryTime = tokenExp - 30; // Refresh 30 seconds before expiry | |||
// console.log("%c [ Expiry Time ]:", 'font-size:13px; background:pink; color:#bf2c9f;', `${new Date(expiryTime * 1000).toLocaleString()}`) | |||
if (currentTime >= expiryTime) { | |||
if (session.current) { | |||
handleRefreshToken(); | |||
} | |||
} | |||
} | |||
}, [handleRefreshToken, session.current]) | |||
useEffect(() => { | |||
const timer = setInterval(checkTokenExpiry, 10000); | |||
return () => clearInterval(timer); | |||
}, [checkTokenExpiry]); | |||
useEffect(() => { | |||
session.current = data as SessionWithTokens | |||
}, [data]) | |||
return ( | |||
<RefreshTokenContext.Provider value={{}}> | |||
{children} | |||
</RefreshTokenContext.Provider> | |||
); | |||
}; | |||
export default RefreshTokenProvider; |
@@ -0,0 +1,9 @@ | |||
import React from "react"; | |||
import RefreshTokenProvider from "./RefreshTokenProvider"; | |||
const RefreshTokenProviderWrapper: React.FC = async () => { | |||
return <RefreshTokenProvider/>; | |||
}; | |||
export default RefreshTokenProviderWrapper; |
@@ -0,0 +1 @@ | |||
export { default } from "./RefreshTokenProviderWrapper"; |
@@ -0,0 +1,3 @@ | |||
"use client"; | |||
export { SessionProvider as default } from "next-auth/react"; |
@@ -0,0 +1 @@ | |||
export { default } from "./SessionProvider"; |
@@ -57,12 +57,27 @@ export const authOptions: AuthOptions = { | |||
callbacks: { | |||
jwt(params) { | |||
// Add the data from user to the token | |||
const { token, user } = params; | |||
const newToken = { ...token, ...user }; | |||
const { token, user, account, trigger, session } = params; | |||
// console.log("--------------------------") | |||
// console.log("%c [ token ]:", 'font-size:13px; background:#A888B5; color:#bf2c9f;', token) | |||
// console.log("%c [ user ]:", 'font-size:13px; background:pink; color:#bf2c9f;', user) | |||
// console.log("%c [ account ]:", 'font-size:13px; background:pink; color:#bf2c9f;', account) | |||
// console.log("%c [ session ]:", 'font-size:13px; background:#FFD2A0; color:#bf2c9f;', session) | |||
// console.log("%c [ trigger ]:", 'font-size:13px; background:#EFB6C8; color:#bf2c9f;', trigger) | |||
// console.log(params) | |||
// console.log("--------------------------") | |||
if (trigger === "update" && session?.accessToken && session?.refreshToken) { | |||
token.accessToken = session.accessToken | |||
token.refreshToken = session.refreshToken | |||
} | |||
let newToken = { ...token, ...user }; | |||
return newToken; | |||
}, | |||
session({ session, token }) { | |||
// console.log(token.accessToken as string | undefined) | |||
const sessionWithToken: SessionWithTokens = { | |||
...session, | |||
role: token.role as string, | |||
@@ -77,5 +92,5 @@ export const authOptions: AuthOptions = { | |||
// console.log(sessionWithToken) | |||
return sessionWithToken; | |||
}, | |||
}, | |||
} | |||
}; |