Skip to content

Commit 33129b9

Browse files
authored
Merge pull request #598 from IABTechLab/sas-UID2-5022-user-management-page
user management page
2 parents 1bf32be + 8217e18 commit 33129b9

File tree

15 files changed

+382
-9
lines changed

15 files changed

+382
-9
lines changed

src/api/configureApi.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
getTraceId,
3434
} from './helpers/loggingHelpers';
3535
import makeMetricsApiMiddleware from './middleware/metrics';
36+
import { createManagementRouter } from './routers/managementRouter';
3637
import { createParticipantsRouter } from './routers/participants/participantsRouter';
3738
import { createSitesRouter } from './routers/sitesRouter';
3839
import { createUsersRouter } from './routers/usersRouter';
@@ -88,6 +89,7 @@ export function configureAndStartApi(useMetrics: boolean = true, portNumber: num
8889
participantsRouter: createParticipantsRouter(),
8990
usersRouter: createUsersRouter(),
9091
sitesRouter: createSitesRouter(),
92+
managementRouter: createManagementRouter(),
9193
};
9294
const router = routers.rootRouter;
9395
app.use((req, res, next) => {
@@ -153,6 +155,7 @@ export function configureAndStartApi(useMetrics: boolean = true, portNumber: num
153155
router.use('/users', routers.usersRouter);
154156
router.use('/participants', routers.participantsRouter.router);
155157
router.use('/sites', routers.sitesRouter);
158+
router.use('/manage', routers.managementRouter);
156159
router.get('/health', async (_req, res) => {
157160
// TODO: More robust health check information
158161
res.json({ node: process.version });

src/api/middleware/tests/userRoleMiddleware.spec.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
createUserParticipantRequest,
1010
} from '../../../testHelpers/apiTestHelpers';
1111
import { UserRoleId } from '../../entities/UserRole';
12-
import { isAdminOrUid2SupportCheck, isUid2SupportCheck } from '../userRoleMiddleware';
12+
import {
13+
isAdminOrUid2SupportCheck,
14+
isSuperUserCheck,
15+
isUid2SupportCheck,
16+
} from '../userRoleMiddleware';
1317

1418
describe('User Role Middleware Tests', () => {
1519
let knex: Knex;
@@ -47,6 +51,32 @@ describe('User Role Middleware Tests', () => {
4751
expect(next).not.toHaveBeenCalled();
4852
});
4953
});
54+
describe('SuperUser check', () => {
55+
it('should call next if requesting user has the SuperUser role', async () => {
56+
const participant = await createParticipant(knex, {});
57+
const user = await createUser({
58+
participantToRoles: [{ participantId: participant.id, userRoleId: UserRoleId.SuperUser }],
59+
});
60+
const userParticipantRequest = createUserParticipantRequest(user.email, participant, user.id);
61+
62+
await isSuperUserCheck(userParticipantRequest, res, next);
63+
64+
expect(res.status).not.toHaveBeenCalled();
65+
expect(next).toHaveBeenCalled();
66+
});
67+
it('should return 403 if requesting user does not have SuperUser role', async () => {
68+
const participant = await createParticipant(knex, {});
69+
const user = await createUser({
70+
participantToRoles: [{ participantId: participant.id, userRoleId: UserRoleId.UID2Support }],
71+
});
72+
const userParticipantRequest = createUserParticipantRequest(user.email, participant, user.id);
73+
74+
await isSuperUserCheck(userParticipantRequest, res, next);
75+
76+
expect(res.status).toHaveBeenCalledWith(403);
77+
expect(next).not.toHaveBeenCalled();
78+
});
79+
});
5080
describe('Admin Role or UID2 Support check', () => {
5181
it.each([
5282
{ role: UserRoleId.Admin, description: 'Admin Role for the participant' },

src/api/routers/businessContactsRouter.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import express, { Response } from 'express';
22

3-
import { BusinessContactSchema } from '../entities/BusinessContact';
3+
import { BusinessContact, BusinessContactSchema } from '../entities/BusinessContact';
44
import {
55
BusinessContactRequest,
66
hasBusinessContactAccess,
@@ -18,14 +18,16 @@ export function createBusinessContactsRouter() {
1818

1919
businessContactsRouter.get('/', async (req: ParticipantRequest, res: Response) => {
2020
const { participant } = req;
21-
const businessContacts = await participant!.$relatedQuery('businessContacts');
21+
const businessContacts = await BusinessContact.query().where('participantId', participant?.id!);
2222
return res.status(200).json(businessContacts);
2323
});
2424

2525
businessContactsRouter.post('/', async (req: ParticipantRequest, res: Response) => {
2626
const data = BusinessContactsDTO.parse(req.body);
2727
const { participant } = req;
28-
const newContact = await participant!.$relatedQuery('businessContacts').insert(data);
28+
const newContact = await BusinessContact.query()
29+
.where('participantId', participant?.id!)
30+
.insert({ ...data, participantId: participant?.id! });
2931
return res.status(201).json(newContact);
3032
});
3133

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import express, { Response } from 'express';
2+
3+
import { isSuperUserCheck } from '../middleware/userRoleMiddleware';
4+
import { ParticipantRequest } from '../services/participantsService';
5+
import { getAllUsersList } from '../services/usersService';
6+
7+
const handleGetAllUsers = async (req: ParticipantRequest, res: Response) => {
8+
const userList = await getAllUsersList();
9+
return res.status(200).json(userList);
10+
};
11+
12+
export function createManagementRouter() {
13+
const managementRouter = express.Router();
14+
15+
managementRouter.get('/users', isSuperUserCheck, handleGetAllUsers);
16+
17+
return managementRouter;
18+
}

src/api/services/usersService.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,8 @@ export const inviteUserToParticipant = async (
198198
await createUserInPortal(userPartial, participant!.id, userRoleId);
199199
}
200200
};
201+
202+
export const getAllUsersList = async () => {
203+
const userList = await User.query().where('deleted', 0).orderBy('email');
204+
return userList;
205+
};

src/database/migrations/20250228040900_CreateSuperUserRole.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Knex } from 'knex';
22

33
export async function up(knex: Knex): Promise<void> {
44
await knex('userRoles').insert({
5-
id: 4,
65
roleName: 'Super User',
76
});
87
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.user-management-item {
2+
line-height: 2;
3+
4+
.user-item-name-cell {
5+
display: flex;
6+
align-items: center;
7+
}
8+
9+
.approve-button {
10+
padding-right: 0;
11+
}
12+
13+
.approver-date {
14+
margin-right: 80px;
15+
}
16+
17+
.approver-name {
18+
margin-right: 40px;
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { UserDTO } from '../../../api/entities/User';
2+
3+
import './UserManagementItem.scss';
4+
5+
type UserManagementItemProps = Readonly<{
6+
user: UserDTO;
7+
}>;
8+
9+
export function UserManagementItem({ user }: UserManagementItemProps) {
10+
return (
11+
<tr className='user-management-item'>
12+
<td>{user.email}</td>
13+
<td>{user.firstName}</td>
14+
<td>{user.lastName}</td>
15+
<td>{user.jobFunction}</td>
16+
<td>{user.acceptedTerms ? 'True' : 'False'}</td>
17+
<td />
18+
<td />
19+
</tr>
20+
);
21+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
.users-table-container {
2+
.users-table {
3+
width: 100%;
4+
5+
tr th {
6+
font-size: 0.875rem;
7+
line-height: 1.5;
8+
}
9+
}
10+
11+
.users-table-header {
12+
display: flex;
13+
justify-content: end;
14+
align-items: baseline;
15+
padding-bottom: 10px;
16+
17+
&-right {
18+
display: flex;
19+
justify-content: right;
20+
}
21+
22+
.users-search-bar-container {
23+
display: flex;
24+
align-items: center;
25+
border-bottom: 2px solid var(--theme-action);
26+
padding: 6px 2px;
27+
::placeholder {
28+
color: var(--theme-search-text);
29+
opacity: 1;
30+
}
31+
}
32+
33+
.users-search-bar-icon {
34+
color: var(--theme-search-text);
35+
height: 16px;
36+
margin-left: 8px;
37+
}
38+
39+
.users-search-bar {
40+
width: 100%;
41+
border: none;
42+
outline: none;
43+
color: var(--theme-search-text);
44+
font-weight: 400;
45+
font-size: 0.75rem;
46+
background: none;
47+
}
48+
}
49+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
3+
import { UserJobFunction } from '../../../api/entities/User';
4+
import UserManagementTable from './UserManagementTable';
5+
6+
const meta: Meta<typeof UserManagementTable> = {
7+
component: UserManagementTable,
8+
title: 'Manage Users/All Users Table',
9+
};
10+
export default meta;
11+
12+
type Story = StoryObj<typeof UserManagementTable>;
13+
14+
export const AllUsers: Story = {
15+
args: {
16+
users: [
17+
{
18+
firstName: 'Test',
19+
lastName: 'User',
20+
email: 'test@user.com',
21+
id: 1,
22+
jobFunction: UserJobFunction.Marketing,
23+
acceptedTerms: true,
24+
},
25+
],
26+
},
27+
};
28+
29+
export const NoUsers: Story = {
30+
args: {
31+
users: [],
32+
},
33+
};

0 commit comments

Comments
 (0)