diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 438a6b9..8544c9a 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -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" }, }} > - - - - - {children} - - + + + + + + + {children} + + + ); diff --git a/src/app/api/token/actions.ts b/src/app/api/token/actions.ts new file mode 100644 index 0000000..1ba5eb0 --- /dev/null +++ b/src/app/api/token/actions.ts @@ -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(`${BASE_API_URL}/refresh-token`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); +}); \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index b6498de..b5868a0 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -22,6 +22,7 @@ export const serverFetch: typeof fetch = async (input, init) => { const session = await getServerSession(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(...args: FetchParams) { switch (response.status) { case 401: signOutUser(); + case 417: + signOutUser(); case 422: throw new ServerFetchError( JSON.parse(errorText).error, diff --git a/src/components/AutoLogoutProvider/AutoLogoutProvider.tsx b/src/components/AutoLogoutProvider/AutoLogoutProvider.tsx index e82244f..e33b028 100644 --- a/src/components/AutoLogoutProvider/AutoLogoutProvider.tsx +++ b/src/components/AutoLogoutProvider/AutoLogoutProvider.tsx @@ -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>; } - + export const TimerContext = createContext(undefined); - + interface AutoLogoutProviderProps { children?: ReactNode; - isUserLoggedIn: boolean; } - -const AutoLogoutProvider: React.FC = ({ children, isUserLoggedIn }) => { + +const AutoLogoutProvider: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ children, isUse } } }, 1000); // Check every second - + return () => { clearInterval(interval); }; }, [lastRequestTime, logoutInterval]); - - + + return ( {children} ); }; - + export default AutoLogoutProvider; \ No newline at end of file diff --git a/src/components/AutoLogoutProvider/AutoLogoutProviderWrapper.tsx b/src/components/AutoLogoutProvider/AutoLogoutProviderWrapper.tsx index ffdc593..24c88bf 100644 --- a/src/components/AutoLogoutProvider/AutoLogoutProviderWrapper.tsx +++ b/src/components/AutoLogoutProvider/AutoLogoutProviderWrapper.tsx @@ -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 ; + return ; }; export default AutoLogoutProviderWrapper; diff --git a/src/components/RefreshTokenProvider/RefreshTokenProvider.tsx b/src/components/RefreshTokenProvider/RefreshTokenProvider.tsx new file mode 100644 index 0000000..8e826ca --- /dev/null +++ b/src/components/RefreshTokenProvider/RefreshTokenProvider.tsx @@ -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(undefined); + +interface RefreshTokenProviderProps { + children?: ReactNode; +} + +const RefreshTokenProvider: React.FC = ({ children }) => { + const { data, update } = useSession(); + const session = useRef(data as SessionWithTokens) + // const token = useRef(session.current?.accessToken ?? null) + const isRefresh = useRef(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 ( + + {children} + + ); +}; + +export default RefreshTokenProvider; \ No newline at end of file diff --git a/src/components/RefreshTokenProvider/RefreshTokenProviderWrapper.tsx b/src/components/RefreshTokenProvider/RefreshTokenProviderWrapper.tsx new file mode 100644 index 0000000..174adc9 --- /dev/null +++ b/src/components/RefreshTokenProvider/RefreshTokenProviderWrapper.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import RefreshTokenProvider from "./RefreshTokenProvider"; + +const RefreshTokenProviderWrapper: React.FC = async () => { + + return ; +}; + +export default RefreshTokenProviderWrapper; \ No newline at end of file diff --git a/src/components/RefreshTokenProvider/index.ts b/src/components/RefreshTokenProvider/index.ts new file mode 100644 index 0000000..f6759ec --- /dev/null +++ b/src/components/RefreshTokenProvider/index.ts @@ -0,0 +1 @@ +export { default } from "./RefreshTokenProviderWrapper"; \ No newline at end of file diff --git a/src/components/SessionProvider/SessionProvider.tsx b/src/components/SessionProvider/SessionProvider.tsx new file mode 100644 index 0000000..54267a7 --- /dev/null +++ b/src/components/SessionProvider/SessionProvider.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { SessionProvider as default } from "next-auth/react"; \ No newline at end of file diff --git a/src/components/SessionProvider/index.ts b/src/components/SessionProvider/index.ts new file mode 100644 index 0000000..c6d78b1 --- /dev/null +++ b/src/components/SessionProvider/index.ts @@ -0,0 +1 @@ +export { default } from "./SessionProvider"; diff --git a/src/config/authConfig.ts b/src/config/authConfig.ts index 2edd89f..5e12b22 100644 --- a/src/config/authConfig.ts +++ b/src/config/authConfig.ts @@ -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; }, - }, + } };