Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

S24/sayi/add user management endpoints #35

Merged
merged 6 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions backend/typescript/middlewares/validators/userValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
113 changes: 96 additions & 17 deletions backend/typescript/rest/userRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Router } from "express";

import { isAuthorizedByRole } from "../middlewares/auth";
import { getAccessToken, isAuthorizedByRole } from "../middlewares/auth";
import {
createUserDtoValidator,
updateUserDtoValidator,
Expand Down Expand Up @@ -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);
Expand All @@ -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) });
}
}
}
}
Expand All @@ -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,
Expand All @@ -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"]);
jerry-cheng5 marked this conversation as resolved.
Show resolved Hide resolved
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) });
}
}
});

Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions backend/typescript/services/implementations/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading