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 0f3ad1d..e1cf9cb 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, @@ -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) }); + } } } } @@ -101,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, skillLevel: req.body.skillLevel ?? null, canSeeAllLogs: req.body.canSeeAllLogs ?? null, canAssignUsersToTasks: req.body.canSeeAllUsers ?? null, @@ -119,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) }); + } } }); @@ -151,6 +196,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 @@ -160,10 +220,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; @@ -176,6 +248,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) { 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,