From 80f0c052bd0691a2c61838db88c935a3401b03bc Mon Sep 17 00:00:00 2001 From: Fritz Madden Date: Mon, 10 Feb 2025 17:57:24 -0700 Subject: [PATCH] Implemented Hook and Helper function for redirects This navigate() and window.location.assign() replacement maintains x-ms-routing-name parameter in the query string, and can be used for other query parameters later if need be. --- user-interface/src/lib/components/GoHome.tsx | 6 +-- .../src/lib/hooks/UseCamsNavigator.ts | 47 +++++++++++++++++++ user-interface/src/login/AccessDenied.tsx | 5 +- user-interface/src/login/Session.tsx | 9 ++-- user-interface/src/login/SessionEnd.tsx | 9 ++-- user-interface/src/login/broadcast-logout.ts | 3 +- user-interface/src/login/http401-logout.ts | 3 +- user-interface/src/login/inactive-logout.ts | 3 +- 8 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 user-interface/src/lib/hooks/UseCamsNavigator.ts diff --git a/user-interface/src/lib/components/GoHome.tsx b/user-interface/src/lib/components/GoHome.tsx index 8d0a62fed..68eb0e033 100644 --- a/user-interface/src/lib/components/GoHome.tsx +++ b/user-interface/src/lib/components/GoHome.tsx @@ -1,6 +1,6 @@ import { LOGIN_SUCCESS_PATH } from '@/login/login-library'; import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import useCamsNavigator from '../hooks/UseCamsNavigator'; export type GoHomeProps = { path?: string; @@ -17,9 +17,9 @@ export type GoHomeProps = { * @returns */ export function GoHome(props: GoHomeProps) { - const navigate = useNavigate(); + const navigator = useCamsNavigator(); useEffect(() => { - navigate(props.path ?? LOGIN_SUCCESS_PATH); + navigator.navigateTo(props.path ?? LOGIN_SUCCESS_PATH); }, []); return <>; } diff --git a/user-interface/src/lib/hooks/UseCamsNavigator.ts b/user-interface/src/lib/hooks/UseCamsNavigator.ts new file mode 100644 index 000000000..c2dc6a791 --- /dev/null +++ b/user-interface/src/lib/hooks/UseCamsNavigator.ts @@ -0,0 +1,47 @@ +import { useLocation, useNavigate, Location } from 'react-router-dom'; + +function getFinalDestination( + destination: string, + location: globalThis.Location | Location = window.location, +) { + let qParams: string = ''; + const msRoutingName = 'x-ms-routing-name'; + if (location.search.includes(msRoutingName)) { + const query: Record = location.search + .substring(1) + .split('&') + .reduce>( + (acc, item) => { + const [key, val] = item.split('='); + if (key && val) acc[key] = val; + return acc; + }, + {} as Record, + ); + if (query[msRoutingName]) { + qParams += `?${msRoutingName}=${query[msRoutingName]}`; + } + } + return destination + qParams; +} + +export const redirectTo = ( + destination: string, + location: globalThis.Location | Location = window.location, +) => { + window.location.assign(getFinalDestination(destination, location)); +}; + +export default function useCamsNavigator() { + const location = useLocation(); + const navigate = useNavigate(); + + const navigateTo = (destination: string) => { + navigate(getFinalDestination(destination, location)); + }; + + return { + navigateTo, + redirectTo, + }; +} diff --git a/user-interface/src/login/AccessDenied.tsx b/user-interface/src/login/AccessDenied.tsx index 6afa51247..d3b537ecb 100644 --- a/user-interface/src/login/AccessDenied.tsx +++ b/user-interface/src/login/AccessDenied.tsx @@ -2,6 +2,7 @@ import Alert, { UswdsAlertStyle } from '@/lib/components/uswds/Alert'; import { BlankPage } from './BlankPage'; import { LOGIN_PATH } from './login-library'; import Button from '@/lib/components/uswds/Button'; +import useCamsNavigator from '@/lib/hooks/UseCamsNavigator'; const DEFAULT_MESSAGE = 'Access to this application is denied without successful authentication.'; @@ -10,10 +11,12 @@ export type AccessDeniedProps = { }; export function AccessDenied(props: AccessDeniedProps) { + const navigator = useCamsNavigator(); + function handleLoginRedirect() { const { host, protocol } = window.location; const loginUri = protocol + '//' + host + LOGIN_PATH; - window.location.assign(loginUri); + navigator.redirectTo(loginUri); } return ( diff --git a/user-interface/src/login/Session.tsx b/user-interface/src/login/Session.tsx index 895a1e741..8ce2fd840 100644 --- a/user-interface/src/login/Session.tsx +++ b/user-interface/src/login/Session.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren, useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { LOGIN_PATHS, LOGIN_BASE_PATH } from './login-library'; import { LocalStorage } from '@/lib/utils/local-storage'; import Api2 from '@/lib/models/api2'; @@ -7,6 +7,7 @@ import { AccessDenied } from './AccessDenied'; import { Interstitial } from './Interstitial'; import { CamsSession } from '@common/cams/session'; import { CamsUser } from '@common/cams/users'; +import useCamsNavigator from '@/lib/hooks/UseCamsNavigator'; type SessionState = { isLoaded: boolean; @@ -59,7 +60,7 @@ export type SessionProps = Omit & PropsWithChildren & { use export function Session(props: SessionProps) { const { accessToken, provider, expires, issuer } = props; const user = props.user ?? { id: '', name: '' }; - const navigate = useNavigate(); + const navigator = useCamsNavigator(); const location = useLocation(); const { state, actions } = useStateAndActions(); @@ -70,7 +71,9 @@ export function Session(props: SessionProps) { }, []); useEffect(() => { - if (LOGIN_PATHS.includes(location.pathname)) navigate(LOGIN_BASE_PATH); + if (LOGIN_PATHS.includes(location.pathname)) { + navigator.navigateTo(LOGIN_BASE_PATH); + } }, [state.isLoaded === true && !state.isError]); if (!state.isLoaded) { diff --git a/user-interface/src/login/SessionEnd.tsx b/user-interface/src/login/SessionEnd.tsx index 150f9756a..24e5f85b3 100644 --- a/user-interface/src/login/SessionEnd.tsx +++ b/user-interface/src/login/SessionEnd.tsx @@ -1,18 +1,19 @@ import { useEffect } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import Alert, { UswdsAlertStyle } from '@/lib/components/uswds/Alert'; import Button from '@/lib/components/uswds/Button'; import { LocalStorage } from '@/lib/utils/local-storage'; import { LOGIN_PATH, LOGOUT_SESSION_END_PATH } from './login-library'; import { BlankPage } from './BlankPage'; import { broadcastLogout } from '@/login/broadcast-logout'; +import useCamsNavigator from '@/lib/hooks/UseCamsNavigator'; export function SessionEnd() { const location = useLocation(); - const navigate = useNavigate(); + const navigator = useCamsNavigator(); function handleLoginRedirect() { - navigate(LOGIN_PATH); + navigator.navigateTo(LOGIN_PATH); } LocalStorage.removeSession(); @@ -21,7 +22,7 @@ export function SessionEnd() { useEffect(() => { if (location.pathname !== LOGOUT_SESSION_END_PATH) { - navigate(LOGOUT_SESSION_END_PATH); + navigator.navigateTo(LOGOUT_SESSION_END_PATH); } }, []); diff --git a/user-interface/src/login/broadcast-logout.ts b/user-interface/src/login/broadcast-logout.ts index 2c836acec..d464e9a77 100644 --- a/user-interface/src/login/broadcast-logout.ts +++ b/user-interface/src/login/broadcast-logout.ts @@ -1,3 +1,4 @@ +import { redirectTo } from '@/lib/hooks/UseCamsNavigator'; import { BroadcastChannelHumble } from '@/lib/humble/broadcast-channel-humble'; import { LOGOUT_PATH } from '@/login/login-library'; @@ -6,7 +7,7 @@ let channel: BroadcastChannelHumble; export function handleLogoutBroadcast() { const { host, protocol } = window.location; const logoutUri = protocol + '//' + host + LOGOUT_PATH; - window.location.assign(logoutUri); + redirectTo(logoutUri); channel?.close(); } diff --git a/user-interface/src/login/http401-logout.ts b/user-interface/src/login/http401-logout.ts index 5bf0d4678..e4de2e022 100644 --- a/user-interface/src/login/http401-logout.ts +++ b/user-interface/src/login/http401-logout.ts @@ -1,10 +1,11 @@ import { isCamsApi } from '@/configuration/apiConfiguration'; import { LOGOUT_PATH } from './login-library'; +import { redirectTo } from '@/lib/hooks/UseCamsNavigator'; export async function http401Hook(response: Response) { if (response.status === 401 && isCamsApi(response.url)) { const { host, protocol } = window.location; const logoutUri = protocol + '//' + host + LOGOUT_PATH; - window.location.assign(logoutUri); + redirectTo(logoutUri); } } diff --git a/user-interface/src/login/inactive-logout.ts b/user-interface/src/login/inactive-logout.ts index f2bb621a8..8bc8d40d0 100644 --- a/user-interface/src/login/inactive-logout.ts +++ b/user-interface/src/login/inactive-logout.ts @@ -1,5 +1,6 @@ import LocalStorage from '@/lib/utils/local-storage'; import { LOGOUT_PATH } from './login-library'; +import { redirectTo } from '@/lib/hooks/UseCamsNavigator'; const POLLING_INTERVAL = 60000; // milliseconds const TIMEOUT_MINUTES = import.meta.env['CAMS_INACTIVE_TIMEOUT'] ?? 30; @@ -18,7 +19,7 @@ export function checkForInactivity() { const { host, protocol } = window.location; const logoutUri = protocol + '//' + host + LOGOUT_PATH; - window.location.assign(logoutUri); + redirectTo(logoutUri); } }