diff --git a/site.config.ci.tsx b/site.config.ci.tsx
index 577e871b..1a065205 100644
--- a/site.config.ci.tsx
+++ b/site.config.ci.tsx
@@ -13,7 +13,6 @@ const siteConfig: SiteConfig = {
logoutUrl: 'http://localhost:8000/logout',
environment: EnvironmentTypes.PRODUCTION,
- basename: '/admin-console',
apps: [
shellApp,
headerApp,
diff --git a/site.config.dev.tsx b/site.config.dev.tsx
index aae20a78..e90dcf30 100644
--- a/site.config.dev.tsx
+++ b/site.config.dev.tsx
@@ -15,7 +15,6 @@ const siteConfig: SiteConfig = {
logoutUrl: 'http://local.openedx.io:8000/logout',
environment: EnvironmentTypes.DEVELOPMENT,
- basename: '/admin-console',
apps: [
shellApp,
headerApp,
diff --git a/site.config.test.tsx b/site.config.test.tsx
index db7698ed..6fec5a0f 100644
--- a/site.config.test.tsx
+++ b/site.config.test.tsx
@@ -12,7 +12,6 @@ const siteConfig: SiteConfig = {
// Use 'test' instead of EnvironmentTypes.TEST to break a circular dependency
// when mocking `@openedx/frontend-base` itself.
environment: 'test' as SiteConfig['environment'],
- basename: '/admin-console',
apps: [{
appId,
config: {},
diff --git a/src/authz-module/audit-user/index.test.tsx b/src/authz-module/audit-user/index.test.tsx
index e83bcb1b..4643f2fc 100644
--- a/src/authz-module/audit-user/index.test.tsx
+++ b/src/authz-module/audit-user/index.test.tsx
@@ -93,7 +93,7 @@ const renderWithRouter = (route = '/audit/johndoe') => {
} />
- Home Page} />
+ Home Page} />
diff --git a/src/authz-module/components/AddRoleButton.test.tsx b/src/authz-module/components/AddRoleButton.test.tsx
index 048a1735..79a976e8 100644
--- a/src/authz-module/components/AddRoleButton.test.tsx
+++ b/src/authz-module/components/AddRoleButton.test.tsx
@@ -74,7 +74,7 @@ describe('AddRoleButton', () => {
await user.click(button);
expect(mockNavigate).toHaveBeenCalledTimes(1);
- expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role');
+ expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/assign-role');
});
it('navigates to assign role page with username query parameter when presetUsername is provided', async () => {
@@ -86,7 +86,7 @@ describe('AddRoleButton', () => {
await user.click(button);
expect(mockNavigate).toHaveBeenCalledTimes(1);
- expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?users=${presetUsername}`);
+ expect(mockNavigate).toHaveBeenCalledWith(`/admin-console/authz/assign-role?users=${presetUsername}`);
});
it('handles special characters in presetUsername correctly', async () => {
@@ -98,7 +98,7 @@ describe('AddRoleButton', () => {
await user.click(button);
expect(mockNavigate).toHaveBeenCalledTimes(1);
- expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?${new URLSearchParams({ users: presetUsername }).toString()}`);
+ expect(mockNavigate).toHaveBeenCalledWith(`/admin-console/authz/assign-role?${new URLSearchParams({ users: presetUsername }).toString()}`);
});
});
@@ -113,7 +113,7 @@ describe('AddRoleButton', () => {
expect(button).toHaveFocus();
await user.keyboard('{Enter}');
- expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role');
+ expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/assign-role');
});
it('responds to spacebar activation', async () => {
@@ -124,7 +124,7 @@ describe('AddRoleButton', () => {
button.focus();
await user.keyboard(' ');
- expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role');
+ expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/assign-role');
});
it('handles multiple clicks gracefully', async () => {
@@ -138,7 +138,7 @@ describe('AddRoleButton', () => {
await user.click(button);
expect(mockNavigate).toHaveBeenCalledTimes(3);
- expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role?users=testuser');
+ expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/assign-role?users=testuser');
});
});
diff --git a/src/authz-module/components/TableCells.test.tsx b/src/authz-module/components/TableCells.test.tsx
index bd79c35c..fbf116b9 100644
--- a/src/authz-module/components/TableCells.test.tsx
+++ b/src/authz-module/components/TableCells.test.tsx
@@ -181,7 +181,7 @@ describe('TableCells Components', () => {
const viewButton = screen.getByRole('button', { name: /view/i });
await user.click(viewButton);
- expect(mockNavigate).toHaveBeenCalledWith('/authz/user/johndoe');
+ expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/johndoe');
});
it('navigates with correct username for different user', async () => {
@@ -201,7 +201,7 @@ describe('TableCells Components', () => {
const viewButton = screen.getByRole('button', { name: /view/i });
await user.click(viewButton);
- expect(mockNavigate).toHaveBeenCalledWith('/authz/user/janedoe');
+ expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/janedoe');
});
it('handles empty username gracefully', async () => {
@@ -221,7 +221,7 @@ describe('TableCells Components', () => {
const viewButton = screen.getByRole('button', { name: /view/i });
await user.click(viewButton);
- expect(mockNavigate).toHaveBeenCalledWith('/authz/user/');
+ expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/');
});
it('handles special characters in username', async () => {
@@ -241,7 +241,7 @@ describe('TableCells Components', () => {
const viewButton = screen.getByRole('button', { name: /view/i });
await user.click(viewButton);
- expect(mockNavigate).toHaveBeenCalledWith('/authz/user/user+with@special.chars');
+ expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/user%2Bwith%40special.chars');
});
it('disables the view action and shows a tooltip when course authoring is disabled for the course', async () => {
diff --git a/src/authz-module/components/TableCells.tsx b/src/authz-module/components/TableCells.tsx
index 36ec474d..f1df74d1 100644
--- a/src/authz-module/components/TableCells.tsx
+++ b/src/authz-module/components/TableCells.tsx
@@ -8,7 +8,7 @@ import { UserRoleWithPermissions, RoleToDelete } from '@src/types';
import { useNavigate } from 'react-router-dom';
import { useContext, useMemo, type ComponentProps } from 'react';
import {
- ADMIN_ROLES, DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL,
+ ADMIN_ROLES, buildAuditUserPath, DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL,
} from '@src/authz-module/constants';
import {
Icon, IconButton, OverlayTrigger, Tooltip, DataTableContext,
@@ -96,7 +96,7 @@ const NameCell = ({ row }: CellProps) => {
const ViewActionCell = ({ row, isCourseEnabled }: CellProps & Partial) => {
const { formatMessage } = useIntl();
const navigate = useNavigate();
- const viewPath = `/authz/user/${row.original.username}`;
+ const viewPath = buildAuditUserPath(row.original.username ?? '');
const isCourseScope = !row.original.role?.startsWith('lib') && !DJANGO_MANAGED_ROLES.includes(row.original.role);
const isDisabled = isCourseEnabled !== undefined && isCourseScope && !isCourseEnabled(row.original.scope);
diff --git a/src/authz-module/constants.test.ts b/src/authz-module/constants.test.ts
index 60b4f9d0..6423e965 100644
--- a/src/authz-module/constants.test.ts
+++ b/src/authz-module/constants.test.ts
@@ -1,9 +1,9 @@
import {
- buildWizardPath, getOrgAggregateScopeKey, getPlatformAggregateScopeKey, ROUTES,
+ AUTHZ_HOME_PATH, buildAuditUserPath, buildWizardPath, getOrgAggregateScopeKey, getPlatformAggregateScopeKey, ROUTES,
} from './constants';
import type { ContextType } from './constants';
-const BASE = `${ROUTES.HOME_PATH}${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`;
+const BASE = `${AUTHZ_HOME_PATH}/${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`;
describe('buildWizardPath', () => {
it('returns the base path when called with no arguments', () => {
@@ -36,6 +36,17 @@ describe('buildWizardPath', () => {
});
});
+describe('buildAuditUserPath', () => {
+ it('builds the absolute audit path for a username', () => {
+ expect(buildAuditUserPath('alice')).toBe(`${AUTHZ_HOME_PATH}/user/alice`);
+ });
+
+ it('URL-encodes the username', () => {
+ expect(buildAuditUserPath('user+with@special.chars'))
+ .toBe(`${AUTHZ_HOME_PATH}/user/user%2Bwith%40special.chars`);
+ });
+});
+
describe('getOrgAggregateScopeKey', () => {
it('returns course wildcard scope for course context', () => {
expect(getOrgAggregateScopeKey('course', 'MIT')).toBe('course-v1:MIT+*');
diff --git a/src/authz-module/constants.ts b/src/authz-module/constants.ts
index 040e97ad..9dc9fcc0 100644
--- a/src/authz-module/constants.ts
+++ b/src/authz-module/constants.ts
@@ -37,14 +37,19 @@ export const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
roles: [],
}));
+/** Absolute base path of the module; src/routes.tsx derives its route prefix from this. */
+export const AUTHZ_HOME_PATH = '/admin-console/authz';
+
+/** Segments nested under the authz splat route, relative to AUTHZ_HOME_PATH. */
export const ROUTES = {
- HOME_PATH: '/authz',
- AUDIT_USER_PATH: '/user/:username',
- ASSIGN_ROLE_WIZARD_PATH: '/assign-role',
+ AUDIT_USER_PATH: 'user/:username',
+ ASSIGN_ROLE_WIZARD_PATH: 'assign-role',
};
+export const buildAuditUserPath = (username: string) => `${AUTHZ_HOME_PATH}/user/${encodeURIComponent(username)}`;
+
export const buildWizardPath = (options?: { users?: string; from?: string }) => {
- const base = `${ROUTES.HOME_PATH}${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`;
+ const base = `${AUTHZ_HOME_PATH}/${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`;
if (!options) return base;
const params = new URLSearchParams();
if (options.users) params.set('users', options.users);
@@ -63,8 +68,6 @@ export enum RoleOperationErrorStatus {
export const MAX_TABLE_FILTERS_APPLIED = 10;
-export const AUTHZ_HOME_PATH = '/authz';
-
export const MAP_ROLE_KEY_TO_LABEL: Record = {
library_admin: 'Library Admin',
library_author: 'Library Author',
diff --git a/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.test.tsx b/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.test.tsx
index e19bbab2..8c50badb 100644
--- a/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.test.tsx
+++ b/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.test.tsx
@@ -104,15 +104,15 @@ describe('AssignRoleWizardPage', () => {
const user = userEvent.setup();
renderPage();
await user.click(screen.getByRole('button', { name: /Cancel/i }));
- expect(navigate).toHaveBeenCalledWith('/authz');
+ expect(navigate).toHaveBeenCalledWith('/admin-console/authz');
});
it('navigates to the from= path when Cancel is clicked and a from param is present', async () => {
- const { navigate } = setupMocks({ from: '/authz/libraries/lib:123/alice' });
+ const { navigate } = setupMocks({ from: '/admin-console/authz/libraries/lib:123/alice' });
const user = userEvent.setup();
renderPage();
await user.click(screen.getByRole('button', { name: /Cancel/i }));
- expect(navigate).toHaveBeenCalledWith('/authz/libraries/lib:123/alice');
+ expect(navigate).toHaveBeenCalledWith('/admin-console/authz/libraries/lib:123/alice');
});
it('navigates to home path when from is an external URL', async () => {
@@ -120,7 +120,7 @@ describe('AssignRoleWizardPage', () => {
const user = userEvent.setup();
renderPage();
await user.click(screen.getByRole('button', { name: /Cancel/i }));
- expect(navigate).toHaveBeenCalledWith('/authz');
+ expect(navigate).toHaveBeenCalledWith('/admin-console/authz');
});
it('navigates to the user-specific view when a single preset user is set', async () => {
@@ -128,15 +128,15 @@ describe('AssignRoleWizardPage', () => {
const user = userEvent.setup();
renderPage();
await user.click(screen.getByRole('button', { name: /Cancel/i }));
- expect(navigate).toHaveBeenCalledWith('/authz/user/alice');
+ expect(navigate).toHaveBeenCalledWith('/admin-console/authz/user/alice');
});
it('navigates to returnTo when multiple preset users are set', async () => {
- const { navigate } = setupMocks({ users: 'alice,bob', from: '/authz/team' });
+ const { navigate } = setupMocks({ users: 'alice,bob', from: '/admin-console/authz/team' });
const user = userEvent.setup();
renderPage();
await user.click(screen.getByRole('button', { name: /Cancel/i }));
- expect(navigate).toHaveBeenCalledWith('/authz/team');
+ expect(navigate).toHaveBeenCalledWith('/admin-console/authz/team');
});
describe('assignable roles', () => {
diff --git a/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.tsx b/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.tsx
index 24ab3984..f618f103 100644
--- a/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.tsx
+++ b/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.tsx
@@ -3,7 +3,7 @@ import { useIntl } from '@openedx/frontend-base';
import { useValidateUserPermissionsNonSuspense } from '@src/data/hooks';
import AssignRoleWizard from './AssignRoleWizard';
import AuthZLayout from '@src/authz-module/components/AuthZLayout';
-import { ROUTES } from '@src/authz-module/constants';
+import { AUTHZ_HOME_PATH, buildAuditUserPath } from '@src/authz-module/constants';
import messages from './messages';
import {
CONTENT_COURSE_PERMISSIONS, CONTENT_LIBRARY_PERMISSIONS, courseRolesMetadata, libraryRolesMetadata,
@@ -17,11 +17,11 @@ const AssignRoleWizardPage = () => {
const [searchParams] = useSearchParams();
const initialUsers = searchParams.get('users') || '';
const raw = searchParams.get('from') ?? '';
- const returnTo = (raw.startsWith('/') && !raw.startsWith('//')) ? raw : ROUTES.HOME_PATH;
+ const returnTo = (raw.startsWith('/') && !raw.startsWith('//')) ? raw : AUTHZ_HOME_PATH;
const presetUser = initialUsers.trim();
const destination = (presetUser && !presetUser.includes(','))
- ? `${ROUTES.HOME_PATH}/user/${presetUser}`
+ ? buildAuditUserPath(presetUser)
: returnTo;
const { data: managePermissions } = useValidateUserPermissionsNonSuspense(MANAGE_TEAM_PERMISSIONS);
diff --git a/src/authz-module/team-members/TeamMembersTable.test.tsx b/src/authz-module/team-members/TeamMembersTable.test.tsx
index 946a9c0a..c9ba6ca4 100644
--- a/src/authz-module/team-members/TeamMembersTable.test.tsx
+++ b/src/authz-module/team-members/TeamMembersTable.test.tsx
@@ -218,7 +218,7 @@ describe('TeamMembersTable', () => {
});
const viewButtons = screen.getAllByRole('button', { name: /view/i });
await user.click(viewButtons[0]);
- expect(mockNavigate).toHaveBeenCalledWith('/authz/user/johndoe');
+ expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/johndoe');
});
it('renders safely when role assignments data is undefined', () => {
diff --git a/src/routes.test.tsx b/src/routes.test.tsx
index 3346fe33..0ed942c8 100644
--- a/src/routes.test.tsx
+++ b/src/routes.test.tsx
@@ -17,7 +17,7 @@ describe('routes', () => {
it('has the expected id and path', () => {
expect(routes[0].id).toBe('org.openedx.frontend.route.adminConsole.main');
- expect(routes[0].path).toBe('/authz/*');
+ expect(routes[0].path).toBe('/admin-console/authz/*');
});
it('is labelled with the admin console role', () => {
diff --git a/src/routes.tsx b/src/routes.tsx
index 71831b67..de5d8da8 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -1,12 +1,14 @@
import { lazy } from 'react';
import { authenticatedLoader } from '@openedx/frontend-base';
+import { AUTHZ_HOME_PATH } from './authz-module/constants';
+
const Main = lazy(() => import('./Main'));
const routes = [
{
id: 'org.openedx.frontend.route.adminConsole.main',
- path: '/authz/*',
+ path: `${AUTHZ_HOME_PATH}/*`,
loader: authenticatedLoader,
Component: Main,
handle: {