diff --git a/backend/typescript/middlewares/validators/userValidators.ts b/backend/typescript/middlewares/validators/userValidators.ts index 9e08d29..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 && @@ -63,16 +60,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.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..6b8b7af --- /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/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..59f3180 --- /dev/null +++ b/backend/typescript/migrations/2024.10.22T19.38.07.create-behaviour-level-details-table.ts @@ -0,0 +1,55 @@ +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, { + id: { + type: DataType.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true, + }, + 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: true, + }, + training_instructions: { + type: DataType.STRING, + allowNull: true, + }, + }); + + await sequelize.getQueryInterface().addConstraint(TABLE_NAME, { + fields: ["behaviour_id", "level"], + type: "unique", + 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 new file mode 100644 index 0000000..6807e88 --- /dev/null +++ b/backend/typescript/migrations/2024.10.22T19.42.10.add-behaviour-columns.ts @@ -0,0 +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(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(TABLE_NAME, "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/behaviourLevelDetails.model.ts b/backend/typescript/models/behaviourLevelDetails.model.ts new file mode 100644 index 0000000..9dcc859 --- /dev/null +++ b/backend/typescript/models/behaviourLevelDetails.model.ts @@ -0,0 +1,24 @@ +import { + Column, + Model, + Table, + ForeignKey, + DataType, +} from "sequelize-typescript"; +import Behaviour from "./behaviour.model"; + +@Table({ timestamps: false, tableName: "behaviour_level_details" }) +export default class BehaviourLevelDetails extends Model { + @ForeignKey(() => Behaviour) + @Column({ type: DataType.INTEGER, allowNull: false }) + behaviour_id!: number; + + @Column({ type: DataType.INTEGER, allowNull: false }) + level!: number; + + @Column({ type: DataType.STRING, allowNull: true }) + description?: string; + + @Column({ type: DataType.STRING, allowNull: true }) + training_instructions?: string; +} 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/userAnimalType.model.ts b/backend/typescript/models/userAnimalType.model.ts new file mode 100644 index 0000000..c5503b5 --- /dev/null +++ b/backend/typescript/models/userAnimalType.model.ts @@ -0,0 +1,26 @@ +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; +} 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 0f3ad1d..5e57120 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, @@ -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, @@ -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,12 +89,17 @@ 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) }); + } } } } }); +// This endpoint is for testing purposes /* Create a user */ userRouter.post("/", createUserDtoValidator, async (req, res) => { try { @@ -100,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: req.body.status ?? UserStatus.ACTIVE, // TODO: make this default to inactive once user registration flow is done + 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) { @@ -119,26 +124,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 +195,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 +219,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 +247,13 @@ userRouter.delete("/", async (req, res) => { .json({ error: "email query parameter must be a string." }); } else { try { + const user: UserDTO = await userService.getUserByEmail(email); + if (user.status === "Active") { + res.status(400).json({ + error: "user status must be 'Inactive' or 'Invited' for deletion.", + }); + return; + } await userService.deleteUserByEmail(email); res.status(204).send(); } catch (error: unknown) { diff --git a/backend/typescript/services/implementations/authService.ts b/backend/typescript/services/implementations/authService.ts index 0f1c543..afd4cf5 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,10 @@ class AuthService implements IAuthService { const userRole = await this.userService.getUserRoleByAuthId( decodedIdToken.uid, ); - - const firebaseUser = await firebaseAdmin - .auth() - .getUser(decodedIdToken.uid); - - return firebaseUser.emailVerified && roles.has(userRole); + // const firebaseUser = await firebaseAdmin + // .auth() + // .getUser(decodedIdToken.uid); + 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 5606dc9..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, @@ -240,6 +235,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 +270,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/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"; diff --git a/frontend/src/APIClients/UserAPIClient.ts b/frontend/src/APIClients/UserAPIClient.ts new file mode 100644 index 0000000..20ddc2e --- /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 }; diff --git a/frontend/src/components/pages/UserManagementPage.tsx b/frontend/src/components/pages/UserManagementPage.tsx index 21431ca..cdc53b0 100644 --- a/frontend/src/components/pages/UserManagementPage.tsx +++ b/frontend/src/components/pages/UserManagementPage.tsx @@ -1,11 +1,64 @@ -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

- + + + + + + + + + + + + {users.map((user) => ( + + + + + + ))} + +
First NameLast NameRole
{user.firstName}{user.lastName}{user.role}
+
+ + +
); }; 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; +};