From 0cc9d0e94e76902f606e19df3e110f034d290f9d Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 15:18:37 +0700 Subject: [PATCH 1/3] hotfix: Centralize SSO redirection logic into a new helper with loop protection, integrate it into the HTTP client and `RequireAuth` component, and add an authentication failure UI. --- src/components/helper/RequireAuth.tsx | 48 ++++++++++++++++----------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 119d74cb..53853b96 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -1,54 +1,46 @@ 'use client'; import { ReactNode, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import useSWRImmutable from 'swr/immutable'; +import useSWR from 'swr'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; import { AxiosError } from 'axios'; +import { redirectToSSO } from '@/lib/auth-helper'; interface RequireAuthProps { children?: ReactNode; } const RequireAuth = ({ children }: RequireAuthProps) => { - const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); const { data: userResponse, isLoading: isLoadingUserResponse, error: userErrorResponse, - } = useSWRImmutable< + } = useSWR< GetMeResponse & { ok?: boolean }, AxiosError, SWRHttpKey >('/sso/userinfo', httpClientFetcher, { shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, }); - useEffect(() => { - setIsLoadingUser(isLoadingUserResponse); - }, [isLoadingUserResponse, setIsLoadingUser]); - useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else if ( - isResponseError(userErrorResponse?.response?.data) && - typeof window !== 'undefined' - ) { - router.replace( - `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` - ); } - }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); + }, [userResponse, setUser]); + + // Explicitly handle 401 redirect from the component level + useEffect(() => { + if (userErrorResponse?.response?.status === 401) { + redirectToSSO(); + } + }, [userErrorResponse]); if (isLoadingUserResponse && !userResponse && !userErrorResponse) { return ( @@ -58,6 +50,24 @@ const RequireAuth = ({ children }: RequireAuthProps) => { ); } + if (userErrorResponse) { + return ( +
+

Authentication Failed

+

+ Please try refreshing the page or contact support if the problem + persists. +

+ +
+ ); + } + return <>{isResponseSuccess(userResponse) && children}; }; From 46d70e36dd96c40edbd67e3fdf6edfed88937550 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 15:21:10 +0700 Subject: [PATCH 2/3] feat: create auth-helper file and redirectToSSO helper function --- src/lib/auth-helper.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/lib/auth-helper.ts diff --git a/src/lib/auth-helper.ts b/src/lib/auth-helper.ts new file mode 100644 index 00000000..97d31a9f --- /dev/null +++ b/src/lib/auth-helper.ts @@ -0,0 +1,25 @@ +/** + * Redirects the user to the SSO login page with loop protection. + * + * This function checks a session storage timestamp to ensure that redirects + * do not happen too frequently (blocking infinite redirect loops). + */ +export const redirectToSSO = () => { + if (typeof window === 'undefined') return; + + const lastRedirect = sessionStorage.getItem('auth_redirect_timestamp'); + const now = Date.now(); + + // Loop protection: allow redirect only if last one was > 2 seconds ago + // or if no redirect has happened yet. + if (!lastRedirect || now - parseInt(lastRedirect, 10) > 2000) { + sessionStorage.setItem('auth_redirect_timestamp', now.toString()); + // const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`; + + const ltiSsoStart = `${process.env.NEXT_PUBLIC_API_BASE_URL as string}/sso/start?client_id=${process.env.NEXT_PUBLIC_CLIENT_ID as string}&redirect_url=${window.location.href}`; + const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${ltiSsoStart}`; + window.location.href = ssoLoginUrl; + } else { + console.error('Redirect loop detected. Aborting redirect.'); + } +}; From 757e0435ac7fc711caa6b322ecc4dc7dc56e6b3b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 15:21:46 +0700 Subject: [PATCH 3/3] hotfix: use redirectToSSO function --- src/services/http/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/http/client.ts b/src/services/http/client.ts index f9389a16..68b5282a 100644 --- a/src/services/http/client.ts +++ b/src/services/http/client.ts @@ -2,6 +2,8 @@ import axios from 'axios'; import type { AxiosError, AxiosRequestConfig } from 'axios'; import { RequestOptions } from '@/services/http/base'; +import { redirectToSSO } from '@/lib/auth-helper'; + const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ''; const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 10_000 }); @@ -9,8 +11,7 @@ axiosClient.interceptors.response.use( (response) => response, (error: AxiosError) => { if (error.response?.status === 401) { - const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`; - window.location.href = ssoLoginUrl; + redirectToSSO(); } return Promise.reject(error);