Skip to content

Commit

Permalink
#137 [FEAT]: Embed user actions and filter sensitive fields in multip…
Browse files Browse the repository at this point in the history
…le endpoints responses
  • Loading branch information
IosBonaldi committed Jan 23, 2025
1 parent dcc357c commit d233578
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 53 deletions.
85 changes: 72 additions & 13 deletions src/controllers/applicationAnswerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,14 @@ const validateAnswers = async (itemAnswerGroups: any, applicationId: number) =>
}
}
};

const dropSensitiveFields = (applicationAnswer: any) => {
const filteredApplicationAnswer = { ...applicationAnswer };
delete filteredApplicationAnswer.userId;
delete filteredApplicationAnswer.application;
return filteredApplicationAnswer;
};

const fields = {
id: true,
date: true,
Expand All @@ -266,8 +274,8 @@ const fields = {
createdAt: true,
updatedAt: true,
approved: true,
coordinateId: true,
coordinate: { select: { latitude: true, longitude: true } },
application: { select: { applierId: true, protocol: { select: { creatorId: true, managers: { select: { id: true } } } } } },
itemAnswerGroups: {
select: {
id: true,
Expand Down Expand Up @@ -422,8 +430,15 @@ export const createApplicationAnswer = async (req: Request, res: Response) => {
select: fields,
});
});

res.status(201).json({ message: 'Application answer created.', data: createdApplicationAnswer });
// Embed user actions in the response
const processedApplicationAnswer = {
...createdApplicationAnswer,
actions: await getApplicationAnswerActions(user, createdApplicationAnswer, undefined),
};
// Filter sensitive fields from the response
const filteredApplicationAnswer = dropSensitiveFields(processedApplicationAnswer);

res.status(201).json({ message: 'Application answer created.', data: filteredApplicationAnswer });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) if (existsSync(file.path)) unlinkSync(file.path);
Expand Down Expand Up @@ -640,7 +655,15 @@ export const updateApplicationAnswer = async (req: Request, res: Response): Prom
return await prisma.applicationAnswer.findUnique({ where: { id: applicationAnswerId }, select: fields });
});

res.status(200).json({ message: 'Application answer updated.', data: upsertedApplicationAnswer });
// Embed user actions in the response
const processedApplicationAnswer = {
...upsertedApplicationAnswer,
actions: await getApplicationAnswerActions(user, upsertedApplicationAnswer, undefined),
};
// Filter sensitive fields from the response
const filteredApplicationAnswer = dropSensitiveFields(processedApplicationAnswer);

res.status(200).json({ message: 'Application answer updated.', data: filteredApplicationAnswer });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) if (existsSync(file.path)) unlinkSync(file.path);
Expand All @@ -655,9 +678,20 @@ export const getAllApplicationAnswers = async (req: Request, res: Response): Pro
// Check if user is allowed to get all application answers
await checkAuthorization(user, undefined, undefined, 'getAll');
// Prisma operation
const applicationAnswers: ApplicationAnswer[] = await prismaClient.applicationAnswer.findMany({ select: fields });
const applicationAnswers = await prismaClient.applicationAnswer.findMany({ select: fields });
// Embed user actions in the response
const processedApplicationAnswers = await Promise.all(
applicationAnswers.map(async (applicationAnswer) => {
return {
...applicationAnswer,
actions: await getApplicationAnswerActions(user, applicationAnswer, undefined),
};
})
);
// Filter sensitive fields from the response
const filteredApplicationAnswers = processedApplicationAnswers.map(dropSensitiveFields);

res.status(200).json({ message: 'All application answers found.', data: applicationAnswers });
res.status(200).json({ message: 'All application answers found.', data: filteredApplicationAnswers });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand All @@ -670,12 +704,23 @@ export const getMyApplicationAnswers = async (req: Request, res: Response): Prom
// Check if user is allowed to get their application answers
await checkAuthorization(user, undefined, undefined, 'getMy');
// Prisma operation
const applicationAnswers: ApplicationAnswer[] = await prismaClient.applicationAnswer.findMany({
const applicationAnswers = await prismaClient.applicationAnswer.findMany({
where: { userId: user.id },
select: fields,
});
// Embed user actions in the response
const processedApplicationAnswers = await Promise.all(
applicationAnswers.map(async (applicationAnswer) => {
return {
...applicationAnswer,
actions: await getApplicationAnswerActions(user, applicationAnswer, undefined),
};
})
);
// Filter sensitive fields from the response
const filteredApplicationAnswers = processedApplicationAnswers.map(dropSensitiveFields);

res.status(200).json({ message: 'My application answers found.', data: applicationAnswers });
res.status(200).json({ message: 'My application answers found.', data: filteredApplicationAnswers });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand All @@ -690,12 +735,19 @@ export const getApplicationAnswer = async (req: Request, res: Response): Promise
// Check if user is allowed to view this application answer
await checkAuthorization(user, applicationAnswerId, undefined, 'get');
// Prisma operation
const applicationAnswer: ApplicationAnswer = await prismaClient.applicationAnswer.findUniqueOrThrow({
const applicationAnswer = await prismaClient.applicationAnswer.findUniqueOrThrow({
where: { id: applicationAnswerId },
select: fields,
});

res.status(200).json({ message: 'Application answer found.', data: applicationAnswer });
// Embed user actions in the response
const processedApplicationAnswer = {
...applicationAnswer,
actions: await getApplicationAnswerActions(user, applicationAnswer, undefined),
};
// Filter sensitive fields from the response
const filteredApplicationAnswer = dropSensitiveFields(processedApplicationAnswer);

res.status(200).json({ message: 'Application answer found.', data: filteredApplicationAnswer });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand All @@ -715,8 +767,15 @@ export const approveApplicationAnswer = async (req: Request, res: Response): Pro
data: { approved: true },
select: fields,
});

res.status(200).json({ message: 'Application answer approved.', data: approvedApplicationAnswer });
// Embed user actions in the response
const processedApplicationAnswer = {
...approvedApplicationAnswer,
actions: await getApplicationAnswerActions(user, approvedApplicationAnswer, undefined),
};
// Filter sensitive fields from the response
const filteredApplicationAnswer = dropSensitiveFields(processedApplicationAnswer);

res.status(200).json({ message: 'Application answer approved.', data: filteredApplicationAnswer });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand Down
114 changes: 92 additions & 22 deletions src/controllers/applicationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const getProtocolUserRoles = async (user: User, protocol: any, protocolId: numbe
return { creator, manager, applier, viewer, answersViewer };
};

const getApplicationUserActions = async (user: User, application: Application, applicationId: number | undefined) => {
const getApplicationUserActions = async (user: User, application: any, applicationId: number | undefined) => {
const roles = await getApplicationUserRoles(user, application, applicationId);

// Only protocol managers/applier/protocol creator can perform update operations on applications
Expand Down Expand Up @@ -179,13 +179,34 @@ const validateVisibility = async (
}
};

const dropSensitiveFields = (application: any) => {
const filteredApplication = { ...application };
delete filteredApplication.viewersUser;
delete filteredApplication.viewersClassroom;
delete filteredApplication.answersViewersUser;
delete filteredApplication.answersViewersClassroom;
delete filteredApplication.protocol.managers;
delete filteredApplication.protocol.creatorId;
return filteredApplication;
};

const dropUnapprovedAnswers = (application: any) => {
const filteredApplication = { ...application };
filteredApplication.answers = filteredApplication.answers.filter((answer: any) => answer.approved);
return filteredApplication;
};

const fields = {
id: true,
protocol: { select: { id: true, title: true, description: true } },
visibility: true,
answersVisibility: true,
keepLocation: true,
applier: { select: { id: true, username: true, institutionId: true } },
viewersClassroom: { select: { users: { select: { id: true } } } },
viewersUser: { select: { id: true } },
answersViewersClassroom: { select: { users: { select: { id: true } } } },
answersViewersUser: { select: { id: true } },
protocol: { select: { id: true, title: true, description: true, creatorId: true, managers: { select: { id: true } } } },
createdAt: true,
updatedAt: true,
};
Expand Down Expand Up @@ -316,7 +337,15 @@ export const createApplication = async (req: Request, res: Response) => {
select: fieldsWViewers,
});

res.status(201).json({ message: 'Application created.', data: createdApplication });
// Embed user actions in the response
const processedApplication = {
...createdApplication,
actions: await getApplicationUserActions(user, createdApplication, undefined),
};
// Filter sensitive fields
const filteredApplication = dropSensitiveFields(createdApplication);

res.status(201).json({ message: 'Application created.', data: filteredApplication });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand Down Expand Up @@ -370,7 +399,15 @@ export const updateApplication = async (req: Request, res: Response): Promise<vo
select: fieldsWViewers,
});

res.status(200).json({ message: 'Application updated.', data: updatedApplication });
// Embed user actions in the response
const processedApplication = {
...updatedApplication,
actions: await getApplicationUserActions(user, updatedApplication, undefined),
};
// Filter sensitive fields
const filteredApplication = dropSensitiveFields(updatedApplication);

res.status(200).json({ message: 'Application updated.', data: filteredApplication });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand All @@ -388,7 +425,17 @@ export const getMyApplications = async (req: Request, res: Response): Promise<vo
select: fieldsWViewers,
});

res.status(200).json({ message: 'All your applications found.', data: applications });
// Embed user actions in the response
const processedApplications = await Promise.all(
applications.map(async (application) => ({
...application,
actions: await getApplicationUserActions(user, application, application.id),
}))
);
// Filter sensitive fields
const filteredApplications = processedApplications.map((application) => dropSensitiveFields(application));

res.status(200).json({ message: 'All your applications found.', data: filteredApplications });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand Down Expand Up @@ -416,7 +463,17 @@ export const getVisibleApplications = async (req: Request, res: Response): Promi
select: fields,
});

res.status(200).json({ message: 'All visible applications found.', data: applications });
// Embed user actions in the response
const processedApplications = await Promise.all(
applications.map(async (application) => ({
...application,
actions: await getApplicationUserActions(user, application, application.id),
}))
);
// Filter sensitive fields
const filteredApplications = processedApplications.map((application) => dropSensitiveFields(application));

res.status(200).json({ message: 'All visible applications found.', data: filteredApplications });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand All @@ -433,7 +490,17 @@ export const getAllApplications = async (req: Request, res: Response): Promise<v
select: fieldsWViewers,
});

res.status(200).json({ message: 'All applications found.', data: applications });
// Embed user actions in the response
const processedApplications = await Promise.all(
applications.map(async (application) => ({
...application,
actions: await getApplicationUserActions(user, application, application.id),
}))
);
// Filter sensitive fields
const filteredApplications = processedApplications.map((application) => dropSensitiveFields(application));

res.status(200).json({ message: 'All applications found.', data: filteredApplications });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand All @@ -460,19 +527,15 @@ export const getApplication = async (req: Request, res: Response): Promise<void>
},
select: fieldsWViewers,
});
// Filter the application based on the user's role
const visibleApplication =
user.role === UserRole.ADMIN || application.applier.id === user.id
? application
: {
...application,
viewersUser: undefined,
viewersClassroom: undefined,
answersViewersUser: undefined,
answersViewersClassroom: undefined,
};

res.status(200).json({ message: 'Application found.', data: visibleApplication });
// Embed user actions in the response
const processedApplication = {
...application,
actions: await getApplicationUserActions(user, application, applicationId),
};
// Filter sensitive fields
const filteredApplication = processedApplication.actions.toUpdate ? application : dropSensitiveFields(application);

res.status(200).json({ message: 'Application found.', data: filteredApplication });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand All @@ -491,8 +554,15 @@ export const getApplicationWithProtocol = async (req: Request, res: Response): P
where: { id: applicationId },
select: fieldsWProtocol,
});

res.status(200).json({ message: 'Application with protocol found.', data: application });
// Embed user actions in the response
const processedApplication = {
...application,
actions: await getApplicationUserActions(user, application, applicationId),
};
// Filter sensitive fields
const filteredApplication = processedApplication;

res.status(200).json({ message: 'Application with protocol found.', data: filteredApplication });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
Expand Down
Loading

0 comments on commit d233578

Please sign in to comment.