diff --git a/backend/typescript/middlewares/validators/activityValidators.ts b/backend/typescript/middlewares/validators/activityTypeValidators.ts similarity index 90% rename from backend/typescript/middlewares/validators/activityValidators.ts rename to backend/typescript/middlewares/validators/activityTypeValidators.ts index e06b7f9..d1b5e11 100644 --- a/backend/typescript/middlewares/validators/activityValidators.ts +++ b/backend/typescript/middlewares/validators/activityTypeValidators.ts @@ -3,7 +3,7 @@ import { getApiValidationError, validatePrimitive } from "./util"; /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable-next-line import/prefer-default-export */ -export const activityRequestDtoValidator = async ( +export const activityTypeRequestDtoValidator = async ( req: Request, res: Response, next: NextFunction, 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/migrations/2024.10.09T02.40.17.rename-activity-table-to-activity-type.ts b/backend/typescript/migrations/2024.10.09T02.40.17.rename-activity-table-to-activity-type.ts new file mode 100644 index 0000000..bbd58a1 --- /dev/null +++ b/backend/typescript/migrations/2024.10.09T02.40.17.rename-activity-table-to-activity-type.ts @@ -0,0 +1,65 @@ +import { DataType } from "sequelize-typescript"; +import { Migration } from "../umzug"; + +const OLD_TABLE_NAME = "activities"; +const NEW_TABLE_NAME = "activity_types"; +const USER_PET_ACTIVITIES_TABLE = "user_pet_activities"; +const ACTIVITIES_TABLE = "activities"; + +export const up: Migration = async ({ context: sequelize }) => { + // Rename the activities table to activity_types + await sequelize + .getQueryInterface() + .renameTable(OLD_TABLE_NAME, NEW_TABLE_NAME); + + // Change the activity_id column in user_pet_activities to activity_type_id + await sequelize + .getQueryInterface() + .renameColumn(USER_PET_ACTIVITIES_TABLE, "activity_id", "activity_type_id"); + + // Update the references for activity_type_id to point to the new activity_types table + await sequelize + .getQueryInterface() + .changeColumn(USER_PET_ACTIVITIES_TABLE, "activity_type_id", { + type: DataType.INTEGER, + allowNull: false, + references: { + model: NEW_TABLE_NAME, // Reference the new table name + key: "id", + }, + }); + + // Change the name of user_pet_activities to activities + await sequelize + .getQueryInterface() + .renameTable(USER_PET_ACTIVITIES_TABLE, ACTIVITIES_TABLE); +}; + +export const down: Migration = async ({ context: sequelize }) => { + // Rename the activities table back to user_pet_activities + await sequelize + .getQueryInterface() + .renameTable(ACTIVITIES_TABLE, USER_PET_ACTIVITIES_TABLE); + + // Rename the activity_types table back to activities + await sequelize + .getQueryInterface() + .renameTable(NEW_TABLE_NAME, OLD_TABLE_NAME); + + // Revert the activity_type_id column back to activity_id + await sequelize + .getQueryInterface() + .renameColumn(USER_PET_ACTIVITIES_TABLE, "activity_type_id", "activity_id"); + + // Revert the activity_id column to reference the old activities table + await sequelize + .getQueryInterface() + .changeColumn(USER_PET_ACTIVITIES_TABLE, "activity_id", { + type: DataType.INTEGER, + allowNull: false, + references: { + model: OLD_TABLE_NAME, // Revert back to the old table name + key: "id", + }, + }); +}; diff --git a/backend/typescript/models/activity.model.ts b/backend/typescript/models/activity.model.ts index 3db7cc9..56926bf 100644 --- a/backend/typescript/models/activity.model.ts +++ b/backend/typescript/models/activity.model.ts @@ -1,7 +1,56 @@ -import { Column, Model, Table } from "sequelize-typescript"; +import { + Column, + Model, + Table, + DataType, + ForeignKey, + BelongsTo, +} from "sequelize-typescript"; +import User from "./user.model"; +import Pet from "./pet.model"; +import ActivityType from "./activityType.model"; @Table({ timestamps: false, tableName: "activities" }) export default class Activity extends Model { - @Column - activity_name!: string; + @Column({}) + activity_id!: number; + + @ForeignKey(() => User) // in case of null, task has not been assigned + @Column({}) + user_id?: number; + + @BelongsTo(() => User) + user?: User; + + @ForeignKey(() => Pet) + @Column({}) + pet_id!: number; + + @BelongsTo(() => Pet) + pet!: Pet; + + @ForeignKey(() => ActivityType) + @Column({}) + activity_type_id!: number; + + @BelongsTo(() => ActivityType) + activity_type!: ActivityType; + + @Column({}) + scheduled_start_time?: Date; + + @Column({}) + start_time?: Date; + + @Column({}) + end_time?: Date; + + @Column({ type: DataType.TEXT }) + notes?: string; + + @Column({}) + created_at!: Date; + + @Column({}) + updated_at?: Date; } diff --git a/backend/typescript/models/activityType.model.ts b/backend/typescript/models/activityType.model.ts new file mode 100644 index 0000000..68a6d01 --- /dev/null +++ b/backend/typescript/models/activityType.model.ts @@ -0,0 +1,7 @@ +import { Column, Model, Table } from "sequelize-typescript"; + +@Table({ timestamps: false, tableName: "activity_types" }) +export default class ActivityType extends Model { + @Column + activity_name!: string; +} diff --git a/backend/typescript/models/userPetActivity.ts b/backend/typescript/models/userPetActivity.ts deleted file mode 100644 index a50b3e4..0000000 --- a/backend/typescript/models/userPetActivity.ts +++ /dev/null @@ -1,57 +0,0 @@ -// not started -import { - Column, - Model, - Table, - DataType, - ForeignKey, - BelongsTo, -} from "sequelize-typescript"; -import User from "./user.model"; -import Pet from "./pet.model"; -import Activity from "./activity.model"; - -@Table({ timestamps: false, tableName: "user_pet_activities" }) -export default class UserPetActivity extends Model { - @Column({}) - user_pet_activity_id!: number; - - @ForeignKey(() => User) // in case of null, task has not been assigned - @Column({}) - user_id?: number; - - @BelongsTo(() => User) - user?: User; - - @ForeignKey(() => Pet) - @Column({}) - pet_id!: number; - - @BelongsTo(() => Pet) - pet!: Pet; - - @ForeignKey(() => Activity) - @Column({}) - activity_id!: number; - - @BelongsTo(() => Activity) - activity!: Activity; - - @Column({}) - scheduled_start_time?: Date; - - @Column({}) - start_time?: Date; - - @Column({}) - end_time?: Date; - - @Column({ type: DataType.TEXT }) - notes?: string; - - @Column({}) - created_at!: Date; - - @Column({}) - updated_at?: Date; -} diff --git a/backend/typescript/rest/activityRoutes.ts b/backend/typescript/rest/activityRoutes.ts deleted file mode 100644 index 6227e0f..0000000 --- a/backend/typescript/rest/activityRoutes.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Router } from "express"; -import { activityRequestDtoValidator } from "../middlewares/validators/activityValidators"; -import ActivityService from "../services/implementations/activityService"; -import { - ActivityResponseDTO, - IActivityService, -} from "../services/interfaces/activityService"; -import { - getErrorMessage, - NotFoundError, - INTERNAL_SERVER_ERROR_MESSAGE, -} from "../utilities/errorUtils"; -import { sendResponseByMimeType } from "../utilities/responseUtil"; - -const activityRouter: Router = Router(); - -const activityService: IActivityService = new ActivityService(); - -/* Create Activity */ -activityRouter.post("/", activityRequestDtoValidator, async (req, res) => { - try { - const { body } = req; - const newActivity = await activityService.createActivity({ - activityName: body.activityName, - }); - res.status(201).json(newActivity); - } catch (e: unknown) { - if (e instanceof NotFoundError) { - res.status(404).send(getErrorMessage(e)); - } else { - res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE); - } - } -}); - -/* Get all Activities */ -activityRouter.get("/", async (req, res) => { - const contentType = req.headers["content-type"]; - try { - const activities = await activityService.getActivities(); - await sendResponseByMimeType( - res, - 200, - contentType, - activities, - ); - } catch (e: unknown) { - await sendResponseByMimeType(res, 500, contentType, [ - { - error: INTERNAL_SERVER_ERROR_MESSAGE, - }, - ]); - } -}); - -/* Get Activity by id */ -activityRouter.get("/:id", async (req, res) => { - const { id } = req.params; - - try { - const activity = await activityService.getActivity(id); - res.status(200).json(activity); - } catch (e: unknown) { - if (e instanceof NotFoundError) { - res.status(404).send(getErrorMessage(e)); - } else { - res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE); - } - } -}); - -/* Update Activity by id */ -activityRouter.put("/:id", activityRequestDtoValidator, async (req, res) => { - const { id } = req.params; - try { - const { body } = req; - const activity = await activityService.updateActivity(id, { - activityName: body.activityName, - }); - res.status(200).json(activity); - } catch (e: unknown) { - if (e instanceof NotFoundError) { - res.status(404).send(getErrorMessage(e)); - } else { - res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE); - } - } -}); - -/* Delete Activity by id */ -activityRouter.delete("/:id", async (req, res) => { - const { id } = req.params; - - try { - const deletedId = await activityService.deleteActivity(id); - res.status(200).json({ id: deletedId }); - } catch (e: unknown) { - if (e instanceof NotFoundError) { - res.status(404).send(getErrorMessage(e)); - } else { - res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE); - } - } -}); - -export default activityRouter; diff --git a/backend/typescript/rest/activityTypeRoutes.ts b/backend/typescript/rest/activityTypeRoutes.ts new file mode 100644 index 0000000..7e37bb9 --- /dev/null +++ b/backend/typescript/rest/activityTypeRoutes.ts @@ -0,0 +1,114 @@ +import { Router } from "express"; +import { activityTypeRequestDtoValidator } from "../middlewares/validators/activityTypeValidators"; +import ActivityTypeService from "../services/implementations/activityTypeService"; +import { + ActivityTypeResponseDTO, + IActivityTypeService, +} from "../services/interfaces/activityTypeService"; +import { + getErrorMessage, + NotFoundError, + INTERNAL_SERVER_ERROR_MESSAGE, +} from "../utilities/errorUtils"; +import { sendResponseByMimeType } from "../utilities/responseUtil"; + +const activityTypeRouter: Router = Router(); + +const activityTypeService: IActivityTypeService = new ActivityTypeService(); + +/* Create ActivityType */ +activityTypeRouter.post( + "/", + activityTypeRequestDtoValidator, + async (req, res) => { + try { + const { body } = req; + const newActivityType = await activityTypeService.createActivityType({ + activityName: body.activityName, + }); + res.status(201).json(newActivityType); + } catch (e: unknown) { + if (e instanceof NotFoundError) { + res.status(404).send(getErrorMessage(e)); + } else { + res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE); + } + } + }, +); + +/* Get all ActivityTypes */ +activityTypeRouter.get("/", async (req, res) => { + const contentType = req.headers["content-type"]; + try { + const activityTypes = await activityTypeService.getActivityTypes(); + await sendResponseByMimeType( + res, + 200, + contentType, + activityTypes, + ); + } catch (e: unknown) { + await sendResponseByMimeType(res, 500, contentType, [ + { + error: INTERNAL_SERVER_ERROR_MESSAGE, + }, + ]); + } +}); + +/* Get ActivityType by id */ +activityTypeRouter.get("/:id", async (req, res) => { + const { id } = req.params; + + try { + const activityType = await activityTypeService.getActivityType(id); + res.status(200).json(activityType); + } catch (e: unknown) { + if (e instanceof NotFoundError) { + res.status(404).send(getErrorMessage(e)); + } else { + res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE); + } + } +}); + +/* Update ActivityType by id */ +activityTypeRouter.put( + "/:id", + activityTypeRequestDtoValidator, + async (req, res) => { + const { id } = req.params; + try { + const { body } = req; + const activityType = await activityTypeService.updateActivityType(id, { + activityName: body.activityName, + }); + res.status(200).json(activityType); + } catch (e: unknown) { + if (e instanceof NotFoundError) { + res.status(404).send(getErrorMessage(e)); + } else { + res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE); + } + } + }, +); + +/* Delete ActivityType by id */ +activityTypeRouter.delete("/:id", async (req, res) => { + const { id } = req.params; + + try { + const deletedId = await activityTypeService.deleteActivityType(id); + res.status(200).json({ id: deletedId }); + } catch (e: unknown) { + if (e instanceof NotFoundError) { + res.status(404).send(getErrorMessage(e)); + } else { + res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE); + } + } +}); + +export default activityTypeRouter; 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/server.ts b/backend/typescript/server.ts index 4baf25e..640c84d 100644 --- a/backend/typescript/server.ts +++ b/backend/typescript/server.ts @@ -7,7 +7,7 @@ import YAML from "yamljs"; import { sequelize } from "./models"; import authRouter from "./rest/authRoutes"; -import activityRouter from "./rest/activityRoutes"; +import activityTypeRouter from "./rest/activityTypeRoutes"; import behaviourRouter from "./rest/behaviourRoutes"; import animalTypeRouter from "./rest/animalTypeRoutes"; import entityRouter from "./rest/entityRoutes"; @@ -36,7 +36,7 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use("/auth", authRouter); -app.use("/activities", activityRouter); +app.use("/activity-types", activityTypeRouter); app.use("/animal-types", animalTypeRouter); app.use("/behaviours", behaviourRouter); app.use("/entities", entityRouter); diff --git a/backend/typescript/services/implementations/activityService.ts b/backend/typescript/services/implementations/activityService.ts deleted file mode 100644 index e6be92d..0000000 --- a/backend/typescript/services/implementations/activityService.ts +++ /dev/null @@ -1,119 +0,0 @@ -import PgActivity from "../../models/activity.model"; -import { - IActivityService, - ActivityRequestDTO, - ActivityResponseDTO, -} from "../interfaces/activityService"; -import { getErrorMessage, NotFoundError } from "../../utilities/errorUtils"; -import logger from "../../utilities/logger"; - -const Logger = logger(__filename); - -class ActivityService implements IActivityService { - /* eslint-disable class-methods-use-this */ - async getActivity(id: string): Promise { - let activity: PgActivity | null; - try { - activity = await PgActivity.findByPk(id, { raw: true }); - if (!activity) { - throw new NotFoundError(`Activity id ${id} not found`); - } - } catch (error: unknown) { - Logger.error( - `Failed to get activity. Reason = ${getErrorMessage(error)}`, - ); - throw error; - } - - return { - id: activity.id, - activityName: activity.activity_name, - }; - } - - async getActivities(): Promise { - try { - const activities: Array = await PgActivity.findAll({ - raw: true, - }); - return activities.map((activity) => ({ - id: activity.id, - activityName: activity.activity_name, - })); - } catch (error: unknown) { - Logger.error( - `Failed to get activities. Reason = ${getErrorMessage(error)}`, - ); - throw error; - } - } - - async createActivity( - activity: ActivityRequestDTO, - ): Promise { - let newActivity: PgActivity | null; - try { - newActivity = await PgActivity.create({ - activity_name: activity.activityName, - }); - } catch (error: unknown) { - Logger.error( - `Failed to create activity. Reason = ${getErrorMessage(error)}`, - ); - throw error; - } - return { - id: newActivity.id, - activityName: newActivity.activity_name, - }; - } - - async updateActivity( - id: string, - activity: ActivityRequestDTO, - ): Promise { - let resultingActivity: PgActivity | null; - let updateResult: [number, PgActivity[]] | null; - try { - updateResult = await PgActivity.update( - { - activity_name: activity.activityName, - }, - { where: { id }, returning: true }, - ); - - if (!updateResult[0]) { - throw new NotFoundError(`Activity id ${id} not found`); - } - [, [resultingActivity]] = updateResult; - } catch (error: unknown) { - Logger.error( - `Failed to update activity. Reason = ${getErrorMessage(error)}`, - ); - throw error; - } - return { - id: resultingActivity.id, - activityName: resultingActivity?.activity_name, - }; - } - - async deleteActivity(id: string): Promise { - try { - const deleteResult: number | null = await PgActivity.destroy({ - where: { id }, - }); - if (!deleteResult) { - throw new NotFoundError(`Activity id ${id} not found`); - } - return id; - } catch (error: unknown) { - Logger.error( - `Failed to delete activity. Reason = ${getErrorMessage(error)}`, - ); - throw error; - } - } -} - -export default ActivityService; diff --git a/backend/typescript/services/implementations/activityTypeService.ts b/backend/typescript/services/implementations/activityTypeService.ts new file mode 100644 index 0000000..8b93383 --- /dev/null +++ b/backend/typescript/services/implementations/activityTypeService.ts @@ -0,0 +1,121 @@ +import PgActivityType from "../../models/activityType.model"; +import { + IActivityTypeService, + ActivityTypeRequestDTO, + ActivityTypeResponseDTO, +} from "../interfaces/activityTypeService"; +import { getErrorMessage, NotFoundError } from "../../utilities/errorUtils"; +import logger from "../../utilities/logger"; + +const Logger = logger(__filename); + +class ActivityTypeService implements IActivityTypeService { + /* eslint-disable class-methods-use-this */ + async getActivityType(id: string): Promise { + let activityType: PgActivityType | null; + try { + activityType = await PgActivityType.findByPk(id, { raw: true }); + if (!activityType) { + throw new NotFoundError(`ActivityType id ${id} not found`); + } + } catch (error: unknown) { + Logger.error( + `Failed to get activity type. Reason = ${getErrorMessage(error)}`, + ); + throw error; + } + + return { + id: activityType.id, + activityName: activityType.activity_name, + }; + } + + async getActivityTypes(): Promise { + try { + const activityTypes: Array = await PgActivityType.findAll( + { + raw: true, + }, + ); + return activityTypes.map((activityType) => ({ + id: activityType.id, + activityName: activityType.activity_name, + })); + } catch (error: unknown) { + Logger.error( + `Failed to get activity types. Reason = ${getErrorMessage(error)}`, + ); + throw error; + } + } + + async createActivityType( + activityType: ActivityTypeRequestDTO, + ): Promise { + let newActivityType: PgActivityType | null; + try { + newActivityType = await PgActivityType.create({ + activity_name: activityType.activityName, + }); + } catch (error: unknown) { + Logger.error( + `Failed to create activity type. Reason = ${getErrorMessage(error)}`, + ); + throw error; + } + return { + id: newActivityType.id, + activityName: newActivityType.activity_name, + }; + } + + async updateActivityType( + id: string, + activityType: ActivityTypeRequestDTO, + ): Promise { + let resultingActivityType: PgActivityType | null; + let updateResult: [number, PgActivityType[]] | null; + try { + updateResult = await PgActivityType.update( + { + activity_name: activityType.activityName, + }, + { where: { id }, returning: true }, + ); + + if (!updateResult[0]) { + throw new NotFoundError(`ActivityType id ${id} not found`); + } + [, [resultingActivityType]] = updateResult; + } catch (error: unknown) { + Logger.error( + `Failed to update activity type. Reason = ${getErrorMessage(error)}`, + ); + throw error; + } + return { + id: resultingActivityType.id, + activityName: resultingActivityType?.activity_name, + }; + } + + async deleteActivityType(id: string): Promise { + try { + const deleteResult: number | null = await PgActivityType.destroy({ + where: { id }, + }); + if (!deleteResult) { + throw new NotFoundError(`ActivityType id ${id} not found`); + } + return id; + } catch (error: unknown) { + Logger.error( + `Failed to delete activity type. Reason = ${getErrorMessage(error)}`, + ); + throw error; + } + } +} + +export default ActivityTypeService; 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, diff --git a/backend/typescript/services/interfaces/activityService.ts b/backend/typescript/services/interfaces/activityService.ts deleted file mode 100644 index 6a38bf7..0000000 --- a/backend/typescript/services/interfaces/activityService.ts +++ /dev/null @@ -1,54 +0,0 @@ -export interface ActivityRequestDTO { - activityName: string; -} - -export interface ActivityResponseDTO { - id: number; - activityName: string; -} - -export interface IActivityService { - /** - * retrieve the Activity with the given id - * @param id Activity id - * @returns requested Activity - * @throws Error if retrieval fails - */ - getActivity(id: string): Promise; - - /** - * retrieve all Activities - * @param - * @returns returns array of Activities - * @throws Error if retrieval fails - */ - getActivities(): Promise; - - /** - * create a Activity with the fields given in the DTO, return created Activity - * @param activity new Activity - * @returns the created Activity - * @throws Error if creation fails - */ - createActivity(activity: ActivityRequestDTO): Promise; - - /** - * update the Activity with the given id with fields in the DTO, return updated Activity - * @param id Activity id - * @param activity Updated Activity - * @returns the updated Activity - * @throws Error if update fails - */ - updateActivity( - id: string, - activity: ActivityRequestDTO, - ): Promise; - - /** - * delete the Activity with the given id - * @param id Activity id - * @returns id of the Activity deleted - * @throws Error if deletion fails - */ - deleteActivity(id: string): Promise; -} diff --git a/backend/typescript/services/interfaces/activityTypeService.ts b/backend/typescript/services/interfaces/activityTypeService.ts new file mode 100644 index 0000000..6ed667a --- /dev/null +++ b/backend/typescript/services/interfaces/activityTypeService.ts @@ -0,0 +1,55 @@ +export interface ActivityTypeRequestDTO { + activityName: string; +} + +export interface ActivityTypeResponseDTO { + id: number; + activityName: string; +} + +export interface IActivityTypeService { + /** + * Retrieve the ActivityType with the given id. + * @param id ActivityType id + * @returns Requested ActivityType + * @throws Error if retrieval fails + */ + getActivityType(id: string): Promise; + + /** + * Retrieve all Activity Types. + * @returns Returns an array of ActivityTypes + * @throws Error if retrieval fails + */ + getActivityTypes(): Promise; + + /** + * Create an ActivityType with the fields given in the DTO, return created ActivityType + * @param activity New ActivityType + * @returns The created ActivityType + * @throws Error if creation fails + */ + createActivityType( + activity: ActivityTypeRequestDTO, + ): Promise; + + /** + * Update the ActivityType with the given id with fields in the DTO, return updated ActivityType + * @param id ActivityType id + * @param activity Updated ActivityType + * @returns The updated ActivityType + * @throws Error if update fails + */ + updateActivityType( + id: string, + activity: ActivityTypeRequestDTO, + ): Promise; + + /** + * Delete the ActivityType with the given id + * @param id ActivityType id + * @returns id of the ActivityType deleted + * @throws Error if deletion fails + */ + deleteActivityType(id: string): Promise; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2409c59..0bfb321 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -29,6 +29,8 @@ import EditTeamInfoPage from "./components/pages/EditTeamPage"; import HooksDemo from "./components/pages/HooksDemo"; import NotificationsPage from "./components/pages/NotificationsPage"; import ProfilePage from "./components/pages/ProfilePage"; +import UserManagementPage from "./components/pages/UserManagementPage"; +import AdminPage from "./components/pages/AdminPage"; import { AuthenticatedUser } from "./types/AuthTypes"; @@ -138,6 +140,18 @@ const App = (): React.ReactElement => { component={ProfilePage} allowedRoles={AuthConstants.ALL_ROLES} /> + + diff --git a/frontend/src/components/pages/AdminPage.tsx b/frontend/src/components/pages/AdminPage.tsx new file mode 100644 index 0000000..85bd03b --- /dev/null +++ b/frontend/src/components/pages/AdminPage.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import MainPageButton from "../common/MainPageButton"; + +const AdminPage = (): React.ReactElement => { + return ( +
+

Admin Page

+ +
+ ); +}; + +export default AdminPage; diff --git a/frontend/src/components/pages/UserManagementPage.tsx b/frontend/src/components/pages/UserManagementPage.tsx new file mode 100644 index 0000000..21431ca --- /dev/null +++ b/frontend/src/components/pages/UserManagementPage.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import MainPageButton from "../common/MainPageButton"; + +const UserManagementPage = (): React.ReactElement => { + return ( +
+

User Management

+ +
+ ); +}; + +export default UserManagementPage; diff --git a/frontend/src/constants/Routes.ts b/frontend/src/constants/Routes.ts index 21e0d7b..ac46e9e 100644 --- a/frontend/src/constants/Routes.ts +++ b/frontend/src/constants/Routes.ts @@ -27,3 +27,7 @@ export const NOTIFICATIONS_PAGE = "/notifications"; export const PROFILE_PAGE = "/profile"; export const DEV_UTILITY_PAGE = "/dev-utility"; // TODO: This is only here for development purposes + +export const USER_MANAGEMENT_PAGE = "/admin/users"; + +export const ADMIN_PAGE = "/admin";