| @@ -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; | |||
| }, | |||
| }, | |||
| } | |||
| }; | |||