From 318700778f899d181711d1ee6a017ce3fe6936b0 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Tue, 13 Oct 2026 15:23:01 +1100 Subject: [PATCH] fix: show app drawer for administrators (PM-6555) --- src/README.md | 15 +++++++++++ src/lib/config/auth.ts | 5 ++-- src/lib/functions/user-profile.provider.ts | 12 +++++++-- tests/tool-selector-nav-items.test.mjs | 26 ++++++++++++++++--- types/src/lib/config/auth.d.ts | 1 + .../lib/functions/user-profile.provider.d.ts | 7 +++++ 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/README.md b/src/README.md index b8a30e3e..77f9959a 100644 --- a/src/README.md +++ b/src/README.md @@ -11,6 +11,7 @@ For information re how to integrate the UniNav into a web property, see the main - [Serve the local app](#serve-the-local-app) - [Demo Apps](#demo-apps) - [Instructions for using the Demo Apps](#instructions-for-using-the-demo-apps) +- [App Drawer Roles](#app-drawer-roles) - [Production Distribution](#production-distribution) - [Recommended IDE Setup](#recommended-ide-setup) - [Typescript Type Declaration Generation](#typescript-type-declaration-generation) @@ -82,6 +83,20 @@ There are 2 sample integrations in the [/demo](./demo/) folder: - Marketing Nav: http://local.topcoder-dev.com:8081 - Tool Nav: http://local.topcoder-dev.com:8082 +## App Drawer Roles + +The signed-in user's app drawer is filtered using the roles in the `tcjwt` +cookie by `getUserAppRoles` and the guards in `src/lib/config/auth.ts`. +`Topcoder Staff` and `administrator` users see both Talent and Staff sections, +including administrators who also have `Topcoder Customer` or `Topcoder Talent`. +The administrator role is matched case-insensitively, so legacy `Administrator` +claims receive the same navigation. Other role names retain their exact matching. + +Members with `Topcoder Talent`, or without a recognized navigation role, see +Talent only. Customer-only users have no drawer items, so the drawer trigger is +hidden. Application-specific roles such as `Payment Admin` do not grant the Staff +section. Each linked application still enforces its own access permissions. + ## Production Distribution Run `npm run build` to build the files for production. The output is to be found in the `dist` folder. diff --git a/src/lib/config/auth.ts b/src/lib/config/auth.ts index 4c9c1d56..d8327032 100644 --- a/src/lib/config/auth.ts +++ b/src/lib/config/auth.ts @@ -2,14 +2,15 @@ export enum AUTH_USER_ROLE { talent = 'Topcoder Talent', customer = 'Topcoder Customer', staff = 'Topcoder Staff', + administrator = 'administrator', noRole = 'no-role', } export const AUTH_USER_ROLE_VALUES = Object.values(AUTH_USER_ROLE); -export const AUTH_TALENT_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.talent, AUTH_USER_ROLE.staff, AUTH_USER_ROLE.noRole]; +export const AUTH_TALENT_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.talent, AUTH_USER_ROLE.staff, AUTH_USER_ROLE.administrator, AUTH_USER_ROLE.noRole]; export const AUTH_CUSTOMER_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.customer, AUTH_USER_ROLE.staff, AUTH_USER_ROLE.noRole]; -export const AUTH_STAFF_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.staff]; +export const AUTH_STAFF_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.staff, AUTH_USER_ROLE.administrator]; export const TALENT_ROUTE_GUARD = { auth: { diff --git a/src/lib/functions/user-profile.provider.ts b/src/lib/functions/user-profile.provider.ts index 6c6d0732..59532710 100644 --- a/src/lib/functions/user-profile.provider.ts +++ b/src/lib/functions/user-profile.provider.ts @@ -18,9 +18,17 @@ export const getJwtUserRoles = (): AuthUser['roles'] | undefined => { return getAuthJwtDomainProp('roles') } -// get the user roles that match the config/auth's user roles definitions +/** + * Reads the current tcjwt cookie and returns the roles recognized by navigation guards. + * Administrator roles are matched case-insensitively and returned in their canonical form. + * Takes no parameters; unrelated roles are omitted from the returned array. + * @returns Recognized navigation roles, or an empty array when no roles are present. + * @throws If the tcjwt cookie cannot be decoded or contains invalid JSON. + */ export const getUserAppRoles = (): AuthUser['roles'] => { - return (getJwtUserRoles() ?? []).filter(d => AUTH_USER_ROLE_VALUES.includes(d as AUTH_USER_ROLE)) + return (getJwtUserRoles() ?? []) + .map(role => role.toLowerCase() === AUTH_USER_ROLE.administrator ? AUTH_USER_ROLE.administrator : role) + .filter(role => AUTH_USER_ROLE_VALUES.includes(role as AUTH_USER_ROLE)) } export const checkUserAppRole = (role: AUTH_USER_ROLE): boolean => ( diff --git a/tests/tool-selector-nav-items.test.mjs b/tests/tool-selector-nav-items.test.mjs index 4b1bc7c6..80eca69d 100644 --- a/tests/tool-selector-nav-items.test.mjs +++ b/tests/tool-selector-nav-items.test.mjs @@ -75,16 +75,34 @@ test('staff see the Talent and Staff sections laid out as in the design', () => assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT, STAFF]); }); +for (const roles of [ + ['administrator'], + ['Topcoder User', 'administrator', 'Topcoder Customer'], + ['administrator', 'Topcoder Talent'], + ['Administrator', 'Topcoder Customer'], +]) { + test(`administrators see Talent and Staff with roles: ${roles.join(', ')}`, () => { + signIn(roles); + assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT, STAFF]); + }); +} + test('members only see the Talent section', () => { - for (const roles of [['Topcoder User'], ['Topcoder User', 'Topcoder Talent']]) { + for (const roles of [[], ['Topcoder User'], ['Topcoder User', 'Topcoder Talent'], ['Payment Admin']]) { signIn(roles); assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT]); } }); -test('customers without talent or staff roles get no drawer items', () => { - signIn(['Topcoder Customer']); - assert.deepEqual(getToolSelectorItems(), []); +test('customers without talent, staff or administrator roles get no drawer items', () => { + for (const roles of [ + ['Topcoder Customer'], + ['Topcoder Customer', 'Payment Admin'], + ['Topcoder Customer', 'not-administrator'], + ]) { + signIn(roles); + assert.deepEqual(getToolSelectorItems(), []); + } }); test('every drawer tool has a description, a sprite icon and its production url', async () => { diff --git a/types/src/lib/config/auth.d.ts b/types/src/lib/config/auth.d.ts index fe692176..d6e54c47 100644 --- a/types/src/lib/config/auth.d.ts +++ b/types/src/lib/config/auth.d.ts @@ -2,6 +2,7 @@ export declare enum AUTH_USER_ROLE { talent = "Topcoder Talent", customer = "Topcoder Customer", staff = "Topcoder Staff", + administrator = "administrator", noRole = "no-role" } export declare const AUTH_USER_ROLE_VALUES: AUTH_USER_ROLE[]; diff --git a/types/src/lib/functions/user-profile.provider.d.ts b/types/src/lib/functions/user-profile.provider.d.ts index b5898c65..c972eb5e 100644 --- a/types/src/lib/functions/user-profile.provider.d.ts +++ b/types/src/lib/functions/user-profile.provider.d.ts @@ -3,6 +3,13 @@ import { AUTH_USER_ROLE } from '../config/auth'; export type fetchUserProfileFn = () => AuthUser | null; export declare const getJwtUserhandle: () => AuthUser["handle"] | undefined; export declare const getJwtUserRoles: () => AuthUser["roles"] | undefined; +/** + * Reads the current tcjwt cookie and returns the roles recognized by navigation guards. + * Administrator roles are matched case-insensitively and returned in their canonical form. + * Takes no parameters; unrelated roles are omitted from the returned array. + * @returns Recognized navigation roles, or an empty array when no roles are present. + * @throws If the tcjwt cookie cannot be decoded or contains invalid JSON. + */ export declare const getUserAppRoles: () => AuthUser["roles"]; export declare const checkUserAppRole: (role: AUTH_USER_ROLE) => boolean; /**