Skip to content

Commit

Permalink
Merge pull request #503 from IABTechLab/ajy-UID2-3989-Always-provide-…
Browse files Browse the repository at this point in the history
…participantId-in-API-calls

Always provide participantId in API calls
  • Loading branch information
alex-yau-ttd committed Aug 25, 2024
2 parents e3f8ebd + f84cd6f commit 2de78d0
Show file tree
Hide file tree
Showing 22 changed files with 181 additions and 235 deletions.
2 changes: 1 addition & 1 deletion src/api/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class UserController {

@httpPut('/current/acceptTerms')
public async acceptTerms(@request() req: UserRequest, @response() res: Response): Promise<void> {
// TODO: This just gets the user's first participant, but it will need to get the currently selected participant as part of UID2-2822
// TODO: This just gets the user's first participant, but it will need to get the currently selected participant as part of UID2-3989
const currentParticipantId = req.user?.participants?.[0].id;
const participant = currentParticipantId
? await Participant.query().findById(currentParticipantId)
Expand Down
42 changes: 10 additions & 32 deletions src/api/middleware/participantsMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ import { z } from 'zod';
import { Participant } from '../entities/Participant';
import { getTraceId } from '../helpers/loggingHelpers';
import { ParticipantRequest } from '../services/participantsService';
import { findUserByEmail } from '../services/usersService';
import { isUid2Support, isUserBelongsToParticipant } from './usersMiddleware';

const idParser = z.object({
participantId: z.coerce.number(),
});

const hasParticipantAccess = async (req: ParticipantRequest, res: Response, next: NextFunction) => {
const enrichParticipant = async (req: ParticipantRequest, res: Response, next: NextFunction) => {
const { participantId } = idParser.parse(req.params);
const traceId = getTraceId(req);
const participant = await Participant.query().findById(participantId).withGraphFetched('types');
if (!participant) {
return res.status(404).send([{ message: 'The participant cannot be found.' }]);
}
req.participant = participant;
return next();
};

const verifyUserAccessToParticipant = async (req: ParticipantRequest, res: Response) => {
const { participantId } = idParser.parse(req.params);
const traceId = getTraceId(req);
const userEmail = req.auth?.payload?.email as string;
const isUserUid2Support = await isUid2Support(userEmail);

Expand All @@ -28,39 +32,13 @@ const hasParticipantAccess = async (req: ParticipantRequest, res: Response, next
if (!canUserAccessParticipant) {
return res.status(403).send([{ message: 'You do not have permission to that participant.' }]);
}

req.participant = participant;
return next();
};

const enrichCurrentParticipant = async (
req: ParticipantRequest,
res: Response,
next: NextFunction
) => {
const userEmail = req.auth?.payload?.email as string;
const user = await findUserByEmail(userEmail);
if (!user) {
return res.status(404).send([{ message: 'The user cannot be found.' }]);
}
// TODO: This just gets the user's first participant, but it will need to get the currently selected participant as part of UID2-2822
const participant = user.participants?.[0];

if (!participant) {
return res.status(404).send([{ message: 'The participant cannot be found.' }]);
}
req.participant = participant;
return next();
};

export const checkParticipantId = async (
export const verifyAndEnrichParticipant = async (
req: ParticipantRequest,
res: Response,
next: NextFunction
) => {
// TODO: Remove support for 'current' in UID2-2822
if (req.params.participantId === 'current') {
return enrichCurrentParticipant(req, res, next);
}
return hasParticipantAccess(req, res, next);
await verifyUserAccessToParticipant(req, res);
await enrichParticipant(req, res, next);
};
155 changes: 57 additions & 98 deletions src/api/middleware/tests/participantsMiddleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '../../../testHelpers/apiTestHelpers';
import { UserRoleId } from '../../entities/UserRole';
import { ParticipantRequest } from '../../services/participantsService';
import { checkParticipantId } from '../participantsMiddleware';
import { verifyAndEnrichParticipant } from '../participantsMiddleware';

const createParticipantRequest = (
email: string,
Expand All @@ -27,7 +27,7 @@ const createParticipantRequest = (
} as unknown as ParticipantRequest;
};

describe('Participant Service Tests', () => {
describe('Participant Middleware Tests', () => {
let knex: Knex;
let next: NextFunction;
let res: Response;
Expand All @@ -37,112 +37,71 @@ describe('Participant Service Tests', () => {
next = jest.fn();
({ res } = createResponseObject());
});
describe('checkParticipantId middleware', () => {
describe('when participantId is specified', () => {
it('should call next if participantId is valid and user belongs to participant', async () => {
const relatedParticipant = await createParticipant(knex, {});
const relatedUser = await createUser({
participantToRoles: [{ participantId: relatedParticipant.id }],
});
const participantRequest = createParticipantRequest(
relatedUser.email,
relatedParticipant.id
);

await checkParticipantId(participantRequest, res, next);

expect(res.status).not.toHaveBeenCalled();
expect(next).toHaveBeenCalled();
});
it('should call next if user is UID2 support, even if user does not belong to participant', async () => {
const firstParticipant = await createParticipant(knex, {});
const secondParticipant = await createParticipant(knex, {});
const uid2SupportUser = await createUser({
participantToRoles: [
{ participantId: firstParticipant.id, userRoleId: UserRoleId.UID2Support },
],
});
const participantRequest = createParticipantRequest(
uid2SupportUser.email,
secondParticipant.id
);
it('should call next if participantId is valid and user belongs to participant', async () => {
const relatedParticipant = await createParticipant(knex, {});
const relatedUser = await createUser({
participantToRoles: [{ participantId: relatedParticipant.id }],
});
const participantRequest = createParticipantRequest(relatedUser.email, relatedParticipant.id);

await checkParticipantId(participantRequest, res, next);
await verifyAndEnrichParticipant(participantRequest, res, next);

expect(res.status).not.toHaveBeenCalled();
expect(next).toHaveBeenCalled();
});
expect(res.status).not.toHaveBeenCalled();
expect(next).toHaveBeenCalled();
});
it('should call next if user is UID2 support, even if user does not belong to participant', async () => {
const firstParticipant = await createParticipant(knex, {});
const secondParticipant = await createParticipant(knex, {});
const uid2SupportUser = await createUser({
participantToRoles: [
{ participantId: firstParticipant.id, userRoleId: UserRoleId.UID2Support },
],
});
const participantRequest = createParticipantRequest(
uid2SupportUser.email,
secondParticipant.id
);

it('should return 404 if participant is not found', async () => {
const relatedParticipant = await createParticipant(knex, {});
const relatedUser = await createUser({
participantToRoles: [{ participantId: relatedParticipant.id }],
});
const nonExistentParticipantId = 2;
const participantRequest = createParticipantRequest(
relatedUser.email,
nonExistentParticipantId
);
await verifyAndEnrichParticipant(participantRequest, res, next);

await checkParticipantId(participantRequest, res, next);
expect(res.status).not.toHaveBeenCalled();
expect(next).toHaveBeenCalled();
});

expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith([{ message: 'The participant cannot be found.' }]);
});
it('should return 404 if participant is not found', async () => {
const relatedParticipant = await createParticipant(knex, {});
const relatedUser = await createUser({
participantToRoles: [{ participantId: relatedParticipant.id }],
});
const nonExistentParticipantId = 2;
const participantRequest = createParticipantRequest(
relatedUser.email,
nonExistentParticipantId
);

it('should return 403 if user does not have access to participant', async () => {
const firstParticipant = await createParticipant(knex, {});
const secondParticipant = await createParticipant(knex, {});
const relatedUser = await createUser({
participantToRoles: [
{
participantId: secondParticipant.id,
},
],
});
const participantRequest = createParticipantRequest(relatedUser.email, firstParticipant.id);
await verifyAndEnrichParticipant(participantRequest, res, next);

await checkParticipantId(participantRequest, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith([{ message: 'The participant cannot be found.' }]);
});

expect(res.status).toHaveBeenCalledWith(403);
expect(res.send).toHaveBeenCalledWith([
{ message: 'You do not have permission to that participant.' },
]);
});
it('should return 403 if user does not have access to participant', async () => {
const firstParticipant = await createParticipant(knex, {});
const secondParticipant = await createParticipant(knex, {});
const relatedUser = await createUser({
participantToRoles: [
{
participantId: secondParticipant.id,
},
],
});
// TODO: these will change in UID2-2822
describe(`when participantId is 'current'`, () => {
it('should call next if user has a valid participant', async () => {
const relatedParticipant = await createParticipant(knex, {});
const relatedUser = await createUser({
participantToRoles: [
{
participantId: relatedParticipant.id,
},
],
});
const participantRequest = createParticipantRequest(relatedUser.email, 'current');

await checkParticipantId(participantRequest, res, next);
const participantRequest = createParticipantRequest(relatedUser.email, firstParticipant.id);

expect(res.status).not.toHaveBeenCalled();
expect(next).toHaveBeenCalled();
});
it('should return 404 is user is not found', async () => {
const participantRequest = createParticipantRequest('doesNotMatter@example.com', 'current');
await verifyAndEnrichParticipant(participantRequest, res, next);

await checkParticipantId(participantRequest, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith([{ message: 'The user cannot be found.' }]);
});
it('should return 404 is participant is not found', async () => {
const relatedUser = await createUser({});
const participantRequest = createParticipantRequest(relatedUser.email, 'current');

await checkParticipantId(participantRequest, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith([{ message: 'The participant cannot be found.' }]);
});
});
expect(res.status).toHaveBeenCalledWith(403);
expect(res.send).toHaveBeenCalledWith([
{ message: 'You do not have permission to that participant.' },
]);
});
});
2 changes: 1 addition & 1 deletion src/api/middleware/tests/usersMiddleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const createUserRequest = (email: string, userId: string | number): UserRequest
} as unknown as UserRequest;
};

describe('User Service Tests', () => {
describe('User Middleware Tests', () => {
let knex: Knex;
let next: NextFunction;
let res: Response;
Expand Down
2 changes: 1 addition & 1 deletion src/api/middleware/usersMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const enrichWithUserFromParams = async (
const requestingUserEmail = req.auth?.payload?.email as string;
const isRequestingUserUid2Support = await isUid2Support(requestingUserEmail);

// TODO: This just gets the user's first participant, but it will need to get the currently selected participant as part of UID2-2822
// TODO: This just gets the user's first participant, but it will need to get the currently selected participant as part of UID2-3989
const firstParticipant = user.participants?.[0] as Participant;

const canRequestingUserAccessParticipant =
Expand Down
10 changes: 8 additions & 2 deletions src/api/routers/participants/participantsRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { siteIdNotSetError } from '../../helpers/errorHelpers';
import { getTraceId } from '../../helpers/loggingHelpers';
import { getKcAdminClient } from '../../keycloakAdminClient';
import { isApproverCheck } from '../../middleware/approversMiddleware';
import { checkParticipantId } from '../../middleware/participantsMiddleware';
import { verifyAndEnrichParticipant } from '../../middleware/participantsMiddleware';
import { enrichCurrentUser } from '../../middleware/usersMiddleware';
import {
addKeyPair,
Expand Down Expand Up @@ -210,7 +210,13 @@ export function createParticipantsRouter() {

participantsRouter.put('/', createParticipant);

participantsRouter.use('/:participantId', checkParticipantId);
participantsRouter.use('/:participantId', verifyAndEnrichParticipant);

participantsRouter.get('/:participantId', async (req: ParticipantRequest, res: Response) => {
const { participant } = req;

return res.status(200).json(participant);
});

const invitationParser = z.object({
firstName: z.string(),
Expand Down
2 changes: 1 addition & 1 deletion src/api/services/auditTrailService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const constructAuditTrailObject = (
event: AuditTrailEvents,
eventData: unknown
): InsertAuditTrailDTO => {
// TODO: This just gets the user's first participant, but it will need to get the currently selected participant as part of UID2-2822
// TODO: This just gets the user's first participant, but it will need to get the currently selected participant as part of UID2-3989
const currentParticipant = user.participants?.[0];
return {
userId: user.id,
Expand Down
2 changes: 1 addition & 1 deletion src/api/services/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class UserService {
}

public async getCurrentParticipant(req: UserRequest) {
// TODO: This just gets the user's first participant, but it will need to get the currently selected participant as part of UID2-2822
// TODO: This just gets the user's first participant, but it will need to get the currently selected participant as part of UID2-3989
const currentParticipant = req.user?.participants?.[0];
const currentSite = !currentParticipant?.siteId
? undefined
Expand Down
13 changes: 9 additions & 4 deletions src/web/contexts/ParticipantProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation, useNavigate, useParams } from 'react-router-dom';

import { ParticipantDTO, ParticipantStatus } from '../../api/entities/Participant';
import { Loading } from '../components/Core/Loading/Loading';
import { GetCurrentUsersParticipant } from '../services/participant';
import { GetSelectedParticipant, GetUsersDefaultParticipant } from '../services/participant';
import { ApiError } from '../utils/apiError';
import { useAsyncThrowError } from '../utils/errorHandler';
import { parseParticipantId } from '../utils/urlHelpers';
import { CurrentUserContext } from './CurrentUserProvider';

type ParticipantWithSetter = {
Expand All @@ -25,6 +26,8 @@ function ParticipantProvider({ children }: { children: ReactNode }) {
const navigate = useNavigate();
const throwError = useAsyncThrowError();
const user = LoggedInUser?.user || null;
const { participantId } = useParams();
const parsedParticipantId = parseParticipantId(participantId);

useEffect(() => {
if (
Expand All @@ -41,7 +44,9 @@ function ParticipantProvider({ children }: { children: ReactNode }) {
setIsLoading(true);
try {
if (user) {
const p = await GetCurrentUsersParticipant();
const p = parsedParticipantId
? await GetSelectedParticipant(parsedParticipantId)
: await GetUsersDefaultParticipant();
setParticipant(p);
}
} catch (e: unknown) {
Expand All @@ -51,7 +56,7 @@ function ParticipantProvider({ children }: { children: ReactNode }) {
}
};
if (!participant) loadParticipant();
}, [user, participant, throwError]);
}, [user, participant, throwError, parsedParticipantId]);

const participantContext = useMemo(
() => ({
Expand Down
Loading

0 comments on commit 2de78d0

Please sign in to comment.