@@ -8,6 +8,8 @@ import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | |||||
import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
import Breadcrumb from "@/components/Breadcrumb"; | import Breadcrumb from "@/components/Breadcrumb"; | ||||
import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
import RefreshTokenProvider from "@/components/RefreshTokenProvider"; | |||||
import SessionProvider from "@/components/SessionProvider" | |||||
export default async function MainLayout({ | export default async function MainLayout({ | ||||
children, | children, | ||||
@@ -33,13 +35,16 @@ export default async function MainLayout({ | |||||
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | 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> | </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 session = await getServerSession<any, SessionWithTokens>(authOptions); | ||||
const accessToken = session?.accessToken; | const accessToken = session?.accessToken; | ||||
// console.log("Access Token: ", accessToken) | |||||
// console.log(accessToken); | // console.log(accessToken); | ||||
return fetch(input, { | return fetch(input, { | ||||
...init, | ...init, | ||||
@@ -49,6 +50,8 @@ export async function serverFetchJson<T>(...args: FetchParams) { | |||||
switch (response.status) { | switch (response.status) { | ||||
case 401: | case 401: | ||||
signOutUser(); | signOutUser(); | ||||
case 417: | |||||
signOutUser(); | |||||
case 422: | case 422: | ||||
throw new ServerFetchError( | throw new ServerFetchError( | ||||
JSON.parse(errorText).error, | JSON.parse(errorText).error, | ||||
@@ -3,35 +3,36 @@ import React, { createContext, useState, useEffect, ReactNode } from 'react'; | |||||
import { useIdleTimer } from "react-idle-timer"; | import { useIdleTimer } from "react-idle-timer"; | ||||
import { signOut } from "next-auth/react"; | import { signOut } from "next-auth/react"; | ||||
import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
import { useSession } from "next-auth/react"; | |||||
interface TimerContextProps { | interface TimerContextProps { | ||||
lastRequestTime: number; | lastRequestTime: number; | ||||
setLastRequestTime: React.Dispatch<React.SetStateAction<number>>; | setLastRequestTime: React.Dispatch<React.SetStateAction<number>>; | ||||
} | } | ||||
export const TimerContext = createContext<TimerContextProps | undefined>(undefined); | export const TimerContext = createContext<TimerContextProps | undefined>(undefined); | ||||
interface AutoLogoutProviderProps { | interface AutoLogoutProviderProps { | ||||
children?: ReactNode; | children?: ReactNode; | ||||
isUserLoggedIn: boolean; | |||||
} | } | ||||
const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children, isUserLoggedIn }) => { | |||||
const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children }) => { | |||||
const { t } = useTranslation("common") | const { t } = useTranslation("common") | ||||
const [lastRequestTime, setLastRequestTime] = useState(Date.now()); | const [lastRequestTime, setLastRequestTime] = useState(Date.now()); | ||||
const [logoutInterval, setLogoutInterval] = useState(1); // minute | const [logoutInterval, setLogoutInterval] = useState(1); // minute | ||||
const [state, setState] = useState('Active'); | const [state, setState] = useState('Active'); | ||||
const { data: session } = useSession(); | |||||
const onIdle = () => { | const onIdle = () => { | ||||
setLastRequestTime(Date.now()); | setLastRequestTime(Date.now()); | ||||
setState('Idle') | setState('Idle') | ||||
} | } | ||||
const onActive = () => { | const onActive = () => { | ||||
setLastRequestTime(Date.now()); | setLastRequestTime(Date.now()); | ||||
setState('Active') | setState('Active') | ||||
} | } | ||||
const { | const { | ||||
isLastActiveTab, | isLastActiveTab, | ||||
} = useIdleTimer({ | } = useIdleTimer({ | ||||
@@ -42,21 +43,21 @@ const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children, isUse | |||||
crossTab: true, | crossTab: true, | ||||
syncTimers: 200, | syncTimers: 200, | ||||
}) | }) | ||||
const lastActiveTab = isLastActiveTab() === null ? 'loading' : isLastActiveTab() | const lastActiveTab = isLastActiveTab() === null ? 'loading' : isLastActiveTab() | ||||
const getLogoutInterval = () => { | const getLogoutInterval = () => { | ||||
if (isUserLoggedIn && logoutInterval === 1) { | |||||
if (session && logoutInterval === 1) { | |||||
//TODO: get auto logout time here | //TODO: get auto logout time here | ||||
setLogoutInterval(60); | setLogoutInterval(60); | ||||
} | } | ||||
else { | else { | ||||
if (!isUserLoggedIn && logoutInterval > 1) { | |||||
if (!session && logoutInterval > 1) { | |||||
setLogoutInterval(1); | setLogoutInterval(1); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
useEffect(() => { | useEffect(() => { | ||||
getLogoutInterval() | getLogoutInterval() | ||||
const interval = setInterval(async () => { | const interval = setInterval(async () => { | ||||
@@ -64,6 +65,8 @@ const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children, isUse | |||||
// if (isPasswordExpiry()) { | // if (isPasswordExpiry()) { | ||||
// navigate('/user/changePassword'); | // navigate('/user/changePassword'); | ||||
// } | // } | ||||
// console.log(state) | |||||
// console.log(lastActiveTab) | |||||
if (state !== "Active" && lastActiveTab) { | if (state !== "Active" && lastActiveTab) { | ||||
const timeElapsed = currentTime - lastRequestTime; | const timeElapsed = currentTime - lastRequestTime; | ||||
if (timeElapsed >= logoutInterval * 60 * 1000) { | if (timeElapsed >= logoutInterval * 60 * 1000) { | ||||
@@ -75,18 +78,18 @@ const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children, isUse | |||||
} | } | ||||
} | } | ||||
}, 1000); // Check every second | }, 1000); // Check every second | ||||
return () => { | return () => { | ||||
clearInterval(interval); | clearInterval(interval); | ||||
}; | }; | ||||
}, [lastRequestTime, logoutInterval]); | }, [lastRequestTime, logoutInterval]); | ||||
return ( | return ( | ||||
<TimerContext.Provider value={{ lastRequestTime, setLastRequestTime }}> | <TimerContext.Provider value={{ lastRequestTime, setLastRequestTime }}> | ||||
{children} | {children} | ||||
</TimerContext.Provider> | </TimerContext.Provider> | ||||
); | ); | ||||
}; | }; | ||||
export default AutoLogoutProvider; | export default AutoLogoutProvider; |
@@ -1,14 +1,8 @@ | |||||
import React from "react"; | import React from "react"; | ||||
import AutoLogoutProvider from "./AutoLogoutProvider"; | import AutoLogoutProvider from "./AutoLogoutProvider"; | ||||
import { getServerSession } from "next-auth"; | |||||
import { authOptions } from "@/config/authConfig"; | |||||
const AutoLogoutProviderWrapper: React.FC = async () => { | const AutoLogoutProviderWrapper: React.FC = async () => { | ||||
const session = await getServerSession(authOptions) | |||||
const isUserLoggedIn = session?.user !== null | |||||
return <AutoLogoutProvider isUserLoggedIn={isUserLoggedIn}/>; | |||||
return <AutoLogoutProvider/>; | |||||
}; | }; | ||||
export default AutoLogoutProviderWrapper; | 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: { | callbacks: { | ||||
jwt(params) { | jwt(params) { | ||||
// Add the data from user to the token | // 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; | return newToken; | ||||
}, | }, | ||||
session({ session, token }) { | session({ session, token }) { | ||||
// console.log(token.accessToken as string | undefined) | |||||
const sessionWithToken: SessionWithTokens = { | const sessionWithToken: SessionWithTokens = { | ||||
...session, | ...session, | ||||
role: token.role as string, | role: token.role as string, | ||||
@@ -77,5 +92,5 @@ export const authOptions: AuthOptions = { | |||||
// console.log(sessionWithToken) | // console.log(sessionWithToken) | ||||
return sessionWithToken; | return sessionWithToken; | ||||
}, | }, | ||||
}, | |||||
} | |||||
}; | }; |