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 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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, From 83cb2c599c82c6296f016a5435f46be9a06a422a Mon Sep 17 00:00:00 2001 From: liya-zhu Date: Tue, 15 Oct 2024 20:31:30 -0400 Subject: [PATCH 07/16] Added user animal type table --- ...00.23.02.create-user-animal-types-table.ts | 36 +++++++++++++++++++ backend/typescript/models/animalType.model.ts | 7 +++- backend/typescript/models/user.model.ts | 8 ++++- .../typescript/models/userAnimalType.model.ts | 27 ++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 backend/typescript/migrations/2024.10.04T00.23.02.create-user-animal-types-table.ts create mode 100644 backend/typescript/models/userAnimalType.model.ts diff --git a/backend/typescript/migrations/2024.10.04T00.23.02.create-user-animal-types-table.ts b/backend/typescript/migrations/2024.10.04T00.23.02.create-user-animal-types-table.ts new file mode 100644 index 0000000..3193e89 --- /dev/null +++ b/backend/typescript/migrations/2024.10.04T00.23.02.create-user-animal-types-table.ts @@ -0,0 +1,36 @@ +import { DataType } from "sequelize-typescript"; + +import { Migration } from "../umzug"; + +const TABLE_NAME = "user_animal_types"; + +export const up: Migration = async ({ context: sequelize }) => { + await sequelize.getQueryInterface().createTable(TABLE_NAME, { + user_id: { + type: DataType.INTEGER, + allowNull: false, + primaryKey: true, + references: { + model: "users", + key: "id" + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + animal_type_id: { + type: DataType.INTEGER, + allowNull: false, + primaryKey: true, + references: { + model: "animal_types", + key: "id" + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + } + }); +} + +export const down: Migration = async ({ context: sequelize }) => { + await sequelize.getQueryInterface().dropTable(TABLE_NAME); +}; diff --git a/backend/typescript/models/animalType.model.ts b/backend/typescript/models/animalType.model.ts index 3cc102a..5f284a9 100644 --- a/backend/typescript/models/animalType.model.ts +++ b/backend/typescript/models/animalType.model.ts @@ -1,7 +1,12 @@ -import { Column, Model, Table } from "sequelize-typescript"; +import { Column, Model, Table, BelongsToMany } from "sequelize-typescript"; +import UserAnimalType from "./userAnimalType.model"; +import User from "./user.model"; @Table({ timestamps: false, tableName: "animal_types" }) export default class AnimalType extends Model { @Column({}) animal_type_name!: string; + + @BelongsToMany(() => User, () => UserAnimalType) + users!: User[]; } diff --git a/backend/typescript/models/user.model.ts b/backend/typescript/models/user.model.ts index 928c641..6bac2a6 100644 --- a/backend/typescript/models/user.model.ts +++ b/backend/typescript/models/user.model.ts @@ -4,8 +4,11 @@ import { Model, Table, AllowNull, + BelongsToMany, } from "sequelize-typescript"; import { Role, UserStatus } from "../types"; +import AnimalType from "./animalType.model"; +import UserAnimalType from "./userAnimalType.model"; @Table({ tableName: "users" }) export default class User extends Model { @@ -39,4 +42,7 @@ export default class User extends Model { @Column({ type: DataType.ENUM("Active", "Inactive"), allowNull: false }) status!: UserStatus; -} + + @BelongsToMany(() => AnimalType, () => UserAnimalType) + animalTypes!: AnimalType[]; +} \ No newline at end of file diff --git a/backend/typescript/models/userAnimalType.model.ts b/backend/typescript/models/userAnimalType.model.ts new file mode 100644 index 0000000..fc3115e --- /dev/null +++ b/backend/typescript/models/userAnimalType.model.ts @@ -0,0 +1,27 @@ +import { + Column, + DataType, + Model, + Table, + ForeignKey, + PrimaryKey +} from 'sequelize-typescript'; +import User from './user.model'; +import AnimalType from './animalType.model'; + +@Table({ + tableName: 'User_AnimalTypes', + timestamps: true +}) +export default class UserAnimalType extends Model { + @ForeignKey(() => User) + @PrimaryKey + @Column({ type:DataType.INTEGER, allowNull: false }) + user_id!: number; + + @ForeignKey(() => AnimalType) + @PrimaryKey + @Column({ type: DataType.INTEGER, allowNull: false }) + animal_type_id!: number; +} + From 6c528cfa7598e828aa28e5e9ca0f74d427451a97 Mon Sep 17 00:00:00 2001 From: liya-zhu Date: Thu, 17 Oct 2024 18:54:16 -0400 Subject: [PATCH 08/16] Fixed circular dependencies linter error --- backend/typescript/models/animalType.model.ts | 8 +++++--- backend/typescript/models/user.model.ts | 8 +++++--- backend/typescript/models/userAnimalType.model.ts | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/typescript/models/animalType.model.ts b/backend/typescript/models/animalType.model.ts index 5f284a9..84ef34d 100644 --- a/backend/typescript/models/animalType.model.ts +++ b/backend/typescript/models/animalType.model.ts @@ -1,12 +1,14 @@ import { Column, Model, Table, BelongsToMany } from "sequelize-typescript"; -import UserAnimalType from "./userAnimalType.model"; -import User from "./user.model"; +import type User from "./user.model"; @Table({ timestamps: false, tableName: "animal_types" }) export default class AnimalType extends Model { @Column({}) animal_type_name!: string; - @BelongsToMany(() => User, () => UserAnimalType) + @BelongsToMany( + () => import("./user.model").then((mod) => mod.default), + () => import("./userAnimalType.model").then((mod) => mod.default), + ) users!: User[]; } diff --git a/backend/typescript/models/user.model.ts b/backend/typescript/models/user.model.ts index 6bac2a6..bfe6f24 100644 --- a/backend/typescript/models/user.model.ts +++ b/backend/typescript/models/user.model.ts @@ -7,8 +7,7 @@ import { BelongsToMany, } from "sequelize-typescript"; import { Role, UserStatus } from "../types"; -import AnimalType from "./animalType.model"; -import UserAnimalType from "./userAnimalType.model"; +import type AnimalType from "./animalType.model"; @Table({ tableName: "users" }) export default class User extends Model { @@ -43,6 +42,9 @@ export default class User extends Model { @Column({ type: DataType.ENUM("Active", "Inactive"), allowNull: false }) status!: UserStatus; - @BelongsToMany(() => AnimalType, () => UserAnimalType) + @BelongsToMany( + () => import("./animalType.model").then((mod) => mod.default), + () => import("./userAnimalType.model").then((mod) => mod.default), + ) animalTypes!: AnimalType[]; } \ No newline at end of file diff --git a/backend/typescript/models/userAnimalType.model.ts b/backend/typescript/models/userAnimalType.model.ts index fc3115e..0047bc1 100644 --- a/backend/typescript/models/userAnimalType.model.ts +++ b/backend/typescript/models/userAnimalType.model.ts @@ -16,7 +16,7 @@ import AnimalType from './animalType.model'; export default class UserAnimalType extends Model { @ForeignKey(() => User) @PrimaryKey - @Column({ type:DataType.INTEGER, allowNull: false }) + @Column({ type: DataType.INTEGER, allowNull: false }) user_id!: number; @ForeignKey(() => AnimalType) From cdf3fb5538bca9bbf456dda59ca71edd37bae289 Mon Sep 17 00:00:00 2001 From: sthuray <138075787+sthuray@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:11:44 -0400 Subject: [PATCH 09/16] Update createUser function --- .../middlewares/validators/userValidators.ts | 3 -- backend/typescript/rest/authRoutes.ts | 4 +-- backend/typescript/rest/userRoutes.ts | 9 +++--- .../services/implementations/authService.ts | 29 +++++++------------ .../services/implementations/userService.ts | 29 ++++++++----------- .../services/interfaces/userService.ts | 16 ++-------- backend/typescript/types.ts | 4 +-- 7 files changed, 31 insertions(+), 63 deletions(-) diff --git a/backend/typescript/middlewares/validators/userValidators.ts b/backend/typescript/middlewares/validators/userValidators.ts index 17b9895..03913f1 100644 --- a/backend/typescript/middlewares/validators/userValidators.ts +++ b/backend/typescript/middlewares/validators/userValidators.ts @@ -19,9 +19,6 @@ export const createUserDtoValidator = async ( if (!validatePrimitive(req.body.role, "string")) { return res.status(400).send(getApiValidationError("role", "string")); } - if (!validatePrimitive(req.body.password, "string")) { - return res.status(400).send(getApiValidationError("password", "string")); - } if ( req.body.skillLevel !== undefined && req.body.skillLevel !== null && diff --git a/backend/typescript/rest/authRoutes.ts b/backend/typescript/rest/authRoutes.ts index e157107..eee469c 100644 --- a/backend/typescript/rest/authRoutes.ts +++ b/backend/typescript/rest/authRoutes.ts @@ -13,7 +13,7 @@ import IAuthService from "../services/interfaces/authService"; import IEmailService from "../services/interfaces/emailService"; import IUserService from "../services/interfaces/userService"; import { getErrorMessage } from "../utilities/errorUtils"; -import { Role, UserStatus } from "../types"; +import { Role } from "../types"; const authRouter: Router = Router(); const userService: IUserService = new UserService(); @@ -53,12 +53,10 @@ authRouter.post("/register", registerRequestValidator, 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 skillLevel: req.body.skillLevel ?? null, canSeeAllLogs: req.body.canSeeAllLogs ?? null, canAssignUsersToTasks: req.body.canAssignUsersToTasks ?? null, phoneNumber: req.body.phoneNumber ?? null, - password: req.body.password, }); const authDTO = await authService.generateToken( diff --git a/backend/typescript/rest/userRoutes.ts b/backend/typescript/rest/userRoutes.ts index e1cf9cb..5e57120 100644 --- a/backend/typescript/rest/userRoutes.ts +++ b/backend/typescript/rest/userRoutes.ts @@ -12,7 +12,7 @@ import UserService from "../services/implementations/userService"; import IAuthService from "../services/interfaces/authService"; import IEmailService from "../services/interfaces/emailService"; import IUserService from "../services/interfaces/userService"; -import { Role, UserDTO, UserStatus } from "../types"; +import { Role, UserDTO } from "../types"; import { getErrorMessage, NotFoundError, @@ -99,6 +99,7 @@ userRouter.get("/", async (req, res) => { } }); +// This endpoint is for testing purposes /* Create a user */ userRouter.post("/", createUserDtoValidator, async (req, res) => { try { @@ -106,16 +107,14 @@ userRouter.post("/", createUserDtoValidator, async (req, res) => { firstName: req.body.firstName, lastName: req.body.lastName, email: req.body.email, - role: req.body.role ?? Role.VOLUNTEER, - status: UserStatus.INVITED, + role: req.body.role, skillLevel: req.body.skillLevel ?? null, canSeeAllLogs: req.body.canSeeAllLogs ?? null, canAssignUsersToTasks: req.body.canSeeAllUsers ?? null, phoneNumber: req.body.phoneNumber ?? null, - password: req.body.password, }); - await authService.sendEmailVerificationLink(req.body.email); + // await authService.sendEmailVerificationLink(req.body.email); res.status(201).json(newUser); } catch (error: unknown) { diff --git a/backend/typescript/services/implementations/authService.ts b/backend/typescript/services/implementations/authService.ts index 0f1c543..161bc9a 100644 --- a/backend/typescript/services/implementations/authService.ts +++ b/backend/typescript/services/implementations/authService.ts @@ -3,7 +3,7 @@ import * as firebaseAdmin from "firebase-admin"; import IAuthService from "../interfaces/authService"; import IEmailService from "../interfaces/emailService"; import IUserService from "../interfaces/userService"; -import { AuthDTO, Role, Token, UserStatus } from "../../types"; +import { AuthDTO, Role, Token } from "../../types"; import { getErrorMessage } from "../../utilities/errorUtils"; import FirebaseRestClient from "../../utilities/firebaseRestClient"; import logger from "../../utilities/logger"; @@ -58,18 +58,12 @@ class AuthService implements IAuthService { /* eslint-disable-next-line no-empty */ } catch (error) {} - const user = await this.userService.createUser( - { - firstName: googleUser.firstName, - lastName: googleUser.lastName, - email: googleUser.email, - role: Role.STAFF, - status: UserStatus.ACTIVE, - password: "", - }, - googleUser.localId, - "GOOGLE", - ); + const user = await this.userService.createUser({ + firstName: googleUser.firstName, + lastName: googleUser.lastName, + email: googleUser.email, + role: Role.STAFF, + }); return { ...token, ...user }; } catch (error) { @@ -175,12 +169,11 @@ class AuthService implements IAuthService { const userRole = await this.userService.getUserRoleByAuthId( decodedIdToken.uid, ); + // const firebaseUser = await firebaseAdmin + // .auth() + // .getUser(decodedIdToken.uid); - const firebaseUser = await firebaseAdmin - .auth() - .getUser(decodedIdToken.uid); - - return firebaseUser.emailVerified && roles.has(userRole); + return /* firebaseUser.emailVerified && */ roles.has(userRole); } catch (error) { return false; } diff --git a/backend/typescript/services/implementations/userService.ts b/backend/typescript/services/implementations/userService.ts index 30e4f73..d05f536 100644 --- a/backend/typescript/services/implementations/userService.ts +++ b/backend/typescript/services/implementations/userService.ts @@ -1,6 +1,12 @@ import * as firebaseAdmin from "firebase-admin"; import IUserService from "../interfaces/userService"; -import { CreateUserDTO, Role, UpdateUserDTO, UserDTO } from "../../types"; +import { + CreateUserDTO, + Role, + UpdateUserDTO, + UserDTO, + UserStatus, +} from "../../types"; import { getErrorMessage, NotFoundError } from "../../utilities/errorUtils"; import logger from "../../utilities/logger"; import PgUser from "../../models/user.model"; @@ -165,25 +171,14 @@ class UserService implements IUserService { return userDtos; } - async createUser( - user: CreateUserDTO, - authId?: string, - signUpMethod = "PASSWORD", - ): Promise { + async createUser(user: CreateUserDTO): Promise { let newUser: PgUser; let firebaseUser: firebaseAdmin.auth.UserRecord; try { - if (signUpMethod === "GOOGLE") { - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - firebaseUser = await firebaseAdmin.auth().getUser(authId!); - } else { - // signUpMethod === PASSWORD - firebaseUser = await firebaseAdmin.auth().createUser({ - email: user.email, - password: user.password, - }); - } + firebaseUser = await firebaseAdmin.auth().createUser({ + email: user.email, + }); try { newUser = await PgUser.create({ @@ -191,7 +186,7 @@ class UserService implements IUserService { last_name: user.lastName, auth_id: firebaseUser.uid, role: user.role, - status: user.status, + status: UserStatus.INVITED, email: firebaseUser.email ?? "", skill_level: user.skillLevel, can_see_all_logs: user.canSeeAllLogs, diff --git a/backend/typescript/services/interfaces/userService.ts b/backend/typescript/services/interfaces/userService.ts index 998f89a..de1fb22 100644 --- a/backend/typescript/services/interfaces/userService.ts +++ b/backend/typescript/services/interfaces/userService.ts @@ -1,10 +1,4 @@ -import { - CreateUserDTO, - Role, - SignUpMethod, - UpdateUserDTO, - UserDTO, -} from "../../types"; +import { CreateUserDTO, Role, UpdateUserDTO, UserDTO } from "../../types"; interface IUserService { /** @@ -57,16 +51,10 @@ interface IUserService { /** * Create a user, email verification configurable * @param user the user to be created - * @param authId the user's firebase auth id, optional - * @param signUpMethod the method user used to signup * @returns a UserDTO with the created user's information * @throws Error if user creation fails */ - createUser( - user: CreateUserDTO, - authId?: string, - signUpMethod?: SignUpMethod, - ): Promise; + createUser(user: CreateUserDTO): Promise; /** * Update a user. diff --git a/backend/typescript/types.ts b/backend/typescript/types.ts index daf6250..3073b9f 100644 --- a/backend/typescript/types.ts +++ b/backend/typescript/types.ts @@ -23,7 +23,7 @@ export type UserDTO = { phoneNumber?: string | null; }; -export type CreateUserDTO = Omit & { password: string }; +export type CreateUserDTO = Omit; export type UpdateUserDTO = Omit; @@ -66,5 +66,3 @@ export type NodemailerConfig = { refreshToken: string; }; }; - -export type SignUpMethod = "PASSWORD" | "GOOGLE"; From cf42402dc2c0675ba6f0c9f240470718dd6bf64f Mon Sep 17 00:00:00 2001 From: vips11 <66628544+vips11@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:58:09 -0400 Subject: [PATCH 10/16] Created model and migration files for behaviour and behaviour level tables --- ...07.create-behaviour-level-details-table.ts | 42 +++++++++++++++++++ ...24.10.22T19.42.10.add-behaviour-columns.ts | 17 ++++++++ backend/typescript/models/behaviour.model.ts | 14 ++++++- .../models/behvaiourLevelDetails.model.ts | 28 +++++++++++++ backend/typescript/types.ts | 2 + 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 backend/typescript/migrations/2024.10.22T19.38.07.create-behaviour-level-details-table.ts create mode 100644 backend/typescript/migrations/2024.10.22T19.42.10.add-behaviour-columns.ts create mode 100644 backend/typescript/models/behvaiourLevelDetails.model.ts diff --git a/backend/typescript/migrations/2024.10.22T19.38.07.create-behaviour-level-details-table.ts b/backend/typescript/migrations/2024.10.22T19.38.07.create-behaviour-level-details-table.ts new file mode 100644 index 0000000..87c465f --- /dev/null +++ b/backend/typescript/migrations/2024.10.22T19.38.07.create-behaviour-level-details-table.ts @@ -0,0 +1,42 @@ +import { DataType } from "sequelize-typescript"; + +import { Migration } from "../umzug"; + +const TABLE_NAME = "behaviour_level_details"; + +export const up: Migration = async ({ context: sequelize }) => { + await sequelize.getQueryInterface().createTable(TABLE_NAME, { + id: { + type: DataType.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true, + }, + behaviour_id: { + type: DataType.INTEGER, + allowNull: false, + }, + level: { + type: DataType.INTEGER, + allowNull: false, + }, + description: { + type: DataType.STRING, + allowNull: false, + }, + training_instructions: { + type: DataType.STRING, + allowNull: false, + }, + }); + + await sequelize.getQueryInterface().addConstraint(TABLE_NAME, { + fields: ["behaviour_id", "level"], + type: "unique", + name: "unique_behaviour_level", + }); +}; + +export const down: Migration = async ({ context: sequelize }) => { + await sequelize.getQueryInterface().dropTable(TABLE_NAME); +}; diff --git a/backend/typescript/migrations/2024.10.22T19.42.10.add-behaviour-columns.ts b/backend/typescript/migrations/2024.10.22T19.42.10.add-behaviour-columns.ts new file mode 100644 index 0000000..5099b98 --- /dev/null +++ b/backend/typescript/migrations/2024.10.22T19.42.10.add-behaviour-columns.ts @@ -0,0 +1,17 @@ +import { DataType } from "sequelize-typescript"; +import { Migration } from "../umzug"; + +export const up: Migration = async ({ context: sequelize }) => { + await sequelize + .getQueryInterface() + .addColumn("behaviours", "parent_behaviour_id", { + type: DataType.INTEGER, + allowNull: true, + }); +}; + +export const down: Migration = async ({ context: sequelize }) => { + await sequelize + .getQueryInterface() + .removeColumn("behaviours", "parent_behaviour_id"); +}; diff --git a/backend/typescript/models/behaviour.model.ts b/backend/typescript/models/behaviour.model.ts index 9ffaf6d..edc14dc 100644 --- a/backend/typescript/models/behaviour.model.ts +++ b/backend/typescript/models/behaviour.model.ts @@ -1,7 +1,17 @@ -import { Column, Model, Table } from "sequelize-typescript"; +import { + Column, + DataType, + ForeignKey, + Model, + Table, +} from "sequelize-typescript"; @Table({ timestamps: false, tableName: "behaviours" }) export default class Behaviour extends Model { - @Column + @Column({ type: DataType.STRING, allowNull: false }) behaviour_name!: string; + + @ForeignKey(() => Behaviour) + @Column({ type: DataType.INTEGER }) + parent_behaviour_id?: number | null; } diff --git a/backend/typescript/models/behvaiourLevelDetails.model.ts b/backend/typescript/models/behvaiourLevelDetails.model.ts new file mode 100644 index 0000000..e8148fb --- /dev/null +++ b/backend/typescript/models/behvaiourLevelDetails.model.ts @@ -0,0 +1,28 @@ +import { + Column, + Model, + Table, + ForeignKey, + DataType, +} from "sequelize-typescript"; +import Behaviour from "./behaviour.model"; +import { BehaviourLevel } from "../types"; + +@Table({ timestamps: false, tableName: "behvaiour_level_details" }) +export default class BehaviourLevelDetails extends Model { + @Column({ type: DataType.INTEGER, allowNull: false }) + id!: number; + + @ForeignKey(() => Behaviour) + @Column({ type: DataType.INTEGER, allowNull: false }) + behaviour_id!: number; + + @Column({ type: DataType.ENUM, allowNull: false }) + level!: BehaviourLevel; + + @Column({ type: DataType.STRING, allowNull: false }) + description!: string; + + @Column({ type: DataType.STRING, allowNull: false }) + training_instructions!: string; +} diff --git a/backend/typescript/types.ts b/backend/typescript/types.ts index daf6250..8318e4c 100644 --- a/backend/typescript/types.ts +++ b/backend/typescript/types.ts @@ -33,6 +33,8 @@ export type AuthDTO = Token & UserDTO; export type Letters = "A" | "B" | "C" | "D"; +export type BehaviourLevel = 1 | 2 | 3 | 4; + const sexValues = ["M", "F"] as const; export const sexEnum: Sex[] = [...sexValues]; From 7703a8d1284ec54b9230bad15f45c2d97bf6690c Mon Sep 17 00:00:00 2001 From: vips11 <66628544+vips11@users.noreply.github.com> Date: Thu, 24 Oct 2024 20:01:47 -0400 Subject: [PATCH 11/16] Addressed PR comments --- ...07.create-behaviour-level-details-table.ts | 19 ++++++++++++++++--- ...24.10.22T19.42.10.add-behaviour-columns.ts | 10 ++++++++-- ...odel.ts => behaviourLevelDetails.model.ts} | 18 +++++++----------- backend/typescript/types.ts | 2 -- 4 files changed, 31 insertions(+), 18 deletions(-) rename backend/typescript/models/{behvaiourLevelDetails.model.ts => behaviourLevelDetails.model.ts} (50%) diff --git a/backend/typescript/migrations/2024.10.22T19.38.07.create-behaviour-level-details-table.ts b/backend/typescript/migrations/2024.10.22T19.38.07.create-behaviour-level-details-table.ts index 87c465f..59f3180 100644 --- a/backend/typescript/migrations/2024.10.22T19.38.07.create-behaviour-level-details-table.ts +++ b/backend/typescript/migrations/2024.10.22T19.38.07.create-behaviour-level-details-table.ts @@ -3,6 +3,7 @@ import { DataType } from "sequelize-typescript"; import { Migration } from "../umzug"; const TABLE_NAME = "behaviour_level_details"; +const CONSTRAINT_NAME = "unique_behaviour_level"; export const up: Migration = async ({ context: sequelize }) => { await sequelize.getQueryInterface().createTable(TABLE_NAME, { @@ -15,28 +16,40 @@ export const up: Migration = async ({ context: sequelize }) => { behaviour_id: { type: DataType.INTEGER, allowNull: false, + references: { + model: "behaviours", + key: "id", + }, }, level: { type: DataType.INTEGER, + validate: { + min: 1, + max: 4, + }, allowNull: false, }, description: { type: DataType.STRING, - allowNull: false, + allowNull: true, }, training_instructions: { type: DataType.STRING, - allowNull: false, + allowNull: true, }, }); await sequelize.getQueryInterface().addConstraint(TABLE_NAME, { fields: ["behaviour_id", "level"], type: "unique", - name: "unique_behaviour_level", + name: CONSTRAINT_NAME, }); }; export const down: Migration = async ({ context: sequelize }) => { + await sequelize + .getQueryInterface() + .removeConstraint(TABLE_NAME, CONSTRAINT_NAME); + await sequelize.getQueryInterface().dropTable(TABLE_NAME); }; diff --git a/backend/typescript/migrations/2024.10.22T19.42.10.add-behaviour-columns.ts b/backend/typescript/migrations/2024.10.22T19.42.10.add-behaviour-columns.ts index 5099b98..6807e88 100644 --- a/backend/typescript/migrations/2024.10.22T19.42.10.add-behaviour-columns.ts +++ b/backend/typescript/migrations/2024.10.22T19.42.10.add-behaviour-columns.ts @@ -1,17 +1,23 @@ import { DataType } from "sequelize-typescript"; import { Migration } from "../umzug"; +const TABLE_NAME = "behaviours"; + export const up: Migration = async ({ context: sequelize }) => { await sequelize .getQueryInterface() - .addColumn("behaviours", "parent_behaviour_id", { + .addColumn(TABLE_NAME, "parent_behaviour_id", { type: DataType.INTEGER, allowNull: true, + references: { + model: "behaviours", + key: "id", + }, }); }; export const down: Migration = async ({ context: sequelize }) => { await sequelize .getQueryInterface() - .removeColumn("behaviours", "parent_behaviour_id"); + .removeColumn(TABLE_NAME, "parent_behaviour_id"); }; diff --git a/backend/typescript/models/behvaiourLevelDetails.model.ts b/backend/typescript/models/behaviourLevelDetails.model.ts similarity index 50% rename from backend/typescript/models/behvaiourLevelDetails.model.ts rename to backend/typescript/models/behaviourLevelDetails.model.ts index e8148fb..9dcc859 100644 --- a/backend/typescript/models/behvaiourLevelDetails.model.ts +++ b/backend/typescript/models/behaviourLevelDetails.model.ts @@ -6,23 +6,19 @@ import { DataType, } from "sequelize-typescript"; import Behaviour from "./behaviour.model"; -import { BehaviourLevel } from "../types"; -@Table({ timestamps: false, tableName: "behvaiour_level_details" }) +@Table({ timestamps: false, tableName: "behaviour_level_details" }) export default class BehaviourLevelDetails extends Model { - @Column({ type: DataType.INTEGER, allowNull: false }) - id!: number; - @ForeignKey(() => Behaviour) @Column({ type: DataType.INTEGER, allowNull: false }) behaviour_id!: number; - @Column({ type: DataType.ENUM, allowNull: false }) - level!: BehaviourLevel; + @Column({ type: DataType.INTEGER, allowNull: false }) + level!: number; - @Column({ type: DataType.STRING, allowNull: false }) - description!: string; + @Column({ type: DataType.STRING, allowNull: true }) + description?: string; - @Column({ type: DataType.STRING, allowNull: false }) - training_instructions!: string; + @Column({ type: DataType.STRING, allowNull: true }) + training_instructions?: string; } diff --git a/backend/typescript/types.ts b/backend/typescript/types.ts index 8318e4c..daf6250 100644 --- a/backend/typescript/types.ts +++ b/backend/typescript/types.ts @@ -33,8 +33,6 @@ export type AuthDTO = Token & UserDTO; export type Letters = "A" | "B" | "C" | "D"; -export type BehaviourLevel = 1 | 2 | 3 | 4; - const sexValues = ["M", "F"] as const; export const sexEnum: Sex[] = [...sexValues]; From 7042219eb8e78b8a7a9c17608aa84ccc34229192 Mon Sep 17 00:00:00 2001 From: Justin Lau Date: Tue, 29 Oct 2024 19:59:38 -0400 Subject: [PATCH 12/16] Implement user management functionality with API integration and UI updates --- .../services/implementations/authService.ts | 3 +- frontend/src/APIClients/UserAPIClient.ts | 21 +++++++ .../components/pages/UserManagementPage.tsx | 63 +++++++++++++++++-- frontend/src/types/UserTypes.ts | 8 +++ 4 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 frontend/src/APIClients/UserAPIClient.ts create mode 100644 frontend/src/types/UserTypes.ts diff --git a/backend/typescript/services/implementations/authService.ts b/backend/typescript/services/implementations/authService.ts index 161bc9a..a8e24c4 100644 --- a/backend/typescript/services/implementations/authService.ts +++ b/backend/typescript/services/implementations/authService.ts @@ -172,7 +172,6 @@ class AuthService implements IAuthService { // const firebaseUser = await firebaseAdmin // .auth() // .getUser(decodedIdToken.uid); - return /* firebaseUser.emailVerified && */ roles.has(userRole); } catch (error) { return false; @@ -225,4 +224,4 @@ class AuthService implements IAuthService { } } -export default AuthService; +export default AuthService; \ No newline at end of file diff --git a/frontend/src/APIClients/UserAPIClient.ts b/frontend/src/APIClients/UserAPIClient.ts new file mode 100644 index 0000000..fcea95d --- /dev/null +++ b/frontend/src/APIClients/UserAPIClient.ts @@ -0,0 +1,21 @@ +import { User } from "../types/UserTypes"; +import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; +import baseAPIClient from "./BaseAPIClient"; +import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils"; + +const get = async (): Promise => { + const bearerToken = `Bearer ${getLocalStorageObjProperty( + AUTHENTICATED_USER_KEY, + "accessToken", + )}`; + try { + const { data } = await baseAPIClient.get("/users", { + headers: { Authorization: bearerToken }, + }); + return data; + } catch (error) { + throw new Error(`Failed to get entity: ${error}`); + } +}; + +export default { get }; \ No newline at end of file diff --git a/frontend/src/components/pages/UserManagementPage.tsx b/frontend/src/components/pages/UserManagementPage.tsx index 21431ca..90e40e3 100644 --- a/frontend/src/components/pages/UserManagementPage.tsx +++ b/frontend/src/components/pages/UserManagementPage.tsx @@ -1,13 +1,66 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + VStack, + Button, +} from "@chakra-ui/react"; +import UserAPIClient from "../../APIClients/UserAPIClient"; +import { User } from "../../types/UserTypes"; import MainPageButton from "../common/MainPageButton"; const UserManagementPage = (): React.ReactElement => { + const [users, setUsers] = useState([]); + + const getUsers = async () => { + try { + const fetchedUsers = await UserAPIClient.get(); + if (fetchedUsers != null) { + setUsers(fetchedUsers); + } + } catch (error) { + /* TODO: error handling */ + } + }; + + useEffect(() => { + getUsers(); + }, []); + return ( -
-

User Management

- +
+

User Management Page

+ + + + + + + + + + + + {users.map((user) => ( + + + + + + ))} + +
First NameLast NameRole
{user.firstName}{user.lastName}{user.role}
+
+ + +
); }; -export default UserManagementPage; +export default UserManagementPage; \ No newline at end of file diff --git a/frontend/src/types/UserTypes.ts b/frontend/src/types/UserTypes.ts new file mode 100644 index 0000000..ec548db --- /dev/null +++ b/frontend/src/types/UserTypes.ts @@ -0,0 +1,8 @@ +export type User = { + id: number; + firstName: string; + lastName: string; + email: string; + role: string; + status: string; +}; From 09773b83157a36f8bf634466b75e50386f80f931 Mon Sep 17 00:00:00 2001 From: Justin Lau Date: Tue, 29 Oct 2024 20:02:38 -0400 Subject: [PATCH 13/16] Update header title on User Management Page --- frontend/src/components/pages/UserManagementPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/pages/UserManagementPage.tsx b/frontend/src/components/pages/UserManagementPage.tsx index 90e40e3..f0fd91f 100644 --- a/frontend/src/components/pages/UserManagementPage.tsx +++ b/frontend/src/components/pages/UserManagementPage.tsx @@ -34,7 +34,7 @@ const UserManagementPage = (): React.ReactElement => { return (
-

User Management Page

+

User Management

From f90e2ec09b3a1b8ccd9684f23cce639b8180d8f5 Mon Sep 17 00:00:00 2001 From: Justin Lau Date: Tue, 29 Oct 2024 20:05:54 -0400 Subject: [PATCH 14/16] Fix formatting in authService.ts to ensure proper file structure --- backend/typescript/services/implementations/authService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/typescript/services/implementations/authService.ts b/backend/typescript/services/implementations/authService.ts index a8e24c4..afd4cf5 100644 --- a/backend/typescript/services/implementations/authService.ts +++ b/backend/typescript/services/implementations/authService.ts @@ -224,4 +224,4 @@ class AuthService implements IAuthService { } } -export default AuthService; \ No newline at end of file +export default AuthService; From 7d8c443bf9e9827d3bc86fca9d1d03cfbdcacd46 Mon Sep 17 00:00:00 2001 From: Justin Lau Date: Tue, 29 Oct 2024 20:09:17 -0400 Subject: [PATCH 15/16] Fix formatting in UserAPIClient.ts and UserManagementPage.tsx to ensure proper file structure --- frontend/src/APIClients/UserAPIClient.ts | 2 +- frontend/src/components/pages/UserManagementPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/APIClients/UserAPIClient.ts b/frontend/src/APIClients/UserAPIClient.ts index fcea95d..20ddc2e 100644 --- a/frontend/src/APIClients/UserAPIClient.ts +++ b/frontend/src/APIClients/UserAPIClient.ts @@ -18,4 +18,4 @@ const get = async (): Promise => { } }; -export default { get }; \ No newline at end of file +export default { get }; diff --git a/frontend/src/components/pages/UserManagementPage.tsx b/frontend/src/components/pages/UserManagementPage.tsx index f0fd91f..cdc53b0 100644 --- a/frontend/src/components/pages/UserManagementPage.tsx +++ b/frontend/src/components/pages/UserManagementPage.tsx @@ -63,4 +63,4 @@ const UserManagementPage = (): React.ReactElement => { ); }; -export default UserManagementPage; \ No newline at end of file +export default UserManagementPage; From 83d222fc4dc225540a28dfa76506554203641ccb Mon Sep 17 00:00:00 2001 From: liya-zhu Date: Tue, 29 Oct 2024 20:35:42 -0400 Subject: [PATCH 16/16] Moved BelongsToMany relationship to new file --- ...00.23.02.create-user-animal-types-table.ts | 16 +++++------ backend/typescript/models/animalType.model.ts | 9 +------ backend/typescript/models/index.ts | 12 ++++++++- .../typescript/models/modelRelationships.ts | 7 +++++ backend/typescript/models/user.model.ts | 10 +------ .../typescript/models/userAnimalType.model.ts | 27 +++++++++---------- 6 files changed, 41 insertions(+), 40 deletions(-) create mode 100644 backend/typescript/models/modelRelationships.ts diff --git a/backend/typescript/migrations/2024.10.04T00.23.02.create-user-animal-types-table.ts b/backend/typescript/migrations/2024.10.04T00.23.02.create-user-animal-types-table.ts index 3193e89..6b8b7af 100644 --- a/backend/typescript/migrations/2024.10.04T00.23.02.create-user-animal-types-table.ts +++ b/backend/typescript/migrations/2024.10.04T00.23.02.create-user-animal-types-table.ts @@ -12,10 +12,10 @@ export const up: Migration = async ({ context: sequelize }) => { primaryKey: true, references: { model: "users", - key: "id" + key: "id", }, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', + onUpdate: "CASCADE", + onDelete: "CASCADE", }, animal_type_id: { type: DataType.INTEGER, @@ -23,13 +23,13 @@ export const up: Migration = async ({ context: sequelize }) => { primaryKey: true, references: { model: "animal_types", - key: "id" + key: "id", }, - onUpdate: 'CASCADE', - onDelete: 'CASCADE' - } + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, }); -} +}; export const down: Migration = async ({ context: sequelize }) => { await sequelize.getQueryInterface().dropTable(TABLE_NAME); diff --git a/backend/typescript/models/animalType.model.ts b/backend/typescript/models/animalType.model.ts index 84ef34d..3cc102a 100644 --- a/backend/typescript/models/animalType.model.ts +++ b/backend/typescript/models/animalType.model.ts @@ -1,14 +1,7 @@ -import { Column, Model, Table, BelongsToMany } from "sequelize-typescript"; -import type User from "./user.model"; +import { Column, Model, Table } from "sequelize-typescript"; @Table({ timestamps: false, tableName: "animal_types" }) export default class AnimalType extends Model { @Column({}) animal_type_name!: string; - - @BelongsToMany( - () => import("./user.model").then((mod) => mod.default), - () => import("./userAnimalType.model").then((mod) => mod.default), - ) - users!: User[]; } diff --git a/backend/typescript/models/index.ts b/backend/typescript/models/index.ts index 0ca9ed2..68a973e 100644 --- a/backend/typescript/models/index.ts +++ b/backend/typescript/models/index.ts @@ -1,5 +1,9 @@ import * as path from "path"; import { Sequelize } from "sequelize-typescript"; +import User from "./user.model"; +import AnimalType from "./animalType.model"; +import UserAnimalType from "./userAnimalType.model"; +import defineRelationships from "./modelRelationships"; const DATABASE_URL = process.env.NODE_ENV === "production" @@ -8,6 +12,12 @@ const DATABASE_URL = : `postgres://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.DB_HOST}:5432/${process.env.POSTGRES_DB_DEV}`; /* eslint-disable-next-line import/prefer-default-export */ -export const sequelize = new Sequelize(DATABASE_URL, { +const sequelize = new Sequelize(DATABASE_URL, { models: [path.join(__dirname, "/*.model.ts")], }); + +sequelize.addModels([User, AnimalType, UserAnimalType]); + +defineRelationships(); + +export { sequelize, User, AnimalType, UserAnimalType }; diff --git a/backend/typescript/models/modelRelationships.ts b/backend/typescript/models/modelRelationships.ts new file mode 100644 index 0000000..3dfcad1 --- /dev/null +++ b/backend/typescript/models/modelRelationships.ts @@ -0,0 +1,7 @@ +import User from "./user.model"; +import AnimalType from "./animalType.model"; +import UserAnimalType from "./userAnimalType.model"; + +export default function defineRelationships(): void { + User.belongsToMany(AnimalType, { through: UserAnimalType }); +} diff --git a/backend/typescript/models/user.model.ts b/backend/typescript/models/user.model.ts index bfe6f24..928c641 100644 --- a/backend/typescript/models/user.model.ts +++ b/backend/typescript/models/user.model.ts @@ -4,10 +4,8 @@ import { Model, Table, AllowNull, - BelongsToMany, } from "sequelize-typescript"; import { Role, UserStatus } from "../types"; -import type AnimalType from "./animalType.model"; @Table({ tableName: "users" }) export default class User extends Model { @@ -41,10 +39,4 @@ export default class User extends Model { @Column({ type: DataType.ENUM("Active", "Inactive"), allowNull: false }) status!: UserStatus; - - @BelongsToMany( - () => import("./animalType.model").then((mod) => mod.default), - () => import("./userAnimalType.model").then((mod) => mod.default), - ) - animalTypes!: AnimalType[]; -} \ No newline at end of file +} diff --git a/backend/typescript/models/userAnimalType.model.ts b/backend/typescript/models/userAnimalType.model.ts index 0047bc1..c5503b5 100644 --- a/backend/typescript/models/userAnimalType.model.ts +++ b/backend/typescript/models/userAnimalType.model.ts @@ -1,19 +1,19 @@ -import { - Column, - DataType, - Model, - Table, - ForeignKey, - PrimaryKey -} from 'sequelize-typescript'; -import User from './user.model'; -import AnimalType from './animalType.model'; +import { + Column, + DataType, + Model, + Table, + ForeignKey, + PrimaryKey, +} from "sequelize-typescript"; +import User from "./user.model"; +import AnimalType from "./animalType.model"; @Table({ - tableName: 'User_AnimalTypes', - timestamps: true + tableName: "User_AnimalTypes", + timestamps: true, }) -export default class UserAnimalType extends Model { +export default class UserAnimalType extends Model { @ForeignKey(() => User) @PrimaryKey @Column({ type: DataType.INTEGER, allowNull: false }) @@ -24,4 +24,3 @@ export default class UserAnimalType extends Model { @Column({ type: DataType.INTEGER, allowNull: false }) animal_type_id!: number; } -