diff --git a/README.md b/README.md index 9375a8333..8e34fdd33 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,7 @@ The PlatformRoute model has several useful options: | `authRequired?: boolean` | Requiring authentication for a route means that users who are not logged in will be redirected to the Login Form when they try to access the route. | | `route: string` | The route property is the path to the route, relative to its parent(s). | | `title: string` | The title property is the text that will appear in the Tools or Utils Selectors (this is irrelevant on hidden routes). | +| `rolesRequired: Array` | Requiring roles for a route means that users who do not own the roles will be presented with restricted page when they try to access the route. | ## Git diff --git a/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts b/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts index e70a17e06..7e5fb0aff 100644 --- a/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts +++ b/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts @@ -14,5 +14,7 @@ export function create(profile: UserProfile, token: TokenModel): UserProfile { // rolees. profile.isCustomer = !!token.roles?.some(role => role === UserRole.customer) profile.isMember = !profile.isCustomer + // store roles for custom capability checks + profile.roles = token.roles return profile } diff --git a/src-ts/lib/profile-provider/profile-functions/profile-factory/user-role.enum.ts b/src-ts/lib/profile-provider/profile-functions/profile-factory/user-role.enum.ts index a3c7abb05..f56a29f43 100644 --- a/src-ts/lib/profile-provider/profile-functions/profile-factory/user-role.enum.ts +++ b/src-ts/lib/profile-provider/profile-functions/profile-factory/user-role.enum.ts @@ -1,4 +1,5 @@ export enum UserRole { + gamificationAdmin = 'Gamification Admin', customer = 'Self-Service Customer', member = 'Topcoder User', } diff --git a/src-ts/lib/profile-provider/user-profile.model.ts b/src-ts/lib/profile-provider/user-profile.model.ts index 54c66ed49..51ae0c2e1 100644 --- a/src-ts/lib/profile-provider/user-profile.model.ts +++ b/src-ts/lib/profile-provider/user-profile.model.ts @@ -10,6 +10,7 @@ export interface UserProfile { isMember?: boolean lastName: string photoURL?: string + roles: Array status: string updatedAt: number userId: number diff --git a/src-ts/lib/restricted-page/RestrictedPage.module.scss b/src-ts/lib/restricted-page/RestrictedPage.module.scss new file mode 100644 index 000000000..8b7f47684 --- /dev/null +++ b/src-ts/lib/restricted-page/RestrictedPage.module.scss @@ -0,0 +1,27 @@ +.contentLayout { + width: 100%; + padding-bottom: 0; + + .contentLayout-outer { + width: 100%; + + .contentLayout-inner { + width: 100%; + overflow: visible; + } + } +} + +.container { + display: flex; + padding-top: 26px; + + a { + color: #0D61BF; + + &:hover { + color: #0D61BF; + text-decoration: underline; + } + } +} diff --git a/src-ts/lib/restricted-page/RestrictedPage.tsx b/src-ts/lib/restricted-page/RestrictedPage.tsx new file mode 100644 index 000000000..91d6a8db9 --- /dev/null +++ b/src-ts/lib/restricted-page/RestrictedPage.tsx @@ -0,0 +1,17 @@ +import { ReactElement } from 'react' + +import { ContentLayout } from '..' + +import styles from './RestrictedPage.module.scss' + +export const RestrictedPage: ReactElement = + +
+

Unfortunately, you are not permitted to access the site. If you feel you should be able to, please contact us.

+
+
diff --git a/src-ts/lib/restricted-page/index.ts b/src-ts/lib/restricted-page/index.ts new file mode 100644 index 000000000..fa77e8bc3 --- /dev/null +++ b/src-ts/lib/restricted-page/index.ts @@ -0,0 +1 @@ +export { RestrictedPage } from './RestrictedPage' diff --git a/src-ts/lib/route-provider/platform-route.model.ts b/src-ts/lib/route-provider/platform-route.model.ts index 71d1ba775..558ddf185 100644 --- a/src-ts/lib/route-provider/platform-route.model.ts +++ b/src-ts/lib/route-provider/platform-route.model.ts @@ -7,6 +7,7 @@ export interface PlatformRoute { element: JSX.Element hidden?: boolean memberOnly?: boolean + rolesRequired?: Array route: string title?: string } diff --git a/src-ts/lib/route-provider/require-auth-provider/require-auth.provider.tsx b/src-ts/lib/route-provider/require-auth-provider/require-auth.provider.tsx index e6930755c..4b6fa5e19 100644 --- a/src-ts/lib/route-provider/require-auth-provider/require-auth.provider.tsx +++ b/src-ts/lib/route-provider/require-auth-provider/require-auth.provider.tsx @@ -1,10 +1,12 @@ import { useContext } from 'react' import { profileContext, ProfileContextData } from '../../profile-provider' +import { RestrictedPage } from '../../restricted-page' interface RequireAuthProviderProps { children: JSX.Element loginUrl: string + rolesRequired?: Array } function RequireAuthProvider(props: RequireAuthProviderProps): JSX.Element { @@ -12,11 +14,28 @@ function RequireAuthProvider(props: RequireAuthProviderProps): JSX.Element { const profileContextData: ProfileContextData = useContext(profileContext) const { profile, initialized }: ProfileContextData = profileContextData - // if we have a profile or we're not initialized yet, just return the children - if (!initialized || !!profile) { + // if we're not initialized yet, just return the children + if (!initialized) { return props.children } + // if we have a profile and `rolesRequired` is configured for the route + // check the user's roles, allow access or show restricted page + if (!!profile) { + if (props.rolesRequired) { + if (!profile.roles) { + return RestrictedPage + } + // if the profile doesn't include all the required roles, show the restricted page + if (props.rolesRequired.some(role => !profile.roles.includes(role))) { + return RestrictedPage + } + return props.children + } else { + return props.children + } + } + // redirect to the login page window.location.href = props.loginUrl return <> diff --git a/src-ts/lib/route-provider/route.provider.tsx b/src-ts/lib/route-provider/route.provider.tsx index 0d01ea973..afffcd83c 100644 --- a/src-ts/lib/route-provider/route.provider.tsx +++ b/src-ts/lib/route-provider/route.provider.tsx @@ -124,7 +124,7 @@ export const RouteProvider: FC = (props: RouteProviderProps) const routeElement: JSX.Element = !route.authRequired ? route.element : ( - + {route.element} ) diff --git a/src-ts/tools/gamification-admin/GamificationAdmin.tsx b/src-ts/tools/gamification-admin/GamificationAdmin.tsx index e5d408058..860b2c3ae 100644 --- a/src-ts/tools/gamification-admin/GamificationAdmin.tsx +++ b/src-ts/tools/gamification-admin/GamificationAdmin.tsx @@ -7,7 +7,6 @@ import { } from '../../lib' export const toolTitle: string = 'Gamification Admin' -export const baseUrl: string = '/gamification-admin' const GamificationAdmin: FC<{}> = () => { diff --git a/src-ts/tools/gamification-admin/gamification-admin.routes.tsx b/src-ts/tools/gamification-admin/gamification-admin.routes.tsx index 39fa50119..b5ed435d1 100644 --- a/src-ts/tools/gamification-admin/gamification-admin.routes.tsx +++ b/src-ts/tools/gamification-admin/gamification-admin.routes.tsx @@ -1,10 +1,14 @@ import { PlatformRoute } from '../../lib' +import { UserRole } from '../../lib/profile-provider/profile-functions/profile-factory/user-role.enum' -import GamificationAdmin, { baseUrl, toolTitle } from './GamificationAdmin' +import GamificationAdmin, { toolTitle } from './GamificationAdmin' import BadgeDetailPage from './pages/badge-detail/BadgeDetailPage' import BadgeListingPage from './pages/badge-listing/BadgeListingPage' import CreateBadgePage from './pages/create-badge/CreateBadgePage' +export const baseUrl: string = '/gamification-admin' +export const rolesRequired: Array = [UserRole.gamificationAdmin] + export const gamificationAdminRoutes: Array = [ { authRequired: true, @@ -24,6 +28,7 @@ export const gamificationAdminRoutes: Array = [ ], element: , hidden: true, + rolesRequired, route: baseUrl, title: toolTitle, }, diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx index 11ad903d3..543b3be27 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx +++ b/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx @@ -2,7 +2,8 @@ import { FC, useMemo } from 'react' import { Breadcrumb, ContentLayout } from '../../../../lib' import { BreadcrumbItemModel } from '../../../../lib/breadcrumb/breadcrumb-item/breadcrumb-item.model' -import { baseUrl, toolTitle } from '../../GamificationAdmin' +import { baseUrl } from '../../gamification-admin.routes' +import { toolTitle } from '../../GamificationAdmin' import styles from './BadgeDetailPage.module.scss' diff --git a/src-ts/tools/gamification-admin/pages/create-badge/CreateBadgePage.tsx b/src-ts/tools/gamification-admin/pages/create-badge/CreateBadgePage.tsx index 9dc33c6de..c6bc16dd9 100644 --- a/src-ts/tools/gamification-admin/pages/create-badge/CreateBadgePage.tsx +++ b/src-ts/tools/gamification-admin/pages/create-badge/CreateBadgePage.tsx @@ -2,7 +2,8 @@ import { FC, useMemo } from 'react' import { Breadcrumb, ContentLayout } from '../../../../lib' import { BreadcrumbItemModel } from '../../../../lib/breadcrumb/breadcrumb-item/breadcrumb-item.model' -import { baseUrl, toolTitle } from '../../GamificationAdmin' +import { baseUrl } from '../../gamification-admin.routes' +import { toolTitle } from '../../GamificationAdmin' import styles from './CreateBadgePage.module.scss'