diff --git a/src/courseTeam/CourseTeamPage.test.tsx b/src/courseTeam/CourseTeamPage.test.tsx
new file mode 100644
index 00000000..4817ee9f
--- /dev/null
+++ b/src/courseTeam/CourseTeamPage.test.tsx
@@ -0,0 +1,60 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { renderWithIntl } from '@src/testUtils';
+import CourseTeamPage from './CourseTeamPage';
+
+// Mock the child components, each component should have its own test suite
+jest.mock('./components/MembersContent', () => {
+ return function MembersContent() {
+ return
Members Content
;
+ };
+});
+
+jest.mock('./components/RolesContent', () => {
+ return function RolesContent() {
+ return Roles Content
;
+ };
+});
+
+describe('CourseTeamPage', () => {
+ it('renders the course team title', () => {
+ renderWithIntl( );
+ expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument();
+ });
+
+ it('renders the add team member button', () => {
+ renderWithIntl( );
+ expect(screen.getByRole('button', { name: /add team member/i })).toBeInTheDocument();
+ });
+
+ it('renders both tabs', () => {
+ renderWithIntl( );
+ expect(screen.getByRole('tab', { name: /members/i })).toBeInTheDocument();
+ expect(screen.getByRole('tab', { name: /roles/i })).toBeInTheDocument();
+ });
+
+ it('renders MembersContent by default', () => {
+ renderWithIntl( );
+ expect(screen.getByText('Members Content')).toBeInTheDocument();
+ });
+
+ it('has correct CSS classes on title', () => {
+ renderWithIntl( );
+ const title = screen.getByRole('heading', { level: 3 });
+ expect(title).toHaveClass('text-primary-700', 'mb-0');
+ });
+
+ it('has primary variant on add button', () => {
+ renderWithIntl( );
+ const button = screen.getByRole('button', { name: /add team member/i });
+ expect(button).toHaveClass('btn-primary');
+ });
+
+ it('renders RolesContent when Roles tab is selected', async () => {
+ renderWithIntl( );
+ const rolesTab = screen.getByRole('tab', { name: /roles/i });
+ const user = userEvent.setup();
+ await user.click(rolesTab);
+ expect(screen.getByText('Roles Content')).toBeInTheDocument();
+ });
+});
diff --git a/src/courseTeam/CourseTeamPage.tsx b/src/courseTeam/CourseTeamPage.tsx
index 065f294c..7449abf1 100644
--- a/src/courseTeam/CourseTeamPage.tsx
+++ b/src/courseTeam/CourseTeamPage.tsx
@@ -1,8 +1,27 @@
+import { useIntl } from '@openedx/frontend-base';
+import messages from './messages';
+import { Button, Tab, Tabs } from '@openedx/paragon';
+import MembersContent from './components/MembersContent';
+import RolesContent from './components/RolesContent';
+
const CourseTeamPage = () => {
+ const intl = useIntl();
+
return (
-
-
Course Team
-
+ <>
+
+
{intl.formatMessage(messages.courseTeamTitle)}
+ + {intl.formatMessage(messages.addTeamMember)}
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/src/courseTeam/components/MembersContent.test.tsx b/src/courseTeam/components/MembersContent.test.tsx
new file mode 100644
index 00000000..978920d6
--- /dev/null
+++ b/src/courseTeam/components/MembersContent.test.tsx
@@ -0,0 +1,128 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { renderWithIntl } from '@src/testUtils';
+import { useTeamMembers } from '../data/apiHook';
+import MembersContent from './MembersContent';
+import messages from '../messages';
+
+const courseId = 'course-v1:edX+DemoX+Demo_Course';
+
+jest.mock('../data/apiHook', () => ({
+ useTeamMembers: jest.fn(),
+}));
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({ courseId: courseId }),
+}));
+
+const mockTeamMembers = [
+ { username: 'user1', email: 'user1@example.com', role: 'Admin' },
+ { username: 'user2', email: 'user2@example.com', role: 'Staff' },
+];
+
+const renderComponent = () => renderWithIntl( );
+
+describe('MembersContent', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders loading state correctly', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: { results: [], numPages: 1, count: 0 },
+ isLoading: true,
+ });
+
+ renderComponent();
+ expect(screen.getByRole('table')).toBeInTheDocument();
+ });
+
+ it('renders team members data correctly', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: { results: mockTeamMembers, numPages: 1, count: 2 },
+ isLoading: false,
+ });
+
+ renderComponent();
+
+ expect(screen.getByText(mockTeamMembers[0].username)).toBeInTheDocument();
+ expect(screen.getByText(mockTeamMembers[0].email)).toBeInTheDocument();
+ expect(screen.getByText(mockTeamMembers[0].role)).toBeInTheDocument();
+ expect(screen.getByText(mockTeamMembers[1].username)).toBeInTheDocument();
+ expect(screen.getByText(mockTeamMembers[1].email)).toBeInTheDocument();
+ expect(screen.getByText(mockTeamMembers[1].role)).toBeInTheDocument();
+ });
+
+ it('renders empty state when no team members', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: { results: [], numPages: 1, count: 0 },
+ isLoading: false,
+ });
+
+ renderComponent();
+ expect(screen.getByText(messages.noTeamMembers.defaultMessage)).toBeInTheDocument();
+ });
+
+ it('calls useTeamMembers with correct parameters', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: { results: [], numPages: 1, count: 0 },
+ isLoading: false,
+ });
+
+ renderComponent();
+
+ expect(useTeamMembers).toHaveBeenCalledWith(courseId, {
+ page: 0,
+ emailOrUsername: '',
+ role: '',
+ pageSize: 25,
+ });
+ });
+
+ it('handles pagination correctly', async () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: { results: mockTeamMembers, numPages: 3, count: 50 },
+ isLoading: false,
+ });
+
+ renderComponent();
+
+ const nextPageButton = screen.getByLabelText(/next/i);
+ const user = userEvent.setup();
+ await user.click(nextPageButton);
+
+ expect(useTeamMembers).toHaveBeenLastCalledWith(courseId, {
+ page: 1,
+ emailOrUsername: '',
+ role: '',
+ pageSize: 25,
+ });
+ });
+
+ it('renders action buttons for each row', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: { results: mockTeamMembers, numPages: 1, count: 2 },
+ isLoading: false,
+ });
+
+ renderComponent();
+
+ const editButtons = screen.getAllByText(messages.edit.defaultMessage);
+ expect(editButtons).toHaveLength(2);
+ });
+
+ it('renders table headers correctly', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: { results: mockTeamMembers, numPages: 1, count: 2 },
+ isLoading: false,
+ });
+
+ renderComponent();
+
+ expect(screen.getByText(messages.username.defaultMessage)).toBeInTheDocument();
+ expect(screen.getByText(messages.email.defaultMessage)).toBeInTheDocument();
+ expect(screen.getByText(messages.role.defaultMessage)).toBeInTheDocument();
+ expect(screen.getByText(messages.actions.defaultMessage)).toBeInTheDocument();
+ });
+});
diff --git a/src/courseTeam/components/MembersContent.tsx b/src/courseTeam/components/MembersContent.tsx
new file mode 100644
index 00000000..0e8e3b5e
--- /dev/null
+++ b/src/courseTeam/components/MembersContent.tsx
@@ -0,0 +1,73 @@
+import { useState, useCallback, useMemo } from 'react';
+import { useParams } from 'react-router-dom';
+import { useIntl } from '@openedx/frontend-base';
+import { Button, DataTable } from '@openedx/paragon';
+import messages from '../messages';
+import { useTeamMembers } from '../data/apiHook';
+
+const TEAM_MEMBERS_PAGE_SIZE = 25;
+
+const MembersContent = () => {
+ const intl = useIntl();
+ const { courseId = '' } = useParams<{ courseId: string }>();
+ const [filters, setFilters] = useState({ page: 0, emailOrUsername: '', role: '' });
+ const { data: { results: teamMembers = [], numPages = 1, count = 0 } = {}, isLoading = false } = useTeamMembers(courseId, { ...filters, pageSize: TEAM_MEMBERS_PAGE_SIZE });
+
+ const tableColumns = useMemo(() => [
+ { accessor: 'username', Header: intl.formatMessage(messages.username) },
+ { accessor: 'email', Header: intl.formatMessage(messages.email) },
+ { accessor: 'role', Header: intl.formatMessage(messages.role) },
+ ], [intl]);
+
+ const additionalColumns = useMemo(() => [{
+ id: 'actions',
+ Header: intl.formatMessage(messages.actions),
+ Cell: () => (
+
+ {intl.formatMessage(messages.edit)}
+
+ )
+ }], [intl]);
+
+ const handleFetchData = useCallback(({ pageIndex, filters: tableFilters }: { pageIndex: number, filters: { id: string, value: string }[] }) => {
+ // Filters will be handled in a future iteration, for now we will just update pagination
+ console.log(pageIndex, tableFilters);
+ if (pageIndex !== filters.page) {
+ setFilters(prevFilters => ({
+ ...prevFilters,
+ page: pageIndex,
+ }));
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const tableState = useMemo(() => ({
+ pageIndex: filters.page,
+ pageSize: TEAM_MEMBERS_PAGE_SIZE,
+ }), [filters.page]);
+
+ return (
+ null}
+ >
+
+
+
+
+
+ );
+};
+
+export default MembersContent;
diff --git a/src/courseTeam/components/RolesContent.tsx b/src/courseTeam/components/RolesContent.tsx
new file mode 100644
index 00000000..0d46b418
--- /dev/null
+++ b/src/courseTeam/components/RolesContent.tsx
@@ -0,0 +1,9 @@
+const RolesContent = () => {
+ return (
+
+ Roles content goes here.
+
+ );
+};
+
+export default RolesContent;
diff --git a/src/courseTeam/data/api.test.ts b/src/courseTeam/data/api.test.ts
new file mode 100644
index 00000000..a1ee663a
--- /dev/null
+++ b/src/courseTeam/data/api.test.ts
@@ -0,0 +1,78 @@
+import { getAuthenticatedHttpClient } from '@openedx/frontend-base';
+import { getTeamMembers, getRoles } from './api';
+
+jest.mock('@openedx/frontend-base', () => ({
+ ...jest.requireActual('@openedx/frontend-base'),
+ getAuthenticatedHttpClient: jest.fn(),
+}));
+
+jest.mock('../../data/api', () => ({
+ getApiBaseUrl: jest.fn().mockReturnValue(''),
+}));
+
+const httpClientMock = {
+ get: jest.fn(),
+};
+
+beforeEach(() => {
+ (getAuthenticatedHttpClient as jest.Mock).mockReturnValue(httpClientMock);
+});
+
+describe('courseTeam API', () => {
+ describe('getTeamMembers', () => {
+ it('should call the correct endpoint to get team members', async () => {
+ const courseId = 'course-v1:edX+DemoX+Demo_Course';
+ const params = { page: 0, pageSize: 10 };
+ httpClientMock.get.mockResolvedValue({ data: { results: [], count: 0 } });
+
+ await getTeamMembers(courseId, params);
+
+ const expectedUrl = `/api/instructor/v2/courses/${courseId}/team_members?page=1&page_size=10`;
+ expect(httpClientMock.get).toHaveBeenCalledWith(expectedUrl);
+ });
+
+ it('should include email_or_username in query params if provided', async () => {
+ const courseId = 'course-v1:edX+DemoX+Demo_Course';
+ const params = { page: 0, pageSize: 10, emailOrUsername: 'test@example.com' };
+ httpClientMock.get.mockResolvedValue({ data: { results: [], count: 0 } });
+
+ await getTeamMembers(courseId, params);
+
+ const expectedUrl = `/api/instructor/v2/courses/${courseId}/team_members?page=1&page_size=10&email_or_username=test%40example.com`;
+ expect(httpClientMock.get).toHaveBeenCalledWith(expectedUrl);
+ });
+
+ it('should include role in query params if provided', async () => {
+ const courseId = 'course-v1:edX+DemoX+Demo_Course';
+ const params = { page: 0, pageSize: 10, role: 'instructor' };
+ httpClientMock.get.mockResolvedValue({ data: { results: [], count: 0 } });
+
+ await getTeamMembers(courseId, params);
+
+ const expectedUrl = `/api/instructor/v2/courses/${courseId}/team_members?page=1&page_size=10&role=instructor`;
+ expect(httpClientMock.get).toHaveBeenCalledWith(expectedUrl);
+ });
+ });
+
+ describe('getRoles', () => {
+ it('should call the correct endpoint to get roles', async () => {
+ const courseId = 'course-v1:edX+DemoX+Demo_Course';
+ httpClientMock.get.mockResolvedValue({ data: { roles: [] } });
+
+ await getRoles(courseId);
+
+ const expectedUrl = `/api/instructor/v2/courses/${courseId}/team_roles`;
+ expect(httpClientMock.get).toHaveBeenCalledWith(expectedUrl);
+ });
+
+ it('should return the roles from the response', async () => {
+ const courseId = 'course-v1:edX+DemoX+Demo_Course';
+ const roles = ['instructor', 'staff'];
+ httpClientMock.get.mockResolvedValue({ data: { roles } });
+
+ const result = await getRoles(courseId);
+
+ expect(result).toEqual(roles);
+ });
+ });
+});
diff --git a/src/courseTeam/data/api.ts b/src/courseTeam/data/api.ts
new file mode 100644
index 00000000..f813a043
--- /dev/null
+++ b/src/courseTeam/data/api.ts
@@ -0,0 +1,34 @@
+import { camelCaseObject, getAuthenticatedHttpClient } from '@openedx/frontend-base';
+import { getApiBaseUrl } from '../../data/api';
+import { DataList } from '@src/types';
+import { CourseTeamMember, CourseTeamMemberQueryParams } from '../types';
+
+export const getTeamMembers = async (
+ courseId: string,
+ params: CourseTeamMemberQueryParams
+): Promise> => {
+ const queryParams = new URLSearchParams({
+ page: (params.page + 1).toString(),
+ page_size: params.pageSize.toString(),
+ });
+
+ if (params.emailOrUsername) {
+ queryParams.append('email_or_username', params.emailOrUsername);
+ }
+
+ if (params.role) {
+ queryParams.append('role', params.role);
+ }
+
+ const { data } = await getAuthenticatedHttpClient().get(
+ `${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/team_members?${queryParams.toString()}`
+ );
+ return camelCaseObject(data);
+};
+
+export const getRoles = async (courseId: string): Promise => {
+ const { data } = await getAuthenticatedHttpClient().get(
+ `${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/team_roles`
+ );
+ return data.roles;
+};
diff --git a/src/courseTeam/data/apiHook.test.tsx b/src/courseTeam/data/apiHook.test.tsx
new file mode 100644
index 00000000..642dad9d
--- /dev/null
+++ b/src/courseTeam/data/apiHook.test.tsx
@@ -0,0 +1,142 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactNode } from 'react';
+import { useTeamMembers, useRoles } from './apiHook';
+import * as api from './api';
+import { CourseTeamMember } from '../types';
+import { DataList } from '../../types';
+
+jest.mock('./api');
+
+const mockGetTeamMembers = api.getTeamMembers as jest.MockedFunction;
+const mockGetRoles = api.getRoles as jest.MockedFunction;
+
+const createWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+ const WrappedComponent = ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+ return WrappedComponent;
+};
+
+describe('apiHook', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('useTeamMembers', () => {
+ it('should fetch course team members successfully', async () => {
+ const mockTeamMembers: DataList = {
+ count: 2,
+ next: null,
+ previous: null,
+ numPages: 1,
+ results: [
+ { username: 'john.doe', email: 'john@example.com', role: 'instructor' },
+ { username: 'jane.smith', email: 'jane@example.com', role: 'staff' },
+ ],
+ };
+
+ mockGetTeamMembers.mockResolvedValue(mockTeamMembers);
+
+ const { result } = renderHook(() => useTeamMembers('course-v1:org+course+run', {
+ page: 0,
+ pageSize: 25,
+ }), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ });
+
+ expect(result.current.data).toEqual(mockTeamMembers);
+ expect(mockGetTeamMembers).toHaveBeenCalledWith('course-v1:org+course+run', {
+ page: 0,
+ pageSize: 25,
+ });
+ });
+
+ it('should handle error when fetching course team fails', async () => {
+ const mockError = new Error('Failed to fetch course team');
+ mockGetTeamMembers.mockRejectedValue(mockError);
+
+ const { result } = renderHook(() => useTeamMembers('course-v1:org+course+run', {
+ page: 0,
+ pageSize: 25,
+ }), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isError).toBe(true);
+ });
+
+ expect(result.current.error).toEqual(mockError);
+ });
+
+ it('should be disabled when courseId is empty', () => {
+ const { result } = renderHook(() => useTeamMembers('', {
+ page: 0,
+ pageSize: 25,
+ }), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isPending).toBe(true);
+ expect(result.current.isFetching).toBe(false);
+ expect(result.current.data).toBeUndefined();
+ expect(mockGetTeamMembers).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('useRoles', () => {
+ it('should fetch course roles successfully', async () => {
+ const mockRoles = ['instructor', 'staff', 'beta_testers'];
+
+ mockGetRoles.mockResolvedValue(mockRoles);
+
+ const { result } = renderHook(() => useRoles('course-v1:org+course+run'), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ });
+
+ expect(result.current.data).toEqual(mockRoles);
+ expect(mockGetRoles).toHaveBeenCalledWith('course-v1:org+course+run');
+ });
+
+ it('should handle error when fetching roles fails', async () => {
+ const mockError = new Error('Failed to fetch roles');
+ mockGetRoles.mockRejectedValue(mockError);
+
+ const { result } = renderHook(() => useRoles('course-v1:org+course+run'), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isError).toBe(true);
+ });
+
+ expect(result.current.error).toEqual(mockError);
+ });
+
+ it('should be disabled when courseId is empty', () => {
+ const { result } = renderHook(() => useRoles(''), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isPending).toBe(true);
+ expect(result.current.isFetching).toBe(false);
+ expect(result.current.data).toBeUndefined();
+ expect(mockGetRoles).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/courseTeam/data/apiHook.ts b/src/courseTeam/data/apiHook.ts
new file mode 100644
index 00000000..bd7c925c
--- /dev/null
+++ b/src/courseTeam/data/apiHook.ts
@@ -0,0 +1,20 @@
+import { useQuery } from '@tanstack/react-query';
+import { getRoles, getTeamMembers } from './api';
+import { CourseTeamMemberQueryParams } from '../types';
+import { courseTeamQueryKeys } from './queryKeys';
+
+export const useTeamMembers = (courseId: string, params: CourseTeamMemberQueryParams) => (
+ useQuery({
+ queryKey: courseTeamQueryKeys.byCoursePaginated(courseId, params),
+ queryFn: () => getTeamMembers(courseId, params),
+ enabled: !!courseId,
+ })
+);
+
+export const useRoles = (courseId: string) => (
+ useQuery({
+ queryKey: courseTeamQueryKeys.roles(courseId),
+ queryFn: () => getRoles(courseId),
+ enabled: !!courseId,
+ })
+);
diff --git a/src/courseTeam/data/queryKeys.ts b/src/courseTeam/data/queryKeys.ts
new file mode 100644
index 00000000..dd3cd5e7
--- /dev/null
+++ b/src/courseTeam/data/queryKeys.ts
@@ -0,0 +1,18 @@
+import { appId } from '../../constants';
+import { CourseTeamMemberQueryParams } from '../types';
+
+export const courseTeamQueryKeys = {
+ all: [appId, 'courseTeam'] as const,
+ byCourse: (courseId: string) => [...courseTeamQueryKeys.all, courseId] as const,
+ byCoursePaginated: (
+ courseId: string,
+ params: CourseTeamMemberQueryParams
+ ) => [
+ ...courseTeamQueryKeys.byCourse(courseId),
+ params.page,
+ params.pageSize,
+ params.emailOrUsername || '',
+ params.role || ''
+ ] as const,
+ roles: (courseId: string) => [...courseTeamQueryKeys.byCourse(courseId), 'roles'] as const,
+};
diff --git a/src/courseTeam/messages.ts b/src/courseTeam/messages.ts
new file mode 100644
index 00000000..2f7365a9
--- /dev/null
+++ b/src/courseTeam/messages.ts
@@ -0,0 +1,56 @@
+import { defineMessages } from '@openedx/frontend-base';
+
+const messages = defineMessages({
+ courseTeamTitle: {
+ id: 'instruct.courseTeam.page.title',
+ defaultMessage: 'Course Team Management',
+ description: 'Title for the course team page',
+ },
+ addTeamMember: {
+ id: 'instruct.courseTeam.addTeamMember',
+ defaultMessage: 'Add Team Member',
+ description: 'Button label for adding a team member',
+ },
+ membersTab: {
+ id: 'instruct.courseTeam.membersTab',
+ defaultMessage: 'Members',
+ description: 'Tab title for course team members',
+ },
+ rolesTab: {
+ id: 'instruct.courseTeam.rolesTab',
+ defaultMessage: 'Roles',
+ description: 'Tab title for course team roles',
+ },
+ username: {
+ id: 'instruct.courseTeam.username',
+ defaultMessage: 'Username',
+ description: 'Column header for team member username',
+ },
+ email: {
+ id: 'instruct.courseTeam.email',
+ defaultMessage: 'Email',
+ description: 'Column header for team member email',
+ },
+ role: {
+ id: 'instruct.courseTeam.role',
+ defaultMessage: 'Role',
+ description: 'Column header for team member role',
+ },
+ actions: {
+ id: 'instruct.courseTeam.actions',
+ defaultMessage: 'Actions',
+ description: 'Column header for team member actions',
+ },
+ edit: {
+ id: 'instruct.courseTeam.edit',
+ defaultMessage: 'Edit',
+ description: 'Button label for editing a team member',
+ },
+ noTeamMembers: {
+ id: 'instruct.courseTeam.noTeamMembers',
+ defaultMessage: 'No team members found.',
+ description: 'Message displayed when there are no team members',
+ },
+});
+
+export default messages;
diff --git a/src/courseTeam/types.ts b/src/courseTeam/types.ts
new file mode 100644
index 00000000..e016469e
--- /dev/null
+++ b/src/courseTeam/types.ts
@@ -0,0 +1,12 @@
+export interface CourseTeamMember {
+ username: string,
+ email: string,
+ role: string,
+};
+
+export interface CourseTeamMemberQueryParams {
+ page: number,
+ pageSize: number,
+ emailOrUsername?: string,
+ role?: string,
+};