diff --git a/server/src/common/errors/index.ts b/server/src/common/errors/index.ts index b97fd8c..a81dbee 100644 --- a/server/src/common/errors/index.ts +++ b/server/src/common/errors/index.ts @@ -5,3 +5,35 @@ export class NotImplementedError extends Error { Object.setPrototypeOf(this, NotImplementedError.prototype); } } + +export class NotFoundError extends Error { + constructor(message: string = 'The requested resource was not found.') { + super(message); + this.name = 'NotFoundError'; + Object.setPrototypeOf(this, NotFoundError.prototype); + } +} + +export class BadRequestError extends Error { + constructor(message: string = 'Bad request.') { + super(message); + this.name = 'BadRequestError'; + Object.setPrototypeOf(this, BadRequestError.prototype); + } +} + +export class UnauthorizedError extends Error { + constructor(message: string = 'Unauthorized.') { + super(message); + this.name = 'UnauthorizedError'; + Object.setPrototypeOf(this, UnauthorizedError.prototype); + } +} + +export class ForbiddenError extends Error { + constructor(message: string = 'Forbidden.') { + super(message); + this.name = 'ForbiddenError'; + Object.setPrototypeOf(this, ForbiddenError.prototype); + } +} diff --git a/server/src/common/mongoose-plugins.ts b/server/src/common/mongoose-plugins.ts index 2845a65..d3d90c5 100644 --- a/server/src/common/mongoose-plugins.ts +++ b/server/src/common/mongoose-plugins.ts @@ -27,3 +27,8 @@ export const softDeletePlugin = (schema: mongoose.Schema) => { this.deletedBy = null; }; }; + +export type SoftDeleteMethods = { + softDelete(deletedBy: string | null): void; + restore(): void; +}; diff --git a/server/src/middlewares/error-handler.ts b/server/src/middlewares/error-handler.ts index 8c83c79..4309edd 100644 --- a/server/src/middlewares/error-handler.ts +++ b/server/src/middlewares/error-handler.ts @@ -28,6 +28,30 @@ export const errorHandler = ( }); } + if (err.name === 'BadRequestError') { + return res.status(400).json({ + error: err.message, + }); + } + + if (err.name === 'NotFoundError') { + return res.status(404).json({ + error: err.message, + }); + } + + if (err.name === 'UnauthorizedError') { + return res.status(401).json({ + error: err.message, + }); + } + + if (err.name === 'ForbiddenError') { + return res.status(403).json({ + error: err.message, + }); + } + // Handle mongodb cast errors if (err.name === 'CastError') { return res.status(400).json({ diff --git a/server/src/modules/user/controller.ts b/server/src/modules/user/controller.ts index 3be9376..516f22c 100644 --- a/server/src/modules/user/controller.ts +++ b/server/src/modules/user/controller.ts @@ -27,19 +27,15 @@ export const updateUserById = async (req: Request, res: Response) => { }; export const deleteUserById = async (req: Request, res: Response) => { - const deleteUser = await userService.deleteUserById(req.params.id); + await userService.deleteUser(req.params.id, req.user); - if (!deleteUser) return res.status(404).json({ message: 'User not found' }); return res.status(200).json({ message: 'User delete successful' }); }; export const restoreUser = async (req: Request, res: Response) => { - const user = await userService.restoreUser(req.params.id); + await userService.restoreUser(req.params.id); - if (!user) return res.status(404).json({ message: 'No user found' }); - - return res.status(201).json({ - user: user, + return res.status(200).json({ message: 'User restored successful', }); }; diff --git a/server/src/modules/user/model.ts b/server/src/modules/user/model.ts index 593b93f..4f7e58d 100644 --- a/server/src/modules/user/model.ts +++ b/server/src/modules/user/model.ts @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; import { User } from '@/types'; -import { softDeletePlugin } from '@/common/mongoose-plugins'; +import { SoftDeleteMethods, softDeletePlugin } from '@/common/mongoose-plugins'; const userSchema = new mongoose.Schema( { @@ -18,6 +18,6 @@ const userSchema = new mongoose.Schema( userSchema.plugin(softDeletePlugin); -export type UserDocument = User & mongoose.Document; +export type UserDocument = User & mongoose.Document & SoftDeleteMethods; export const UserModel = mongoose.model('User', userSchema); diff --git a/server/src/modules/user/service.ts b/server/src/modules/user/service.ts index 14fa0c2..b56a67b 100644 --- a/server/src/modules/user/service.ts +++ b/server/src/modules/user/service.ts @@ -1,3 +1,4 @@ +import { Request } from 'express'; import z from 'zod'; import bcrypt from 'bcrypt'; import type { QueryFilter } from 'mongoose'; @@ -7,6 +8,7 @@ import { envConfig } from '@/config'; import { PageResult, UpdateUser, User } from '@/types'; import { UserDocument, UserModel } from './model'; +import { ForbiddenError, NotFoundError } from '@/common/errors'; export const getHashedPassword = async (password: string) => bcrypt.hash(password, envConfig.BCRYPT_SALT_ROUNDS); @@ -50,38 +52,31 @@ export const updateUserById = async ( }); }; -export const deleteUserById = async (_id: string) => { +export const deleteUser = async (_id: string, reqUser: Request['user']) => { const user = await UserModel.findById(_id); - if (!user) return false; + if (!user) { + throw new NotFoundError('User not found'); + } - return UserModel.findByIdAndUpdate( - _id, - { - deleted: true, - deletedAt: new Date().toISOString(), - }, - { - new: true, - }, - ); + if (!reqUser) { + throw new ForbiddenError('Unauthorized'); + } + + await user.softDelete(reqUser?._id); }; export const restoreUser = async (_id: string) => { - const user = await UserModel.findById(_id); + const user = await UserModel.findOne({ + _id, + deleted: true, + }); - if (!user) return false; + if (!user) { + throw new NotFoundError('User not found'); + } - return UserModel.findByIdAndUpdate( - _id, - { - deleted: false, - deletedAt: null, - }, - { - new: true, - }, - ); + await user.restore(); }; export const findUserByEmail = async (email: string) => {