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