Skip to content

Commit

Permalink
feat: ORV2-1749 Tighten up user navigation in frontend (#1006)
Browse files Browse the repository at this point in the history
  • Loading branch information
krishnan-aot authored Jan 10, 2024
1 parent ef3808b commit 41cd734
Show file tree
Hide file tree
Showing 19 changed files with 502 additions and 144 deletions.
11 changes: 8 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ import OnRouteBCContext, {
BCeIDUserDetailContext,
IDIRUserDetailContext,
} from "./common/authentication/OnRouteBCContext";
import { MigratedClient } from "./common/authentication/types";
import { MigratedClient, UserRolesType } from "./common/authentication/types";

const authority =
import.meta.env.VITE_KEYCLOAK_ISSUER_URL || envConfig.VITE_KEYCLOAK_ISSUER_URL;
import.meta.env.VITE_KEYCLOAK_ISSUER_URL ||
envConfig.VITE_KEYCLOAK_ISSUER_URL;

const client_id =
import.meta.env.VITE_KEYCLOAK_AUDIENCE || envConfig.VITE_KEYCLOAK_AUDIENCE;
Expand Down Expand Up @@ -60,7 +61,7 @@ const App = () => {
alertType: "info",
});

const [userRoles, setUserRoles] = useState<Nullable<string[]>>();
const [userRoles, setUserRoles] = useState<Nullable<UserRolesType[]>>();
const [companyId, setCompanyId] = useState<Optional<number>>();
const [onRouteBCClientNumber, setOnRouteBCClientNumber] =
useState<Optional<string>>();
Expand All @@ -71,6 +72,7 @@ const App = () => {
useState<Optional<IDIRUserDetailContext>>();
const [migratedClient, setMigratedClient] =
useState<Optional<MigratedClient>>();
const [isNewBCeIDUser, setIsNewBCeIDUser] = useState<Optional<boolean>>();

// Needed the following usestate and useffect code so that the snackbar would disapear/close
const [displaySnackBar, setDisplaySnackBar] = useState(false);
Expand Down Expand Up @@ -99,6 +101,8 @@ const App = () => {
setOnRouteBCClientNumber,
migratedClient,
setMigratedClient,
isNewBCeIDUser,
setIsNewBCeIDUser,
};
}, [
userRoles,
Expand All @@ -108,6 +112,7 @@ const App = () => {
idirUserDetails,
onRouteBCClientNumber,
migratedClient,
isNewBCeIDUser,
])}
>
<SnackBarContext.Provider
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/common/authentication/OnRouteBCContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Dispatch, SetStateAction } from "react";
import { MigratedClient } from "./types";
import { IDIRUserAuthGroupType, MigratedClient, UserRolesType } from "./types";

import { Nullable, Optional } from "../types/common";

Expand All @@ -11,7 +11,7 @@ export type IDIRUserDetailContext = {
lastName: string;
userName: string;
email: string;
userAuthGroup: string;
userAuthGroup: IDIRUserAuthGroupType;
};

/**
Expand All @@ -34,8 +34,8 @@ export interface BCeIDUserDetailContext {
* The data and functions to in the react context.
*/
export type OnRouteBCContextType = {
setUserRoles?: Dispatch<SetStateAction<Nullable<string[]>>>;
userRoles?: Nullable<string[]>;
setUserRoles?: Dispatch<SetStateAction<Nullable<UserRolesType[]>>>;
userRoles?: Nullable<UserRolesType[]>;
setOnRouteBCClientNumber?: Dispatch<SetStateAction<Optional<string>>>;
onRouteBCClientNumber?: string;
setUserDetails?: Dispatch<SetStateAction<Optional<BCeIDUserDetailContext>>>;
Expand All @@ -50,6 +50,8 @@ export type OnRouteBCContextType = {
>;
setMigratedClient?: Dispatch<SetStateAction<Optional<MigratedClient>>>;
migratedClient?: MigratedClient;
isNewBCeIDUser?: boolean;
setIsNewBCeIDUser?: Dispatch<SetStateAction<Optional<boolean>>>;
};

const defaultBehaviour: OnRouteBCContextType = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
import { useAuth } from "react-oidc-context";
import { useLocation, Navigate, Outlet, useNavigate } from "react-router-dom";
import { useContext, useEffect } from "react";
import { useAuth } from "react-oidc-context";
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";

import { HOME, ERROR_ROUTES } from "./constants";
import { Loading } from "../common/pages/Loading";
import OnRouteBCContext from "../common/authentication/OnRouteBCContext";
import { DoesUserHaveRole } from "../common/authentication/util";
import { LoadBCeIDUserRolesByCompany } from "../common/authentication/LoadBCeIDUserRolesByCompany";
import { LoadBCeIDUserContext } from "../common/authentication/LoadBCeIDUserContext";
import { LoadIDIRUserContext } from "../common/authentication/LoadIDIRUserContext";
import { LoadIDIRUserRoles } from "../common/authentication/LoadIDIRUserRoles";
import { IDPS } from "../common/types/idp";
import { ERROR_ROUTES, HOME } from "../../../routes/constants";
import { Loading } from "../../pages/Loading";
import { IDPS } from "../../types/idp";
import { LoadBCeIDUserContext } from "../LoadBCeIDUserContext";
import { LoadBCeIDUserRolesByCompany } from "../LoadBCeIDUserRolesByCompany";
import OnRouteBCContext from "../OnRouteBCContext";
import { UserRolesType } from "../types";
import { DoesUserHaveRole } from "../util";

const isIDIR = (identityProvider: string) => identityProvider === IDPS.IDIR;

export const ProtectedRoutes = ({
export const BCeIDAuthWall = ({
requiredRole,
}: {
requiredRole?: string;
requiredRole?: UserRolesType;
}) => {
const {
isAuthenticated,
isLoading: isAuthLoading,
user: userFromToken,
} = useAuth();

const { userRoles, companyId, idirUserDetails } =
useContext(OnRouteBCContext);
const { userRoles, companyId, isNewBCeIDUser } = useContext(OnRouteBCContext);

const userIDP = userFromToken?.profile?.identity_provider as string;

Expand All @@ -47,24 +45,23 @@ export const ProtectedRoutes = ({
return <Loading />;
}

if (isAuthenticated) {
if (isAuthenticated && !isNewBCeIDUser) {
if (isIDIR(userIDP)) {
if (!idirUserDetails?.userAuthGroup) {
return (
<>
<LoadIDIRUserContext />
<Loading />
</>
);
}
if (!userRoles) {
return (
<>
<LoadIDIRUserRoles />
<Loading />
</>
);
}
/**
* This is a placeholder to navigate an IDIR user to an unauthorized page
* since a companyId is necessary for them to do anything and
* that feature is yet to be built.
*
* Once we set up idir user acting as a company, there will be appropriate
* handlers here.
*/
return (
<Navigate
to={ERROR_ROUTES.UNEXPECTED}
state={{ from: location }}
replace
/>
);
}
if (!isIDIR(userIDP)) {
if (!companyId) {
Expand Down Expand Up @@ -99,3 +96,5 @@ export const ProtectedRoutes = ({
return <Navigate to={HOME} state={{ from: location }} replace />;
}
};

BCeIDAuthWall.displayName = "BCeIDAuthWall";
110 changes: 110 additions & 0 deletions frontend/src/common/authentication/auth-walls/IDIRAuthWall.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useContext, useEffect } from "react";
import { useAuth } from "react-oidc-context";
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";

import { LoadIDIRUserContext } from "../LoadIDIRUserContext";
import { LoadIDIRUserRoles } from "../LoadIDIRUserRoles";
import OnRouteBCContext from "../OnRouteBCContext";
import { IDIRUserAuthGroupType, UserRolesType } from "../types";
import { DoesUserHaveAuthGroup, DoesUserHaveRole } from "../util";
import { Loading } from "../../pages/Loading";
import { IDPS } from "../../types/idp";
import { ERROR_ROUTES, HOME } from "../../../routes/constants";

const isIDIR = (identityProvider: string) => identityProvider === IDPS.IDIR;

/**
* This component ensures that a page is only available to IDIR users
* with necessary roles and auth groups (as applicable).
*
*/
export const IDIRAuthWall = ({
requiredRole,
allowedAuthGroups,
}: {
requiredRole?: UserRolesType;
/**
* The collection of auth groups allowed to have access to a page or action.
* IDIR System Admin is assumed to be allowed regardless of it being passed.
* If not provided, only a System Admin will be allowed to access.
*/
allowedAuthGroups?: IDIRUserAuthGroupType[];
}) => {
const {
isAuthenticated,
isLoading: isAuthLoading,
user: userFromToken,
} = useAuth();

const { userRoles, idirUserDetails } = useContext(OnRouteBCContext);

const userIDP = userFromToken?.profile?.identity_provider as string;

const location = useLocation();
const navigate = useNavigate();

useEffect(() => {
if (!isAuthLoading && !isAuthenticated) {
/**
* Redirect the user back to login page if they are trying to directly access
* a protected page but are unauthenticated.
*/
navigate(HOME);
}
}, [isAuthLoading, isAuthenticated]);

if (isAuthLoading) {
return <Loading />;
}

if (isAuthenticated) {
if (isIDIR(userIDP)) {
if (!idirUserDetails?.userAuthGroup) {
return (
<>
<LoadIDIRUserContext />
<Loading />
</>
);
}
if (!userRoles) {
return (
<>
<LoadIDIRUserRoles />
<Loading />
</>
);
}
} else {
return (
<Navigate
to={ERROR_ROUTES.UNAUTHORIZED}
state={{ from: location }}
replace
/>
);
}

const doesUserHaveAccess =
DoesUserHaveAuthGroup<IDIRUserAuthGroupType>({
userAuthGroup: idirUserDetails?.userAuthGroup,
allowedAuthGroups,
}) && DoesUserHaveRole(userRoles, requiredRole);

if (doesUserHaveAccess) {
return <Outlet />;
}
// The user does not have access. They should be disallowed.
return (
<Navigate
to={ERROR_ROUTES.UNAUTHORIZED}
state={{ from: location }}
replace
/>
);
} else {
return <Navigate to={HOME} state={{ from: location }} replace />;
}
};

IDIRAuthWall.displayName = "IDIRAuthWall";
85 changes: 85 additions & 0 deletions frontend/src/common/authentication/auth-walls/NewBCeIDAuthWall.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useContext } from "react";
import { useAuth } from "react-oidc-context";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import {
ERROR_ROUTES,
HOME
} from "../../../routes/constants";
import { Loading } from "../../pages/Loading";
import { IDPS } from "../../types/idp";
import { LoadBCeIDUserContext } from "../LoadBCeIDUserContext";
import OnRouteBCContext from "../OnRouteBCContext";

const isBCeID = (identityProvider: string) => identityProvider === IDPS.BCEID;

/**
* This component ensures that a page is only available to new BCeID users.
*/
export const NewBCeIDAuthWall = () => {
const {
isAuthenticated,
isLoading: isAuthLoading,
user: userFromToken,
} = useAuth();

const { isNewBCeIDUser, companyId, userDetails } =
useContext(OnRouteBCContext);

const userIDP = userFromToken?.profile?.identity_provider as string;
const location = useLocation();

if (isAuthLoading) {
return <Loading />;
}

if (isAuthenticated) {
if (isBCeID(userIDP)) {
// Condition to check if the user context must be loaded.
if (
!companyId &&
isNewBCeIDUser === undefined &&
!userDetails?.userAuthGroup
) {
return (
<>
<LoadBCeIDUserContext />
<Loading />
</>
);
}
if (isNewBCeIDUser) {
// The user is now authenticated and confirmed to be a new user
return <Outlet />;
}
/**
* Implementation Note:
* There is no need to load up the user roles.
* User context is sufficient enough to determine whether the user
* already has a profile in the system.
*/
} else {
/**
* This is a placeholder to navigate an IDIR user to the unexpected error page
* since a companyId is necessary for them to do anything and
* that feature is yet to be built.
*
* Once we set up idir user acting as a company, there will be appropriate
* handlers here.
*/
return (
<Navigate
to={ERROR_ROUTES.UNEXPECTED}
state={{ from: location }}
replace
/>
);
}
}
/**
* The user is either a) unauthenticated or b) set up their profile already.
* Redirect them to home page either way.
*/
return <Navigate to={HOME} state={{ from: location }} replace />;
};

NewBCeIDAuthWall.displayName = "NewBCeIDAuthWall";
Loading

0 comments on commit 41cd734

Please sign in to comment.