minor: add isApplicationLoading state in ApplicationStore (#16242)

* minor: add isApplicationLoading state and ApplicationStore

* minor: remove setIsUserAuthenticated(true) from the oidcAuthenticator Callback onSuccess

* chore: add comments

* chore: update isSigningIn to isSigningUp

* update comment

* chore: add switch in app router

* chore: improve setIsAuthenticated usage

* fix test

* add application loading in basic authenticator
This commit is contained in:
Sachin Chaurasiya 2024-05-15 15:40:04 +05:30 committed by GitHub
parent 386e80ca1b
commit 80ccb4e8a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 105 additions and 59 deletions

View File

@ -13,11 +13,12 @@
import { isNil } from 'lodash';
import React, { useCallback, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { Switch, useLocation } from 'react-router-dom';
import { useAnalytics } from 'use-analytics';
import { CustomEventTypes } from '../../generated/analytics/webAnalyticEventData';
import { useApplicationStore } from '../../hooks/useApplicationStore';
import AppContainer from '../AppContainer/AppContainer';
import Loader from '../common/Loader/Loader';
import { UnAuthenticatedAppRouter } from './UnAuthenticatedAppRouter';
const AppRouter = () => {
@ -26,7 +27,7 @@ const AppRouter = () => {
// web analytics instance
const analytics = useAnalytics();
const { isAuthenticated } = useApplicationStore();
const { isAuthenticated, isApplicationLoading } = useApplicationStore();
useEffect(() => {
const { pathname } = location;
@ -64,7 +65,22 @@ const AppRouter = () => {
return () => targetNode.removeEventListener('click', handleClickEvent);
}, [handleClickEvent]);
return isAuthenticated ? <AppContainer /> : <UnAuthenticatedAppRouter />;
/**
* isApplicationLoading is true when the application is loading in AuthProvider
* and is false when the application is loaded.
* If the application is loading, show the loader.
* If the user is authenticated, show the AppContainer.
* If the user is not authenticated, show the UnAuthenticatedAppRouter.
* */
if (isApplicationLoading) {
return <Loader fullScreen />;
}
return (
<Switch>
{isAuthenticated ? <AppContainer /> : <UnAuthenticatedAppRouter />}
</Switch>
);
};
export default AppRouter;

View File

@ -42,7 +42,7 @@ const BasicSignupPage = withSuspenseFallback(
);
export const UnAuthenticatedAppRouter = () => {
const { authConfig, isSigningIn } = useApplicationStore();
const { authConfig, isSigningUp } = useApplicationStore();
const isBasicAuthProvider =
authConfig &&
@ -78,7 +78,7 @@ export const UnAuthenticatedAppRouter = () => {
component={SamlCallback}
path={[ROUTES.SAML_CALLBACK, ROUTES.AUTH_CALLBACK]}
/>
{!isSigningIn && (
{!isSigningUp && (
<Route exact path={ROUTES.HOME}>
<Redirect to={ROUTES.SIGNIN} />
</Route>

View File

@ -25,6 +25,7 @@ import {
} from '../../../rest/auth-API';
import { useApplicationStore } from '../../../hooks/useApplicationStore';
import Loader from '../../common/Loader/Loader';
import { useBasicAuth } from '../AuthProviders/BasicAuthProvider';
interface BasicAuthenticatorInterface {
@ -41,6 +42,7 @@ const BasicAuthenticator = forwardRef(
getRefreshToken,
setRefreshToken,
setOidcToken,
isApplicationLoading,
} = useApplicationStore();
const handleSilentSignIn = async (): Promise<AccessTokenResponse> => {
@ -73,6 +75,17 @@ const BasicAuthenticator = forwardRef(
},
}));
/**
* isApplicationLoading is true when the application is loading in AuthProvider
* and is false when the application is loaded.
* If the application is loading, show the loader.
* If the user is authenticated, show the AppContainer.
* If the user is not authenticated, show the UnAuthenticatedAppRouter.
* */
if (isApplicationLoading) {
return <Loader fullScreen />;
}
return <Fragment>{children}</Fragment>;
}
);

View File

@ -25,7 +25,7 @@ export const GenericAuthenticator = forwardRef(
({ children }: { children: ReactNode }, ref) => {
const {
setIsAuthenticated,
setIsSigningIn,
setIsSigningUp,
removeOidcToken,
setOidcToken,
} = useApplicationStore();
@ -33,7 +33,7 @@ export const GenericAuthenticator = forwardRef(
const handleLogin = () => {
setIsAuthenticated(false);
setIsSigningIn(true);
setIsSigningUp(true);
window.location.assign('api/v1/auth/login');
};

View File

@ -65,13 +65,13 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(
) => {
const {
isAuthenticated,
setIsAuthenticated,
isSigningIn,
setIsSigningIn,
isSigningUp,
setIsSigningUp,
updateAxiosInterceptors,
currentUser,
newUser,
setOidcToken,
isApplicationLoading,
} = useApplicationStore();
const history = useHistory();
const userManager = useMemo(
@ -80,7 +80,7 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(
);
const login = () => {
setIsSigningIn(true);
setIsSigningUp(true);
};
const logout = () => {
@ -113,16 +113,23 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(
return (
<>
<Switch>
{/* render sign in page if user is not authenticated and not signing up
* else redirect to my data page as user is authenticated and not signing up
*/}
<Route exact path={ROUTES.HOME}>
{!isAuthenticated && !isSigningIn ? (
{!isAuthenticated && !isSigningUp ? (
<Redirect to={ROUTES.SIGNIN} />
) : (
<Redirect to={ROUTES.MY_DATA} />
)}
</Route>
{!isSigningIn ? (
{/* render the sign in route only if user is not signing up */}
{!isSigningUp ? (
<Route exact component={SignInPage} path={ROUTES.SIGNIN} />
) : null}
{/* callback route to handle the auth flow after user has successfully provided their consent */}
<Route
path={ROUTES.CALLBACK}
render={() => (
@ -135,7 +142,6 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(
}}
onSuccess={(user) => {
setOidcToken(user.id_token);
setIsAuthenticated(true);
onLoginSuccess(user as OidcUser);
}}
/>
@ -143,6 +149,7 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(
)}
/>
{/* silent callback route to handle the silent auth flow */}
<Route
path={ROUTES.SILENT_CALLBACK}
render={() => (
@ -163,15 +170,21 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(
</>
)}
/>
{/* render the children only if user is authenticated */}
{isAuthenticated ? (
<Fragment>{children}</Fragment>
) : !isSigningIn && isEmpty(currentUser) && isEmpty(newUser) ? (
) : // render the sign in page if user is not authenticated and not signing up
!isSigningUp && isEmpty(currentUser) && isEmpty(newUser) ? (
<Redirect to={ROUTES.SIGNIN} />
) : (
// render the authenticator component to handle the auth flow while user is signing in
<AppWithAuth />
)}
</Switch>
{isSigningIn && <Loader fullScreen />}
{/* show loader when application is loading and user is signing up*/}
{isApplicationLoading && isSigningUp && <Loader fullScreen />}
</>
);
}

View File

@ -42,7 +42,6 @@ Object.defineProperty(window, 'localStorage', {
});
const mockUseAuth0 = useAuth0 as jest.Mock;
const mockSetIsAuthenticated = jest.fn();
const mockHandleSuccessfulLogin = jest.fn();
jest.mock('@auth0/auth0-react', () => ({
@ -53,7 +52,6 @@ jest.mock('../../../../hooks/useApplicationStore', () => {
return {
useApplicationStore: jest.fn(() => ({
authConfig: {},
setIsAuthenticated: mockSetIsAuthenticated,
handleSuccessfulLogin: mockHandleSuccessfulLogin,
setOidcToken: jest.fn(),
})),
@ -108,8 +106,6 @@ describe('Test Auth0Callback component', () => {
// eslint-disable-next-line no-undef
await new Promise(process.nextTick);
expect(mockSetIsAuthenticated).toHaveBeenCalledTimes(1);
expect(mockSetIsAuthenticated).toHaveBeenCalledWith(true);
expect(mockHandleSuccessfulLogin).toHaveBeenCalledTimes(1);
expect(mockHandleSuccessfulLogin).toHaveBeenCalledWith({
id_token: 'raw_id_token',

View File

@ -20,13 +20,11 @@ import { OidcUser } from '../../AuthProviders/AuthProvider.interface';
const Auth0Callback: VFC = () => {
const { isAuthenticated, user, getIdTokenClaims, error } = useAuth0();
const { setIsAuthenticated, handleSuccessfulLogin, setOidcToken } =
useApplicationStore();
const { handleSuccessfulLogin, setOidcToken } = useApplicationStore();
if (isAuthenticated) {
getIdTokenClaims()
.then((token) => {
setOidcToken(token?.__raw || '');
setIsAuthenticated(true);
const oidcUser: OidcUser = {
id_token: token?.__raw || '',
scope: '',

View File

@ -52,8 +52,8 @@ export interface IAuthContext {
setIsAuthenticated: (authenticated: boolean) => void;
authConfig?: AuthenticationConfiguration;
authorizerConfig?: AuthorizerConfiguration;
isSigningIn: boolean;
setIsSigningIn: (authenticated: boolean) => void;
isSigningUp: boolean;
setIsSigningUp: (isSigningUp: boolean) => void;
onLoginHandler: () => void;
onLogoutHandler: () => void;
currentUser?: User;

View File

@ -109,16 +109,18 @@ export const AuthProvider = ({
setHelperFunctionsRef,
setCurrentUser,
updateNewUser: setNewUserProfile,
setIsAuthenticated: setIsUserAuthenticated,
setIsAuthenticated,
authConfig,
setAuthConfig,
setAuthorizerConfig,
setIsSigningIn,
setIsSigningUp,
setJwtPrincipalClaims,
removeRefreshToken,
removeOidcToken,
getOidcToken,
getRefreshToken,
isApplicationLoading,
setApplicationLoading,
} = useApplicationStore();
const { activeDomain } = useDomainStore();
@ -127,7 +129,6 @@ export const AuthProvider = ({
const { t } = useTranslation();
const [timeoutId, setTimeoutId] = useState<number>();
const [loading, setLoading] = useState(false);
const [msalInstance, setMsalInstance] = useState<IPublicClientApplication>();
const authenticatorRef = useRef<AuthenticatorRef>(null);
@ -140,7 +141,7 @@ export const AuthProvider = ({
const clientType = authConfig?.clientType ?? ClientType.Public;
const onLoginHandler = () => {
setLoading(true);
setApplicationLoading(true);
authenticatorRef.current?.invokeLogin();
@ -151,7 +152,7 @@ export const AuthProvider = ({
clearTimeout(timeoutId);
authenticatorRef.current?.invokeLogout();
setIsUserAuthenticated(false);
setIsAuthenticated(false);
// reset the user details on logout
setCurrentUser({} as User);
@ -162,7 +163,7 @@ export const AuthProvider = ({
// remove the refresh token on logout
removeRefreshToken();
setLoading(false);
setApplicationLoading(false);
}, [timeoutId]);
const onRenewIdTokenHandler = () => {
@ -191,8 +192,8 @@ export const AuthProvider = ({
const resetUserDetails = (forceLogout = false) => {
setCurrentUser({} as User);
removeOidcToken();
setIsUserAuthenticated(false);
setLoading(false);
setIsAuthenticated(false);
setApplicationLoading(false);
clearTimeout(timeoutId);
if (forceLogout) {
onLogoutHandler();
@ -203,12 +204,12 @@ export const AuthProvider = ({
};
const getLoggedInUserDetails = async () => {
setLoading(true);
setApplicationLoading(true);
try {
const res = await getLoggedInUser({ fields: userAPIQueryFields });
if (res) {
setCurrentUser(res);
setIsUserAuthenticated(true);
setIsAuthenticated(true);
} else {
resetUserDetails();
}
@ -224,7 +225,7 @@ export const AuthProvider = ({
);
}
} finally {
setLoading(false);
setApplicationLoading(false);
}
};
@ -357,15 +358,15 @@ export const AuthProvider = ({
}, [timeoutId]);
const handleFailedLogin = () => {
setIsSigningIn(false);
setIsUserAuthenticated(false);
setLoading(false);
setIsSigningUp(false);
setIsAuthenticated(false);
setApplicationLoading(false);
history.push(ROUTES.SIGNIN);
};
const handleSuccessfulLogin = async (user: OidcUser) => {
setLoading(true);
setIsUserAuthenticated(true);
setApplicationLoading(true);
setIsAuthenticated(true);
const fields =
authConfig?.provider === AuthProviderEnum.Basic
? userAPIQueryFields + ',' + isEmailVerifyField
@ -389,7 +390,7 @@ export const AuthProvider = ({
if (err && err.response && err.response.status === 404) {
setNewUserProfile(user.profile);
setCurrentUser({} as User);
setIsSigningIn(true);
setIsSigningUp(true);
history.push(ROUTES.SIGNUP);
} else {
// eslint-disable-next-line no-console
@ -397,7 +398,7 @@ export const AuthProvider = ({
history.push(ROUTES.SIGNIN);
}
} finally {
setLoading(false);
setApplicationLoading(false);
}
};
@ -548,7 +549,7 @@ export const AuthProvider = ({
updateAuthInstance(configJson);
if (!getOidcToken()) {
handleStoreProtectedRedirectPath();
setLoading(false);
setApplicationLoading(false);
} else {
if (location.pathname !== ROUTES.AUTH_CALLBACK) {
getLoggedInUserDetails();
@ -556,7 +557,7 @@ export const AuthProvider = ({
}
} else {
// provider is either null or not supported
setLoading(false);
setApplicationLoading(false);
showErrorToast(
t('message.configured-sso-provider-is-not-supported', {
provider: authConfig?.provider,
@ -564,11 +565,11 @@ export const AuthProvider = ({
);
}
} else {
setLoading(false);
setApplicationLoading(false);
showErrorToast(t('message.auth-configuration-missing'));
}
} catch (error) {
setLoading(false);
setApplicationLoading(false);
showErrorToast(
error as AxiosError,
t('server.entity-fetch-error', {
@ -580,7 +581,11 @@ export const AuthProvider = ({
const getProtectedApp = () => {
// Show loader if application in loading state
const childElement = loading ? <Loader fullScreen /> : children;
const childElement = isApplicationLoading ? (
<Loader fullScreen />
) : (
children
);
if (clientType === ClientType.Confidential) {
return (
@ -691,11 +696,11 @@ export const AuthProvider = ({
return cleanup;
}, []);
const isLoading =
const isConfigLoading =
!authConfig ||
(authConfig.provider === AuthProviderEnum.Azure && !msalInstance);
return <>{isLoading ? <Loader fullScreen /> : getProtectedApp()}</>;
return <>{isConfigLoading ? <Loader fullScreen /> : getProtectedApp()}</>;
};
export default AuthProvider;

View File

@ -31,8 +31,7 @@ export const OktaAuthProvider: FunctionComponent<Props> = ({
children,
onLoginSuccess,
}: Props) => {
const { authConfig, setIsAuthenticated, setOidcToken } =
useApplicationStore();
const { authConfig, setOidcToken } = useApplicationStore();
const { clientId, issuer, redirectUri, scopes, pkce } =
authConfig as OktaAuthOptions;
@ -63,7 +62,6 @@ export const OktaAuthProvider: FunctionComponent<Props> = ({
_oktaAuth
.getUser()
.then((info) => {
setIsAuthenticated(true);
const user = {
id_token: idToken,
scope: scopes,

View File

@ -31,6 +31,7 @@ export const OM_SESSION_KEY = 'om-session';
export const useApplicationStore = create<ApplicationStore>()(
persist(
(set, get) => ({
isApplicationLoading: false,
theme: getThemeConfig(),
applicationConfig: {
customTheme: getThemeConfig(),
@ -40,7 +41,7 @@ export const useApplicationStore = create<ApplicationStore>()(
isAuthenticated: Boolean(getOidcToken()),
authConfig: undefined,
authorizerConfig: undefined,
isSigningIn: false,
isSigningUp: false,
jwtPrincipalClaims: [],
userProfilePics: {},
cachedEntityData: {},
@ -77,8 +78,12 @@ export const useApplicationStore = create<ApplicationStore>()(
setIsAuthenticated: (authenticated: boolean) => {
set({ isAuthenticated: authenticated });
},
setIsSigningIn: (signingIn: boolean) => {
set({ isSigningIn: signingIn });
setIsSigningUp: (signingUp: boolean) => {
set({ isSigningUp: signingUp });
},
setApplicationLoading: (loading: boolean) => {
set({ isApplicationLoading: loading });
},
onLoginHandler: () => {

View File

@ -42,6 +42,8 @@ export interface ApplicationStore
extends IAuthContext,
LogoConfiguration,
LoginConfiguration {
isApplicationLoading: boolean;
setApplicationLoading: (loading: boolean) => void;
userProfilePics: Record<string, User>;
cachedEntityData: Record<string, EntityUnion>;
selectedPersona: EntityReference;

View File

@ -33,7 +33,7 @@ jest.mock('react-router-dom', () => ({
jest.mock('../../hooks/useApplicationStore', () => ({
useApplicationStore: jest.fn(() => ({
setIsSigningIn: jest.fn(),
setIsSigningUp: jest.fn(),
newUser: {
name: '',
email: '',

View File

@ -41,7 +41,7 @@ const SignUp = () => {
const { t } = useTranslation();
const history = useHistory();
const {
setIsSigningIn,
setIsSigningUp,
jwtPrincipalClaims = [],
authorizerConfig,
updateCurrentUser,
@ -67,7 +67,7 @@ const SignUp = () => {
if (urlPathname) {
setUrlPathnameExpiryAfterRoute(urlPathname);
}
setIsSigningIn(false);
setIsSigningUp(false);
history.push(ROUTES.HOME);
} catch (error) {
showErrorToast(