Skip to content

Commit 34afd39

Browse files
committed
feat: implement Assign Role Wizard with user validation and role assignment steps
1 parent 62ca5d2 commit 34afd39

11 files changed

Lines changed: 564 additions & 56 deletions

File tree

src/authz-module/constants.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const ROUTES = {
22
LIBRARIES_TEAM_PATH: '/libraries/:libraryId',
33
LIBRARIES_USER_PATH: '/libraries/:libraryId/:username',
4+
ASSIGN_ROLE_WIZARD_PATH: '/assign-role',
45
};
56

67
export enum RoleOperationErrorStatus {
@@ -10,3 +11,120 @@ export enum RoleOperationErrorStatus {
1011
ROLE_ASSIGNMENT_ERROR = 'role_assignment_error',
1112
ROLE_REMOVAL_ERROR = 'role_removal_error',
1213
}
14+
15+
// Content Library Permission Keys
16+
export const CONTENT_LIBRARY_PERMISSIONS = {
17+
DELETE_LIBRARY: 'content_libraries.delete_library',
18+
MANAGE_LIBRARY_TAGS: 'content_libraries.manage_library_tags',
19+
VIEW_LIBRARY: 'content_libraries.view_library',
20+
21+
EDIT_LIBRARY_CONTENT: 'content_libraries.edit_library_content',
22+
PUBLISH_LIBRARY_CONTENT: 'content_libraries.publish_library_content',
23+
REUSE_LIBRARY_CONTENT: 'content_libraries.reuse_library_content',
24+
25+
CREATE_LIBRARY_COLLECTION: 'content_libraries.create_library_collection',
26+
EDIT_LIBRARY_COLLECTION: 'content_libraries.edit_library_collection',
27+
DELETE_LIBRARY_COLLECTION: 'content_libraries.delete_library_collection',
28+
29+
MANAGE_LIBRARY_TEAM: 'content_libraries.manage_library_team',
30+
VIEW_LIBRARY_TEAM: 'content_libraries.view_library_team',
31+
};
32+
33+
// Note: this information will eventually come from the backend API
34+
// but for the MVP we decided to manage it in the frontend
35+
export const libraryRolesMetadata = [
36+
{
37+
role: 'library_admin', name: 'Library Admin', description: 'The Library Admin has full control over the library, including managing users, modifying content, and handling publishing workflows. They ensure content is properly maintained and accessible as needed.', contextType: 'library',
38+
},
39+
{
40+
role: 'library_author', name: 'Library Author', description: 'The Library Author is responsible for creating, editing, and publishing content within a library. They can manage tags and collections but cannot delete libraries or manage users.', contextType: 'library',
41+
},
42+
{
43+
role: 'library_contributor', name: 'Library Contributor', description: 'The Library Contributor can create and edit content within a library but cannot publish it. They support the authoring process while leaving final publishing to Authors or Admins.', contextType: 'library',
44+
},
45+
{
46+
role: 'library_user', name: 'Library User', description: 'The Library User can view and reuse content but cannot edit or delete any resource.', contextType: 'library',
47+
},
48+
];
49+
50+
export const libraryResourceTypes = [
51+
{ key: 'library', label: 'Library', description: 'Permissions related to the library as a whole.' },
52+
{ key: 'library_content', label: 'Content', description: 'Permissions to create, edit, delete, and publish individual content items within the library.' },
53+
{ key: 'library_collection', label: 'Collection', description: 'Permissions to create, edit, and delete content collections within the library.' },
54+
{ key: 'library_team', label: 'Team', description: 'Permissions to manage user access and roles within the library.' },
55+
];
56+
57+
export const libraryPermissions = [
58+
{ key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY, resource: 'library', description: 'Allows the user to delete the library and all its contents.' },
59+
{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS, resource: 'library', description: 'Add or remove tags from content.' },
60+
{ key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY, resource: 'library', description: 'View content, search, filter, and sort within the library.' },
61+
62+
{ key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT, resource: 'library_content', description: 'Edit content in draft mode' },
63+
{ key: CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, resource: 'library_content', description: 'Publish content, making it available for reuse' },
64+
{ key: CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT, resource: 'library_content', description: 'Reuse published content within a course.' },
65+
66+
{ key: CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Create new collections within a library.' },
67+
{ key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Add or remove content from existing collections.' },
68+
{ key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Delete entire collections from the library.' },
69+
70+
{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, resource: 'library_team', description: 'View the list of users who have access to the library.' },
71+
{ key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
72+
];
73+
74+
// Resource Type Definitions
75+
export const RESOURCE_TYPES = {
76+
LIBRARY: 'library',
77+
COURSE: 'course',
78+
} as const;
79+
80+
export type ResourceType = typeof RESOURCE_TYPES[keyof typeof RESOURCE_TYPES];
81+
82+
export const courseRolesMetadata = [
83+
{
84+
role: 'course_admin', name: 'Course Admin', description: 'Can manage the course team and all course settings.', contextType: 'course',
85+
},
86+
{
87+
role: 'course_staff', name: 'Course Staff', description: 'Can publish content and manage the course lifecycle in Studio.', contextType: 'course',
88+
},
89+
{
90+
role: 'course_editor', name: 'Course Editor', description: 'Can create and edit course content, but cannot publish or change critical course settings.', contextType: 'course', disabled: true,
91+
},
92+
{
93+
role: 'course_auditor', name: 'Course Auditor', description: 'Can view course content and settings, but cannot make changes.', contextType: 'course', disabled: true,
94+
},
95+
];
96+
97+
// Course roles placeholder for future implementation
98+
export const COURSE_ROLES_METADATA = courseRolesMetadata;
99+
100+
// Get roles metadata by resource type
101+
export const getRolesMetadata = (resourceType: ResourceType) => {
102+
switch (resourceType) {
103+
case RESOURCE_TYPES.LIBRARY:
104+
return libraryRolesMetadata;
105+
case RESOURCE_TYPES.COURSE:
106+
return COURSE_ROLES_METADATA;
107+
default:
108+
return [];
109+
}
110+
};
111+
112+
// Get permissions by resource type
113+
export const getPermissions = (resourceType: ResourceType) => {
114+
switch (resourceType) {
115+
case RESOURCE_TYPES.LIBRARY:
116+
return libraryPermissions;
117+
default:
118+
return [];
119+
}
120+
};
121+
122+
// Get resource types by resource type
123+
export const getResourceTypes = (resourceType: ResourceType) => {
124+
switch (resourceType) {
125+
case RESOURCE_TYPES.LIBRARY:
126+
return libraryResourceTypes;
127+
default:
128+
return [];
129+
}
130+
};

src/authz-module/data/api.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ export interface AssignTeamMembersRoleRequest {
5050
scope: string;
5151
}
5252

53+
// Validate Users API
54+
export type ValidateUsersRequest = {
55+
users: string[];
56+
};
57+
58+
export type ValidateUsersResponse = {
59+
validUsers: string[];
60+
invalidUsers: string[];
61+
};
62+
5363
export const getTeamMembers = async (object: string, querySettings: QuerySettings): Promise<GetTeamMembersResponse> => {
5464
const url = new URL(getApiUrl(`/api/authz/v1/roles/users/?scope=${object}`));
5565

@@ -77,6 +87,16 @@ export const assignTeamMembersRole = async (
7787
return camelCaseObject(res.data);
7888
};
7989

90+
export const validateUsers = async (
91+
data: ValidateUsersRequest,
92+
): Promise<ValidateUsersResponse> => {
93+
const res = await getAuthenticatedHttpClient().post(
94+
getApiUrl('/api/authz/v1/users/validate'),
95+
data,
96+
);
97+
return camelCaseObject(res.data);
98+
};
99+
80100
// TODO: this should be replaced in the future with Console API
81101
export const getLibrary = async (libraryId: string): Promise<LibraryMetadata> => {
82102
const { data } = await getAuthenticatedHttpClient().get(getStudioApiUrl(`/api/libraries/v2/${libraryId}/`));

src/authz-module/data/hooks.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { LibraryMetadata } from '@src/types';
66
import {
77
assignTeamMembersRole, AssignTeamMembersRoleRequest, getLibrary, getPermissionsByRole, getTeamMembers,
88
GetTeamMembersResponse, PermissionsByRole, QuerySettings, revokeUserRoles, RevokeUserRolesRequest,
9+
validateUsers, ValidateUsersRequest,
910
} from './api';
1011

1112
const authzQueryKeys = {
@@ -91,6 +92,20 @@ export const useAssignTeamMembersRole = () => {
9192
});
9293
};
9394

95+
/**
96+
* React Query hook to validate users exist without assigning roles.
97+
* It checks if the provided usernames/email addresses are valid.
98+
*
99+
* @example
100+
* const { mutate: validateUsers } = useValidateUsers();
101+
* validateUsers({ data: { users: ['jdoe', 'jane@example.com'] } });
102+
*/
103+
export const useValidateUsers = () => useMutation({
104+
mutationFn: async ({ data }: {
105+
data: ValidateUsersRequest
106+
}) => validateUsers(data),
107+
});
108+
94109
/**
95110
* React Query hook to remove roles for a specific team member within a scope.
96111
*

src/authz-module/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import LoadingPage from '@src/components/LoadingPage';
66
import LibrariesErrorFallback from '@src/authz-module/libraries-manager/ErrorPage';
77
import { ToastManagerProvider } from './libraries-manager/ToastManagerContext';
88
import { LibrariesTeamManager, LibrariesUserManager, LibrariesLayout } from './libraries-manager';
9+
import AssignRoleWizardPage from './wizard/AssignRoleWizardPage';
910
import { ROUTES } from './constants';
1011

1112
import './index.scss';
@@ -21,6 +22,7 @@ const AuthZModule = () => (
2122
<Route path={ROUTES.LIBRARIES_TEAM_PATH} element={<LibrariesTeamManager />} />
2223
<Route path={ROUTES.LIBRARIES_USER_PATH} element={<LibrariesUserManager />} />
2324
</Route>
25+
<Route path={ROUTES.ASSIGN_ROLE_WIZARD_PATH} element={<AssignRoleWizardPage />} />
2426
</Routes>
2527
</Suspense>
2628
</ToastManagerProvider>

src/authz-module/libraries-manager/LibrariesTeamManager.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
import { useMemo } from 'react';
22
import { useIntl } from '@edx/frontend-platform/i18n';
33
import {
4-
Container, Skeleton, Tab, Tabs,
4+
Container, Skeleton, Tab, Tabs, Button,
55
} from '@openedx/paragon';
66
import { useLibrary } from '@src/authz-module/data/hooks';
7-
import { useLocation } from 'react-router-dom';
7+
import { useLocation, useNavigate } from 'react-router-dom';
88
import { ROUTES } from '@src/authz-module/constants';
9+
import { Plus } from '@openedx/paragon/icons';
910
import TeamTable from './components/TeamTable';
1011
import AuthZLayout from '../components/AuthZLayout';
1112
import RoleCard from '../components/RoleCard';
1213
import PermissionTable from '../components/PermissionTable';
1314
import { useLibraryAuthZ } from './context';
14-
import { AddNewTeamMemberTrigger } from './components/AddNewTeamMemberModal';
1515
import { buildPermissionMatrixByResource, buildPermissionMatrixByRole } from './utils';
1616

1717
import messages from './messages';
1818

1919
const LibrariesTeamManager = () => {
2020
const intl = useIntl();
2121
const { hash } = useLocation();
22+
const navigate = useNavigate();
2223
const {
2324
libraryId, canManageTeam, roles, permissions, resources,
2425
} = useLibraryAuthZ();
@@ -27,6 +28,11 @@ const LibrariesTeamManager = () => {
2728
const pageTitle = intl.formatMessage(messages['library.authz.manage.page.title']);
2829
const teamMembersPath = `/authz${ROUTES.LIBRARIES_TEAM_PATH.replace(':libraryId', libraryId)}`;
2930

31+
// Handler to navigate to Assign Role Wizard
32+
const handleNavigateToWizard = () => {
33+
navigate(`/authz/assign-role?scope=${libraryId}`);
34+
};
35+
3036
const [libraryPermissionsByRole, libraryPermissionsByResource] = useMemo(() => {
3137
if (!roles && !permissions && !resources) { return [null, null]; }
3238
const permissionsByRole = buildPermissionMatrixByRole({
@@ -50,9 +56,18 @@ const LibrariesTeamManager = () => {
5056
pageTitle={pageTitle}
5157
pageSubtitle={libraryId}
5258
actions={
53-
[
54-
...(canManageTeam ? [<AddNewTeamMemberTrigger libraryId={libraryId} key="add-new-member" />] : []),
55-
]
59+
canManageTeam
60+
? [
61+
<Button
62+
iconBefore={Plus}
63+
variant="primary"
64+
onClick={handleNavigateToWizard}
65+
key="add-new-role"
66+
>
67+
{intl.formatMessage(messages['library.authz.manage.add.role.button']) || 'New Role'}
68+
</Button>,
69+
]
70+
: []
5671
}
5772
>
5873
<Tabs

src/authz-module/libraries-manager/constants.ts

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,17 @@
1-
import { PermissionMetadata, ResourceMetadata, RoleMetadata } from 'types';
2-
3-
export const CONTENT_LIBRARY_PERMISSIONS = {
4-
DELETE_LIBRARY: 'content_libraries.delete_library',
5-
MANAGE_LIBRARY_TAGS: 'content_libraries.manage_library_tags',
6-
VIEW_LIBRARY: 'content_libraries.view_library',
7-
8-
EDIT_LIBRARY_CONTENT: 'content_libraries.edit_library_content',
9-
PUBLISH_LIBRARY_CONTENT: 'content_libraries.publish_library_content',
10-
REUSE_LIBRARY_CONTENT: 'content_libraries.reuse_library_content',
11-
12-
CREATE_LIBRARY_COLLECTION: 'content_libraries.create_library_collection',
13-
EDIT_LIBRARY_COLLECTION: 'content_libraries.edit_library_collection',
14-
DELETE_LIBRARY_COLLECTION: 'content_libraries.delete_library_collection',
15-
16-
MANAGE_LIBRARY_TEAM: 'content_libraries.manage_library_team',
17-
VIEW_LIBRARY_TEAM: 'content_libraries.view_library_team',
1+
import {
2+
CONTENT_LIBRARY_PERMISSIONS,
3+
libraryRolesMetadata,
4+
libraryResourceTypes,
5+
libraryPermissions,
6+
} from '../constants';
7+
8+
export {
9+
CONTENT_LIBRARY_PERMISSIONS,
10+
libraryRolesMetadata,
11+
libraryResourceTypes,
12+
libraryPermissions,
1813
};
1914

20-
// Note: this information will eventually come from the backend API
21-
// but for the MVP we decided to manage it in the frontend
22-
export const libraryRolesMetadata: RoleMetadata[] = [
23-
{ role: 'library_admin', name: 'Library Admin', description: 'The Library Admin has full control over the library, including managing users, modifying content, and handling publishing workflows. They ensure content is properly maintained and accessible as needed.' },
24-
{ role: 'library_author', name: 'Library Author', description: 'The Library Author is responsible for creating, editing, and publishing content within a library. They can manage tags and collections but cannot delete libraries or manage users.' },
25-
{ role: 'library_contributor', name: 'Library Contributor', description: 'The Library Contributor can create and edit content within a library but cannot publish it. They support the authoring process while leaving final publishing to Authors or Admins.' },
26-
{ role: 'library_user', name: 'Library User', description: 'The Library User can view and reuse content but cannot edit or delete any resource.' },
27-
];
28-
29-
export const libraryResourceTypes: ResourceMetadata[] = [
30-
{ key: 'library', label: 'Library', description: 'Permissions related to the library as a whole.' },
31-
{ key: 'library_content', label: 'Content', description: 'Permissions to create, edit, delete, and publish individual content items within the library.' },
32-
{ key: 'library_collection', label: 'Collection', description: 'Permissions to create, edit, and delete content collections within the library.' },
33-
{ key: 'library_team', label: 'Team', description: 'Permissions to manage user access and roles within the library.' },
34-
];
35-
36-
export const libraryPermissions: PermissionMetadata[] = [
37-
{ key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY, resource: 'library', description: 'Allows the user to delete the library and all its contents.' },
38-
{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS, resource: 'library', description: 'Add or remove tags from content.' },
39-
{ key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY, resource: 'library', description: 'View content, search, filter, and sort within the library.' },
40-
41-
{ key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT, resource: 'library_content', description: 'Edit content in draft mode' },
42-
{ key: CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, resource: 'library_content', description: 'Publish content, making it available for reuse' },
43-
{ key: CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT, resource: 'library_content', description: 'Reuse published content within a course.' },
44-
45-
{ key: CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Create new collections within a library.' },
46-
{ key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Add or remove content from existing collections.' },
47-
{ key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Delete entire collections from the library.' },
48-
49-
{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, resource: 'library_team', description: 'View the list of users who have access to the library.' },
50-
{ key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
51-
];
52-
5315
export const DEFAULT_TOAST_DELAY = 5000;
5416
export const RETRY_TOAST_DELAY = 120_000; // 2 minutes
5517
export const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({

src/authz-module/libraries-manager/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const messages = defineMessages({
1111
defaultMessage: 'Manage Access',
1212
description: 'Libreries AuthZ root breafcrumb',
1313
},
14+
'library.authz.manage.add.role.button': {
15+
id: 'library.authz.manage.add.role.button',
16+
defaultMessage: 'Assign Role',
17+
description: 'Button to add a new role',
18+
},
1419
'library.authz.tabs.team': {
1520
id: 'library.authz.tabs.team',
1621
defaultMessage: 'Team Members',

0 commit comments

Comments
 (0)