Procházet zdrojové kódy

Update auto logout & refresh access token

tags/Baseline_180220205_Frontend
cyril.tsui před 8 měsíci
rodič
revize
29795d2bc3
11 změnil soubory, kde provedl 183 přidání a 35 odebrání
  1. +12
    -7
      src/app/(main)/layout.tsx
  2. +24
    -0
      src/app/api/token/actions.ts
  3. +3
    -0
      src/app/utils/fetchUtil.ts
  4. +21
    -18
      src/components/AutoLogoutProvider/AutoLogoutProvider.tsx
  5. +1
    -7
      src/components/AutoLogoutProvider/AutoLogoutProviderWrapper.tsx
  6. +90
    -0
      src/components/RefreshTokenProvider/RefreshTokenProvider.tsx
  7. +9
    -0
      src/components/RefreshTokenProvider/RefreshTokenProviderWrapper.tsx
  8. +1
    -0
      src/components/RefreshTokenProvider/index.ts
  9. +3
    -0
      src/components/SessionProvider/SessionProvider.tsx
  10. +1
    -0
      src/components/SessionProvider/index.ts
  11. +18
    -3
      src/config/authConfig.ts

+ 12
- 7
src/app/(main)/layout.tsx Zobrazit soubor

@@ -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>
</>
);


+ 24
- 0
src/app/api/token/actions.ts Zobrazit soubor

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

+ 3
- 0
src/app/utils/fetchUtil.ts Zobrazit soubor

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


+ 21
- 18
src/components/AutoLogoutProvider/AutoLogoutProvider.tsx Zobrazit soubor

@@ -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
- 7
src/components/AutoLogoutProvider/AutoLogoutProviderWrapper.tsx Zobrazit soubor

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

+ 90
- 0
src/components/RefreshTokenProvider/RefreshTokenProvider.tsx Zobrazit soubor

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

+ 9
- 0
src/components/RefreshTokenProvider/RefreshTokenProviderWrapper.tsx Zobrazit soubor

@@ -0,0 +1,9 @@
import React from "react";
import RefreshTokenProvider from "./RefreshTokenProvider";

const RefreshTokenProviderWrapper: React.FC = async () => {

return <RefreshTokenProvider/>;
};

export default RefreshTokenProviderWrapper;

+ 1
- 0
src/components/RefreshTokenProvider/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./RefreshTokenProviderWrapper";

+ 3
- 0
src/components/SessionProvider/SessionProvider.tsx Zobrazit soubor

@@ -0,0 +1,3 @@
"use client";

export { SessionProvider as default } from "next-auth/react";

+ 1
- 0
src/components/SessionProvider/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./SessionProvider";

+ 18
- 3
src/config/authConfig.ts Zobrazit soubor

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

Načítá se…
Zrušit
Uložit