From b9f9a793910f4132b474bb80d30f16660eafa508 Mon Sep 17 00:00:00 2001 From: sthuray <138075787+sthuray@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:11:07 -0400 Subject: [PATCH 1/6] Implement GET user endpoint --- backend/typescript/rest/userRoutes.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/typescript/rest/userRoutes.ts b/backend/typescript/rest/userRoutes.ts index 4728a31..4e24051 100644 --- a/backend/typescript/rest/userRoutes.ts +++ b/backend/typescript/rest/userRoutes.ts @@ -62,6 +62,8 @@ userRouter.get("/", async (req, res) => { res .status(400) .json({ error: "userId query parameter must be a string." }); + } else if (Number.isNaN(Number(userId))) { + res.status(400).json({ error: "Invalid user ID" }); } else { try { const user = await userService.getUserById(userId); @@ -87,7 +89,11 @@ userRouter.get("/", async (req, res) => { const user = await userService.getUserByEmail(email); res.status(200).json(user); } catch (error: unknown) { - res.status(500).json({ error: getErrorMessage(error) }); + if (error instanceof NotFoundError) { + res.status(404).send(getErrorMessage(error)); + } else { + res.status(500).json({ error: getErrorMessage(error) }); + } } } } From 1a9b9ee24d8810a6a612e6ec2a782b24c547edbb Mon Sep 17 00:00:00 2001 From: sthuray <138075787+sthuray@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:12:33 -0400 Subject: [PATCH 2/6] Implement DELETE user endpoint --- backend/typescript/rest/userRoutes.ts | 36 ++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/backend/typescript/rest/userRoutes.ts b/backend/typescript/rest/userRoutes.ts index 4e24051..bb372f7 100644 --- a/backend/typescript/rest/userRoutes.ts +++ b/backend/typescript/rest/userRoutes.ts @@ -157,6 +157,21 @@ userRouter.delete("/", async (req, res) => { return; } + const accessToken = getAccessToken(req); + if (!accessToken) { + res.status(404).json({ error: "Access token not found" }); + return; + } + + const isAdministrator = await authService.isAuthorizedByRole( + accessToken, + new Set([Role.ADMINISTRATOR]), + ); + if (!isAdministrator) { + res.status(403).json({ error: "Not authorized to delete user" }); + return; + } + if (userId) { if (typeof userId !== "string") { res @@ -166,10 +181,22 @@ userRouter.delete("/", async (req, res) => { res.status(400).json({ error: "Invalid user ID" }); } else { try { + const user: UserDTO = await userService.getUserById(userId); + if (user.status === "Active") { + res.status(400).json({ + error: + "user status must be 'Inactive' or 'Invited' before deletion.", + }); + return; + } await userService.deleteUserById(Number(userId)); res.status(204).send(); } catch (error: unknown) { - res.status(500).json({ error: getErrorMessage(error) }); + if (error instanceof NotFoundError) { + res.status(400).json({ error: getErrorMessage(error) }); + } else { + res.status(500).json({ error: getErrorMessage(error) }); + } } } return; @@ -182,6 +209,13 @@ userRouter.delete("/", async (req, res) => { .json({ error: "email query parameter must be a string." }); } else { try { + const user: UserDTO = await userService.getUserByEmail(email); + if (user.status === "Active") { + res.status(400).json({ + error: "user status must be 'Inactive' or 'Invited' for deletion.", + }); + return; + } await userService.deleteUserByEmail(email); res.status(204).send(); } catch (error: unknown) { From 450976e45c1082fd10a7e96a46e89d244726ab52 Mon Sep 17 00:00:00 2001 From: sthuray <138075787+sthuray@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:13:26 -0400 Subject: [PATCH 3/6] Implement PUT user endpoint --- .../middlewares/validators/userValidators.ts | 24 +++++-- backend/typescript/rest/userRoutes.ts | 69 +++++++++++++++---- backend/typescript/types.ts | 2 +- 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/backend/typescript/middlewares/validators/userValidators.ts b/backend/typescript/middlewares/validators/userValidators.ts index 9e08d29..17b9895 100644 --- a/backend/typescript/middlewares/validators/userValidators.ts +++ b/backend/typescript/middlewares/validators/userValidators.ts @@ -63,16 +63,32 @@ export const updateUserDtoValidator = async ( res: Response, next: NextFunction, ) => { - if (!validatePrimitive(req.body.firstName, "string")) { + if ( + req.body.firstName !== undefined && + req.body.firstName !== null && + !validatePrimitive(req.body.firstName, "string") + ) { return res.status(400).send(getApiValidationError("firstName", "string")); } - if (!validatePrimitive(req.body.lastName, "string")) { + if ( + req.body.lastName !== undefined && + req.body.lastName !== null && + !validatePrimitive(req.body.lastName, "string") + ) { return res.status(400).send(getApiValidationError("lastName", "string")); } - if (!validatePrimitive(req.body.email, "string")) { + if ( + req.body.email !== undefined && + req.body.email !== null && + !validatePrimitive(req.body.email, "string") + ) { return res.status(400).send(getApiValidationError("email", "string")); } - if (!validatePrimitive(req.body.role, "string")) { + if ( + req.body.role !== undefined && + req.body.role !== null && + !validatePrimitive(req.body.role, "string") + ) { return res.status(400).send(getApiValidationError("role", "string")); } if ( diff --git a/backend/typescript/rest/userRoutes.ts b/backend/typescript/rest/userRoutes.ts index bb372f7..c1d003c 100644 --- a/backend/typescript/rest/userRoutes.ts +++ b/backend/typescript/rest/userRoutes.ts @@ -1,6 +1,6 @@ import { Router } from "express"; -import { isAuthorizedByRole } from "../middlewares/auth"; +import { getAccessToken, isAuthorizedByRole } from "../middlewares/auth"; import { createUserDtoValidator, updateUserDtoValidator, @@ -107,7 +107,7 @@ userRouter.post("/", createUserDtoValidator, async (req, res) => { lastName: req.body.lastName, email: req.body.email, role: req.body.role ?? Role.VOLUNTEER, - status: req.body.status ?? UserStatus.ACTIVE, // TODO: make this default to inactive once user registration flow is done + status: UserStatus.INVITED, // TODO: make this default to inactive once user registration flow is done skillLevel: req.body.skillLevel ?? null, canSeeAllLogs: req.body.canSeeAllLogs ?? null, canAssignUsersToTasks: req.body.canSeeAllUsers ?? null, @@ -125,26 +125,65 @@ userRouter.post("/", createUserDtoValidator, async (req, res) => { /* Update the user with the specified userId */ userRouter.put("/:userId", updateUserDtoValidator, async (req, res) => { + const userId = Number(req.params.userId); + if (Number.isNaN(userId)) { + res.status(400).json({ error: "Invalid user ID" }); + return; + } + + const accessToken = getAccessToken(req); + if (!accessToken) { + res.status(404).json({ error: "Access token not found" }); + return; + } + try { - const userId = Number(req.params.userId); - if (Number.isNaN(userId)) { - res.status(400).json({ error: "Invalid user ID" }); + const isBehaviourist = await authService.isAuthorizedByRole( + accessToken, + new Set([Role.ANIMAL_BEHAVIOURIST]), + ); + const behaviouristUpdatableSet = new Set(["skillLevel"]); + if (isBehaviourist) { + const deniedFieldSet = Object.keys(req.body).filter((field) => { + return !behaviouristUpdatableSet.has(field); + }); + if (deniedFieldSet.length > 0) { + const deniedFieldsString = "Not authorized to update field(s): ".concat( + deniedFieldSet.join(", "), + ); + res.status(403).json({ error: deniedFieldsString }); + return; + } + } + } catch (error: unknown) { + if (error instanceof NotFoundError) { + res.status(400).json({ error: getErrorMessage(error) }); + } else { + res.status(500).json({ error: getErrorMessage(error) }); } + } + try { + const user: UserDTO = await userService.getUserById(String(userId)); const updatedUser = await userService.updateUserById(userId, { - firstName: req.body.firstName, - lastName: req.body.lastName, - email: req.body.email, - role: req.body.role, - status: req.body.status, - skillLevel: req.body.skillLevel ?? null, - canSeeAllLogs: req.body.canSeeAllLogs ?? null, - canAssignUsersToTasks: req.body.canSeeAllUsers ?? null, - phoneNumber: req.body.phoneNumber ?? null, + firstName: req.body.firstName ?? user.firstName, + lastName: req.body.lastName ?? user.lastName, + email: req.body.email ?? user.email, + role: req.body.role ?? user.role, + status: req.body.status ?? user.status, + skillLevel: req.body.skillLevel ?? user.skillLevel, + canSeeAllLogs: req.body.canSeeAllLogs ?? user.canSeeAllLogs, + canAssignUsersToTasks: + req.body.canAssignUsersToTasks ?? user.canAssignUsersToTasks, + phoneNumber: req.body.phoneNumber ?? user.phoneNumber, }); res.status(200).json(updatedUser); } catch (error: unknown) { - res.status(500).json({ error: getErrorMessage(error) }); + if (error instanceof NotFoundError) { + res.status(400).json({ error: getErrorMessage(error) }); + } else { + res.status(500).json({ error: getErrorMessage(error) }); + } } }); diff --git a/backend/typescript/types.ts b/backend/typescript/types.ts index daf6250..4648032 100644 --- a/backend/typescript/types.ts +++ b/backend/typescript/types.ts @@ -25,7 +25,7 @@ export type UserDTO = { export type CreateUserDTO = Omit & { password: string }; -export type UpdateUserDTO = Omit; +export type UpdateUserDTO = Partial>; export type RegisterUserDTO = Omit; From 26f6e38dfa1ab9c3a6bddcbf1b0a72e4ee51d717 Mon Sep 17 00:00:00 2001 From: sthuray <138075787+sthuray@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:09:17 -0400 Subject: [PATCH 4/6] Fix type error --- backend/typescript/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/typescript/types.ts b/backend/typescript/types.ts index 4648032..daf6250 100644 --- a/backend/typescript/types.ts +++ b/backend/typescript/types.ts @@ -25,7 +25,7 @@ export type UserDTO = { export type CreateUserDTO = Omit & { password: string }; -export type UpdateUserDTO = Partial>; +export type UpdateUserDTO = Omit; export type RegisterUserDTO = Omit; From 06ff34f9ec40735fe5dbbd5bd923124634c5778b Mon Sep 17 00:00:00 2001 From: sthuray <138075787+sthuray@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:30:37 -0400 Subject: [PATCH 5/6] Fix email update in database --- backend/typescript/services/implementations/userService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/typescript/services/implementations/userService.ts b/backend/typescript/services/implementations/userService.ts index 5606dc9..30e4f73 100644 --- a/backend/typescript/services/implementations/userService.ts +++ b/backend/typescript/services/implementations/userService.ts @@ -240,6 +240,7 @@ class UserService implements IUserService { { first_name: user.firstName, last_name: user.lastName, + email: user.email, role: user.role, status: user.status, skill_level: user.skillLevel, @@ -274,6 +275,7 @@ class UserService implements IUserService { { first_name: oldUser.first_name, last_name: oldUser.last_name, + email: oldUser.email, role: oldUser.role, status: oldUser.status, skill_level: oldUser.skill_level, From 2f421fe70ce2fca24865ae1af7f8297004cf178b Mon Sep 17 00:00:00 2001 From: sthuray <138075787+sthuray@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:30:37 -0400 Subject: [PATCH 6/6] Fix email update in database --- backend/typescript/rest/userRoutes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/typescript/rest/userRoutes.ts b/backend/typescript/rest/userRoutes.ts index c1d003c..12ff25a 100644 --- a/backend/typescript/rest/userRoutes.ts +++ b/backend/typescript/rest/userRoutes.ts @@ -107,7 +107,7 @@ userRouter.post("/", createUserDtoValidator, async (req, res) => { lastName: req.body.lastName, email: req.body.email, role: req.body.role ?? Role.VOLUNTEER, - status: UserStatus.INVITED, // TODO: make this default to inactive once user registration flow is done + status: UserStatus.INVITED, skillLevel: req.body.skillLevel ?? null, canSeeAllLogs: req.body.canSeeAllLogs ?? null, canAssignUsersToTasks: req.body.canSeeAllUsers ?? null,