From 5825b43dfadfd69ae52cefc5fac5fed219605a6b Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R <108045773+ratheesh-aot@users.noreply.github.com> Date: Wed, 10 Apr 2024 09:40:30 -0700 Subject: [PATCH] DESENG-542: Keycloak auth method and redirection fixed (#2456) * DESENG-542: Authentication fixes * DESENG-542: URL Redirection fix * Updated Changelog * Fixing review comments * Fixing review comment --- CHANGELOG.MD | 5 ++ met-web/src/App.tsx | 50 ++++++++-------- .../components/auth/AuthKeycloakContext.tsx | 57 +++++++++++++++++++ .../src/components/layout/Footer/index.tsx | 6 +- .../layout/Header/InternalHeader.tsx | 8 +-- .../layout/SideNav/SideNavElements.tsx | 2 +- met-web/src/constants/tenantConstants.ts | 2 +- met-web/src/index.tsx | 15 +++-- met-web/src/routes/AuthenticatedRoutes.tsx | 2 +- met-web/src/services/userService/index.ts | 24 ++++---- met-web/src/utils/index.ts | 13 +++++ 11 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 met-web/src/components/auth/AuthKeycloakContext.tsx diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 2464ac7bc..f39b993b5 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -3,6 +3,11 @@ ## April 09, 2024 +- **Task**: MET Web - Some URLs not taking users to correct locations/timing out [DESENG-542](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-542) + - Created authentication provider to know the logged-in state before the App component is rendered. + - Optimized tenant identification from the url. + - Optimized path segments identification. + - **Task**: CSS Selector specificity [🎟️ DESENG-577](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-577) - Replace the `!important` flag with more specific CSS selectors, specifically within dropdowns, to ensure that the correct styles are applied, and that states we have not yet designed for diff --git a/met-web/src/App.tsx b/met-web/src/App.tsx index 3df571a04..1916f94b1 100644 --- a/met-web/src/App.tsx +++ b/met-web/src/App.tsx @@ -1,8 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useContext } from 'react'; import '@bcgov/design-tokens/css-prefixed/variables.css'; // Will be available to use in all component import './App.scss'; import { Route, BrowserRouter as Router, Routes } from 'react-router-dom'; -import UserService from './services/userService'; import { useAppSelector, useAppDispatch } from './hooks'; import { MidScreenLoader, MobileToolbar } from './components/common'; import { Box, Container, useMediaQuery, Theme, Toolbar } from '@mui/material'; @@ -26,6 +25,8 @@ import { openNotification } from 'services/notificationService/notificationSlice import i18n from './i18n'; import DocumentTitle from 'DocumentTitle'; import { Language } from 'constants/language'; +import { AuthKeyCloakContext } from './components/auth/AuthKeycloakContext'; +import { determinePathSegments, findTenantInPath } from './utils'; interface Translations { [languageId: string]: { [key: string]: string }; @@ -36,17 +37,13 @@ const App = () => { const isMediumScreen: boolean = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')); const dispatch = useAppDispatch(); const roles = useAppSelector((state) => state.user.roles); - const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); const authenticationLoading = useAppSelector((state) => state.user.authentication.loading); - const pathSegments = window.location.pathname.split('/'); + const pathSegments = determinePathSegments(); const language: LanguageState = useAppSelector((state) => state.language); - const basename = pathSegments[1].toLowerCase(); + const basename = findTenantInPath(); const tenant: TenantState = useAppSelector((state) => state.tenant); const [translations, setTranslations] = useState({}); - - useEffect(() => { - UserService.initKeycloak(dispatch); - }, [dispatch]); + const { isAuthenticated } = useContext(AuthKeyCloakContext); useEffect(() => { sessionStorage.setItem('apiurl', String(AppConfig.apiUrl)); @@ -83,28 +80,35 @@ const App = () => { }; const loadTenant = () => { + // Load default tenant if in a single tenant environment if (AppConfig.tenant.isSingleTenantEnvironment) { fetchTenant(AppConfig.tenant.defaultTenant); return; } + const defaultTenant = AppConfig.tenant.defaultTenant; + const defaultLanguage = AppConfig.language.defaultLanguageId; + + // Determine the appropriate URL to redirect + const redirectToDefaultUrl = (base: string, includeLanguage = true) => { + const languageSegment = includeLanguage ? `/${defaultLanguage}` : '/home'; + window.location.replace(`/${base}${languageSegment}`); + }; + + const shouldIncludeLanguage = !isAuthenticated; + if (basename) { fetchTenant(basename); - if (pathSegments.length === 2) { - const defaultLanguage = AppConfig.language.defaultLanguageId; // Set the default language here - const defaultUrl = `/${basename}/${defaultLanguage}`; - window.location.replace(defaultUrl); + // if language or admin dashboard url not set + if (pathSegments.length < 2) { + redirectToDefaultUrl(basename, shouldIncludeLanguage); } - return; - } - - if (!basename && AppConfig.tenant.defaultTenant) { - const defaultLanguage = AppConfig.language.defaultLanguageId; // Set the default language here - const defaultUrl = `/${AppConfig.tenant.defaultTenant}/${defaultLanguage}`; - window.location.replace(defaultUrl); + } else if (defaultTenant) { + fetchTenant(defaultTenant); + redirectToDefaultUrl(defaultTenant, shouldIncludeLanguage); + } else { + dispatch(loadingTenant(false)); } - - dispatch(loadingTenant(false)); }; const preloadTranslations = async () => { @@ -185,7 +189,7 @@ const App = () => { ); } - if (!isLoggedIn) { + if (!isAuthenticated) { return ( diff --git a/met-web/src/components/auth/AuthKeycloakContext.tsx b/met-web/src/components/auth/AuthKeycloakContext.tsx new file mode 100644 index 000000000..1d2734f9e --- /dev/null +++ b/met-web/src/components/auth/AuthKeycloakContext.tsx @@ -0,0 +1,57 @@ +import React, { createContext, useState, useEffect } from 'react'; +import { useAppDispatch } from 'hooks'; +import UserService from '../../services/userService'; +import { _kc } from 'constants/tenantConstants'; +const KeycloakData = _kc; + +export interface AuthKeyCloakContextProps { + isAuthenticated: boolean; + isAuthenticating: boolean; // Add a flag to indicate ongoing authentication process + keycloakInstance: Keycloak.default | null; +} + +export const AuthKeyCloakContext = createContext({ + isAuthenticated: false, + isAuthenticating: true, // Initially, authentication is in progress + keycloakInstance: null, // Initial value +}); + +export const AuthKeyCloakContextProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => { + const dispatch = useAppDispatch(); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isAuthenticating, setIsAuthenticating] = useState(true); // State to manage authentication loading status + + useEffect(() => { + const initAuth = async () => { + try { + const authenticated = await KeycloakData.init({ + onLoad: 'check-sso', + silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', + pkceMethod: 'S256', + checkLoginIframe: false, + }); + setIsAuthenticated(authenticated); // Update authentication state + UserService.setKeycloakInstance(KeycloakData); + UserService.setAuthData(dispatch); + } catch (error) { + console.error('Authentication initialization failed:', error); + } finally { + setIsAuthenticating(false); // Indicate that authentication process is complete + } + }; + initAuth(); + }, [dispatch]); + + // Render children only after the authentication status is determined + return ( + + {!isAuthenticating && children} + + ); +}; diff --git a/met-web/src/components/layout/Footer/index.tsx b/met-web/src/components/layout/Footer/index.tsx index 4d1aef053..eaaddcec5 100644 --- a/met-web/src/components/layout/Footer/index.tsx +++ b/met-web/src/components/layout/Footer/index.tsx @@ -79,7 +79,11 @@ const Footer = () => { {translate('footer.moreInfo')} - + {translate('footer.home')} diff --git a/met-web/src/components/layout/Header/InternalHeader.tsx b/met-web/src/components/layout/Header/InternalHeader.tsx index da87c06fe..e321388da 100644 --- a/met-web/src/components/layout/Header/InternalHeader.tsx +++ b/met-web/src/components/layout/Header/InternalHeader.tsx @@ -69,7 +69,7 @@ const InternalHeader = ({ drawerWidth = 280 }: HeaderProps) => { cursor: 'pointer', }} onClick={() => { - navigate('/'); + navigate('/home'); }} onError={(_e) => { setImageError(true); @@ -87,7 +87,7 @@ const InternalHeader = ({ drawerWidth = 280 }: HeaderProps) => { marginRight: { xs: '1em', md: '3em' }, }} onClick={() => { - navigate('/'); + navigate('/home'); }} alt="British Columbia Logo" /> @@ -95,7 +95,7 @@ const InternalHeader = ({ drawerWidth = 280 }: HeaderProps) => { {isMediumScreen ? ( { - navigate('/'); + navigate('/home'); }} sx={{ flexGrow: 1, cursor: 'pointer' }} > @@ -104,7 +104,7 @@ const InternalHeader = ({ drawerWidth = 280 }: HeaderProps) => { ) : ( { - navigate('/'); + navigate('/home'); }} sx={{ flexGrow: 1, cursor: 'pointer' }} > diff --git a/met-web/src/components/layout/SideNav/SideNavElements.tsx b/met-web/src/components/layout/SideNav/SideNavElements.tsx index d9c41ab70..fdb73a8e1 100644 --- a/met-web/src/components/layout/SideNav/SideNavElements.tsx +++ b/met-web/src/components/layout/SideNav/SideNavElements.tsx @@ -9,7 +9,7 @@ interface Route { } export const Routes: Route[] = [ - { name: 'Home', path: '/', base: '/', authenticated: false, allowedRoles: [] }, + { name: 'Home', path: '/home', base: '/', authenticated: false, allowedRoles: [] }, { name: 'Engagements', path: '/engagements', diff --git a/met-web/src/constants/tenantConstants.ts b/met-web/src/constants/tenantConstants.ts index b79122a26..c1c8152f3 100644 --- a/met-web/src/constants/tenantConstants.ts +++ b/met-web/src/constants/tenantConstants.ts @@ -10,4 +10,4 @@ export const tenantDetail: ITenantDetail = { }; // eslint-disable-next-line -export const _kc: Keycloak.KeycloakInstance = new (Keycloak as any)(tenantDetail); +export const _kc: Keycloak.default = new (Keycloak as any)(tenantDetail); diff --git a/met-web/src/index.tsx b/met-web/src/index.tsx index cb3a25988..0af4e61ad 100644 --- a/met-web/src/index.tsx +++ b/met-web/src/index.tsx @@ -9,6 +9,7 @@ import { Formio } from '@formio/react'; import MetFormioComponents from 'met-formio'; import '@bcgov/bc-sans/css/BCSans.css'; import { HelmetProvider } from 'react-helmet-async'; +import { AuthKeyCloakContextProvider } from 'components/auth/AuthKeycloakContext'; Formio.use(MetFormioComponents); Formio.Utils.Evaluator.noeval = false; @@ -17,15 +18,19 @@ Formio.Utils.Evaluator.noeval = false; const root = ReactDOM.createRoot(document.getElementById('root')!); root.render( // + - - - - - + + + + + + + , + // ); diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index 03605e52f..4c8e9b038 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -32,7 +32,7 @@ const AuthenticatedRoutes = () => { - } /> + } /> } /> } /> } /> diff --git a/met-web/src/services/userService/index.ts b/met-web/src/services/userService/index.ts index 7996170d1..cf8554b98 100644 --- a/met-web/src/services/userService/index.ts +++ b/met-web/src/services/userService/index.ts @@ -1,4 +1,3 @@ -import { _kc } from 'constants/tenantConstants'; import { userToken, userDetails, @@ -17,18 +16,20 @@ import { getMembershipsByUser } from 'services/membershipService'; import { USER_ROLES } from 'services/userService/constants'; import { getBaseUrl } from 'helper'; -const KeycloakData = _kc; +let KeycloakData: Keycloak.default; + /** - * Initializes Keycloak instance. + * Setting Keycloak instance. */ -const initKeycloak = async (dispatch: Dispatch) => { +const setKeycloakInstance = (instance: Keycloak.default) => { + KeycloakData = instance; +}; +/** + * Setting user authentication data in storage + */ +const setAuthData = async (dispatch: Dispatch) => { try { - const authenticated = await KeycloakData.init({ - onLoad: 'check-sso', - silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', - pkceMethod: 'S256', - checkLoginIframe: false, - }); + const authenticated = !!KeycloakData.token; if (!authenticated) { console.warn('not authenticated!'); dispatch(userAuthentication(authenticated)); @@ -151,7 +152,7 @@ const getAssignedEngagements = async (externalId: string, roles: string[]) => { }; const UserService = { - initKeycloak, + setAuthData, updateUser, doLogin, doLogout, @@ -159,6 +160,7 @@ const UserService = { getToken, hasRole, hasAdminRole, + setKeycloakInstance, }; export default UserService; diff --git a/met-web/src/utils/index.ts b/met-web/src/utils/index.ts index a291ee300..aaa6ff31c 100644 --- a/met-web/src/utils/index.ts +++ b/met-web/src/utils/index.ts @@ -47,3 +47,16 @@ export const blobToFile = (theBlob: Blob, fileName: string): File => { //Cast to a File() type return theBlob; }; + +export const determinePathSegments = () => { + const url = new URL(window.location.href); + // filters out empty segments, which can occur if there are leading or trailing slashes in the pathname + const pathSegments = url.pathname.split('/').filter((segment) => segment.trim() !== ''); + return pathSegments; +}; + +export const findTenantInPath = () => { + // finding tenant from the path segments + const pathSegments = determinePathSegments(); + return pathSegments.length > 0 ? pathSegments[0].toLowerCase() : ''; +};