Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions src/lib/config/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
12 changes: 10 additions & 2 deletions src/lib/functions/user-profile.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,17 @@ export const getJwtUserRoles = (): AuthUser['roles'] | undefined => {
return getAuthJwtDomainProp<AuthUser['roles'] | undefined>('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 => (
Expand Down
26 changes: 22 additions & 4 deletions tests/tool-selector-nav-items.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
1 change: 1 addition & 0 deletions types/src/lib/config/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
7 changes: 7 additions & 0 deletions types/src/lib/functions/user-profile.provider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
Loading