diff --git a/app-backend/src/container/user.container.js b/app-backend/src/container/user.container.js new file mode 100644 index 000000000..55b569ca8 --- /dev/null +++ b/app-backend/src/container/user.container.js @@ -0,0 +1,9 @@ +import UserController from '../controllers/user.controller.js'; +import UserRepository from '../repositories/user.repository.js'; +import UserService from '../services/user.service.js'; + +const userRepository = new UserRepository(); +const userService = new UserService(userRepository); +const userController = new UserController(userService); + +export default userController; \ No newline at end of file diff --git a/app-backend/src/controllers/user.controller.js b/app-backend/src/controllers/user.controller.js index 11b779a94..27d76650c 100644 --- a/app-backend/src/controllers/user.controller.js +++ b/app-backend/src/controllers/user.controller.js @@ -1,286 +1,181 @@ -import User from '../models/User.js'; -import { ACTIONS } from "../middleware/logger.js"; - -/** - * @desc View logged-in user's profile - * @route GET /api/v1/users/me - * @access Private (all roles) - */ -export const getMyProfile = async (req, res) => { - const user = await User.findById(req.user.id).select('-password'); - res.json(user); -}; - -/** - * @desc Admin: List all users - * @route GET /api/v1/users - * @access Private/Admins - */ -export const listUsers = async (req, res) => { - try { - const users = await User.find().select('-password'); - res.json({ total: users.length, users }); - } catch (e) { - res.status(500).json({ message: e.message }); +class UserController { + constructor(userService) { + this.userService = userService; } -}; - -/** - * @desc Update logged-in user's profile - * @route PUT /api/v1/users/me - * @access Private (all roles) - */ -export const updateMyProfile = async (req, res) => { - const fieldsToUpdate = { ...req.body }; - delete fieldsToUpdate.role; // prevent role changes - delete fieldsToUpdate.password; // don’t allow password here - - const updatedUser = await User.findByIdAndUpdate( - req.user.id, - fieldsToUpdate, - { new: true, runValidators: true } - ).select('-password'); - - res.json(updatedUser); -}; - -/** - * @desc Admin: View any user profile - * @route GET /api/v1/users/:id - * @access Private/Admin - */ -export const adminGetUserProfile = async (req, res) => { - const user = await User.findById(req.params.id).select('-password'); - if (!user) return res.status(404).json({ message: 'User not found' }); - res.json(user); -}; - -/** - * @desc Admin: Update any user's profile - * @route PUT /api/v1/users/:id - * @access Private/Admin - */ -export const adminUpdateUserProfile = async (req, res) => { - const fieldsToUpdate = { ...req.body }; - delete fieldsToUpdate.password; // separate password endpoint if needed - const updatedUser = await User.findByIdAndUpdate( - req.params.id, - fieldsToUpdate, - { new: true, runValidators: true } - ).select('-password'); - - if (!updatedUser) return res.status(404).json({ message: 'User not found' }); - - await req.audit.log(req.user.id, ACTIONS.PROFILE_UPDATED, { - updatedUserId: req.params.id, - updatedFields: Object.keys(fieldsToUpdate), - }); - - res.json(updatedUser); -}; - -/** - * @desc Get all guards (Admin + Employee only) - * @route GET /api/v1/users/guards - * @access Private/Admin,Employee - */ -export const getAllGuards = async (req, res) => { - const guards = await User.find({ role: 'guard' }).select('-password'); - res.json(guards); -}; - -/** - * @desc Admin: Delete a user - * @route DELETE /api/v1/users/:userId - * @access Private (Admin, Super Admin) - */ -export const deleteUser = async (req, res) => { - try { - const user = await User.findByIdAndDelete(req.params.userId); - if (!user) { - return res.status(404).json({ message: 'User not found' }); + getMyProfile = async (req, res) => { + try { + const user = await this.userService.getMyProfile(req.user.id); + return res.status(200).json(user); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - await req.audit?.log(req.user.id, ACTIONS.USER_DELETED, { - deletedUserId: req.params.userId, - deletedUserEmail: user.email, - }); - - return res.json({ message: 'User deleted successfully' }); - } catch (e) { - return res.status(500).json({ message: e.message }); - } -}; - -/** - * @desc Get logged-in employer's profile (Employer only) - * @route GET /api/v1/users/profile - * @access Private (Employer only) - */ -export const getEmployerProfile = async (req, res) => { - try { - if (req.user.role !== 'employer') { - return res.status(403).json({ message: 'Access denied.' }); + }; + + listUsers = async (req, res) => { + try { + const result = await this.userService.listUsers(); + return res.status(200).json(result); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - const employer = await User.findById(req.user.id).select('-password'); - if (!employer) { - return res.status(404).json({ message: 'Employer not found' }); + }; + + updateMyProfile = async (req, res) => { + try { + const updatedUser = await this.userService.updateMyProfile( + req.user.id, + req.body + ); + + return res.status(200).json(updatedUser); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - res.status(200).json(employer); - } catch (err) { - res.status(500).json({ message: err.message }); - } -}; - -/** - * @desc Update logged-in employer's profile (Employer only) - * @route PUT /api/v1/users/profile - * @access Private (Employer only) - */ -export const updateEmployerProfile = async (req, res) => { - try { - if (req.user.role !== 'employer') { - return res.status(403).json({ message: 'Access denied.' }); + }; + + adminGetUserProfile = async (req, res) => { + try { + const user = await this.userService.adminGetUserProfile(req.params.userId); + return res.status(200).json(user); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - const fieldsToUpdate = { ...req.body }; - delete fieldsToUpdate.role; - delete fieldsToUpdate.password; - - const updatedEmployer = await User.findByIdAndUpdate( - req.user.id, - fieldsToUpdate, - { new: true, runValidators: true } - ).select('-password'); - - if (!updatedEmployer) return res.status(404).json({ message: 'Employer not found' }); - res.status(200).json(updatedEmployer); - } catch (err) { - res.status(500).json({ message: err.message }); - } -}; - -/** - * @desc Register or update a push token for the logged-in user - * @route POST /api/v1/users/push-token - * @access Private (all roles) - */ -export const registerPushToken = async (req, res) => { - try { - const { token, platform, deviceId } = req.body; - - if (!token || typeof token !== 'string') { - return res.status(400).json({ message: 'Push token is required.' }); + }; + + adminUpdateUserProfile = async (req, res) => { + try { + const updatedUser = await this.userService.adminUpdateUserProfile( + req.user.id, + req.params.userId, + req.body, + req.audit + ); + + return res.status(200).json(updatedUser); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - const user = await User.findById(req.user.id); - if (!user) { - return res.status(404).json({ message: 'User not found' }); + }; + + getAllGuards = async (req, res) => { + try { + const guards = await this.userService.getAllGuards(); + return res.status(200).json(guards); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - const existing = user.pushTokens?.find((item) => item.token === token); - if (existing) { - existing.platform = platform ?? existing.platform; - existing.deviceId = deviceId ?? existing.deviceId; - existing.updatedAt = new Date(); - } else { - user.pushTokens = [ - ...(user.pushTokens ?? []), - { - token, - platform, - deviceId, - updatedAt: new Date(), - }, - ]; + }; + + deleteUser = async (req, res) => { + try { + const result = await this.userService.deleteUser( + req.user.id, + req.params.userId, + req.audit + ); + + return res.status(200).json(result); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - await user.save(); - - return res.status(200).json({ message: 'Push token registered.' }); - } catch (err) { - return res.status(500).json({ message: err.message }); - } -}; -/** - * @desc Add a guard to favourites - * @route POST /api/v1/users/favourites/:guardId - * @access Private (Employer only) - */ -export const addFavouriteGuard = async (req, res) => { - try { - if (req.user.role !== 'employer') { - return res.status(403).json({ message: 'Only employers can favourite guards.' }); + }; + + getEmployerProfile = async (req, res) => { + try { + const employer = await this.userService.getEmployerProfile(req.user); + return res.status(200).json(employer); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - const { guardId } = req.params; - - const guard = await User.findById(guardId); - if (!guard || guard.role !== 'guard') { - return res.status(404).json({ message: 'Guard not found' }); + }; + + updateEmployerProfile = async (req, res) => { + try { + const updatedEmployer = await this.userService.updateEmployerProfile( + req.user, + req.body + ); + + return res.status(200).json(updatedEmployer); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - const employer = await User.findById(req.user.id); - - if (!employer.favourites.includes(guardId)) { - employer.favourites.push(guardId); - await employer.save(); + }; + + registerPushToken = async (req, res) => { + try { + const result = await this.userService.registerPushToken( + req.user.id, + req.body + ); + + return res.status(200).json(result); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - res.status(200).json({ message: 'Guard added to favourites', favourites: employer.favourites }); - } catch (err) { - res.status(500).json({ message: err.message }); - } -}; - - -/** - * @desc Remove a guard from favourites - * @route DELETE /api/v1/users/favourites/:guardId - * @access Private (Employer only) - */ -export const removeFavouriteGuard = async (req, res) => { - try { - if (req.user.role !== 'employer') { - return res.status(403).json({ message: 'Only employers can modify favourites.' }); + }; + + addFavouriteGuard = async (req, res) => { + try { + const result = await this.userService.addFavouriteGuard( + req.user, + req.params.guardId + ); + + return res.status(200).json(result); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } - - const { guardId } = req.params; - - const employer = await User.findById(req.user.id); - - employer.favourites = employer.favourites.filter( - (id) => id.toString() !== guardId - ); - - await employer.save(); - - res.status(200).json({ message: 'Guard removed from favourites', favourites: employer.favourites }); - } catch (err) { - res.status(500).json({ message: err.message }); - } -}; - - -/** - * @desc Get favourite guards list - * @route GET /api/v1/users/favourites - * @access Private (Employer only) - */ -export const getFavouriteGuards = async (req, res) => { - try { - if (req.user.role !== 'employer') { - return res.status(403).json({ message: 'Only employers can view favourites.' }); + }; + + removeFavouriteGuard = async (req, res) => { + try { + const result = await this.userService.removeFavouriteGuard( + req.user, + req.params.guardId + ); + + return res.status(200).json(result); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); } + }; + + getFavouriteGuards = async (req, res) => { + try { + const favourites = await this.userService.getFavouriteGuards(req.user); + return res.status(200).json(favourites); + } catch (err) { + return res + .status(err.statusCode || 500) + .json({ message: err.message }); + } + }; +} - const employer = await User.findById(req.user.id) - .populate('favourites', '-password'); - - res.status(200).json(employer.favourites); - } catch (err) { - res.status(500).json({ message: err.message }); - } -}; \ No newline at end of file +export default UserController; \ No newline at end of file diff --git a/app-backend/src/repositories/user.repository.js b/app-backend/src/repositories/user.repository.js new file mode 100644 index 000000000..c26a2a853 --- /dev/null +++ b/app-backend/src/repositories/user.repository.js @@ -0,0 +1,40 @@ +import User from '../models/User.js'; + +class UserRepository { + async findById(userId) { + return User.findById(userId); + } + + async findByIdWithoutPassword(userId) { + return User.findById(userId).select('-password'); + } + + async findAllWithoutPassword() { + return User.find().select('-password'); + } + + async updateByIdWithoutPassword(userId, fieldsToUpdate) { + return User.findByIdAndUpdate(userId, fieldsToUpdate, { + new: true, + runValidators: true, + }).select('-password'); + } + + async deleteById(userId) { + return User.findByIdAndDelete(userId); + } + + async findGuardsWithoutPassword() { + return User.find({ role: 'guard' }).select('-password'); + } + + async save(user) { + return user.save(); + } + + async findEmployerFavourites(userId) { + return User.findById(userId).populate('favourites', '-password'); + } +} + +export default UserRepository; \ No newline at end of file diff --git a/app-backend/src/routes/user.routes.js b/app-backend/src/routes/user.routes.js index b3e106bf5..863bf4c0e 100644 --- a/app-backend/src/routes/user.routes.js +++ b/app-backend/src/routes/user.routes.js @@ -8,21 +8,7 @@ import { requireSameBranchAsTargetUser, ROLES, } from '../middleware/rbac.js'; -import { - getMyProfile, - updateMyProfile, - getEmployerProfile, - updateEmployerProfile, - adminGetUserProfile, - adminUpdateUserProfile, - getAllGuards, - listUsers, - deleteUser, - addFavouriteGuard, - removeFavouriteGuard, - getFavouriteGuards, - registerPushToken -} from '../controllers/user.controller.js'; +import userController from '../container/user.container.js'; const router = express.Router(); @@ -46,6 +32,8 @@ const router = express.Router(); * description: Successfully retrieved profile. * 401: * description: Unauthorized + * 404: + * description: User not found * put: * summary: Update logged-in user's profile * tags: [Users] @@ -64,25 +52,26 @@ const router = express.Router(); * email: * type: string * example: john.doe@example.com - * password: + * phone: * type: string - * example: newPassword123 - * role: + * example: "+61400123456" + * address: * type: string - * enum: [user, admin] - * example: user + * example: 123 Main Street * responses: * 200: * description: Successfully updated profile. * 401: * description: Unauthorized + * 404: + * description: User not found * 422: * description: Validation error */ router .route('/me') - .get(auth, loadUser, getMyProfile) - .put(auth, loadUser, updateMyProfile); + .get(auth, loadUser, userController.getMyProfile) + .put(auth, loadUser, userController.updateMyProfile); /** * @swagger @@ -103,19 +92,29 @@ router * properties: * token: * type: string + * example: expo_push_token_here * platform: * type: string + * example: android * deviceId: * type: string + * example: device-123 * responses: * 200: - * description: Token registered + * description: Push token registered successfully. * 400: - * description: Validation error + * description: Push token is required. * 401: * description: Unauthorized + * 404: + * description: User not found */ -router.post('/push-token', auth, loadUser, registerPushToken); +router.post( + '/push-token', + auth, + loadUser, + userController.registerPushToken +); /** * @swagger @@ -132,6 +131,8 @@ router.post('/push-token', auth, loadUser, registerPushToken); * description: Unauthorized * 403: * description: Forbidden (only employers) + * 404: + * description: Employer not found * put: * summary: Update logged-in employer's profile * tags: [Users] @@ -146,16 +147,16 @@ router.post('/push-token', auth, loadUser, registerPushToken); * properties: * name: * type: string - * example: "Krish Uppal" + * example: Krish Uppal * email: * type: string - * example: "krish@example.com" + * example: krish@example.com * phone: * type: string * example: "+61400123456" * address: * type: string - * example: "123 Main Street" + * example: 123 Main Street * responses: * 200: * description: Successfully updated employer profile. @@ -163,13 +164,16 @@ router.post('/push-token', auth, loadUser, registerPushToken); * description: Unauthorized * 403: * description: Forbidden + * 404: + * description: Employer not found * 422: * description: Validation error */ router .route('/profile') - .get(auth, loadUser, getEmployerProfile) - .put(auth, loadUser, updateEmployerProfile); + .get(auth, loadUser, userController.getEmployerProfile) + .put(auth, loadUser, userController.updateEmployerProfile); + /** * @swagger * /api/v1/users/favourites: @@ -181,14 +185,18 @@ router * responses: * 200: * description: List of favourite guards + * 401: + * description: Unauthorized * 403: * description: Only employers allowed + * 404: + * description: Employer not found */ router.get( '/favourites', auth, loadUser, - getFavouriteGuards + userController.getFavouriteGuards ); /** @@ -209,16 +217,18 @@ router.get( * responses: * 200: * description: Guard added to favourites + * 401: + * description: Unauthorized * 403: * description: Only employers allowed * 404: - * description: Guard not found + * description: Guard or employer not found */ router.post( '/favourites/:guardId', auth, loadUser, - addFavouriteGuard + userController.addFavouriteGuard ); /** @@ -239,15 +249,20 @@ router.post( * responses: * 200: * description: Guard removed from favourites + * 401: + * description: Unauthorized * 403: * description: Only employers allowed + * 404: + * description: Employer not found */ router.delete( '/favourites/:guardId', auth, loadUser, - removeFavouriteGuard + userController.removeFavouriteGuard ); + /** * @swagger * /api/v1/users/guards: @@ -270,7 +285,7 @@ router.get( loadUser, authorizeRoles(ROLES.ADMIN, ROLES.EMPLOYEE), authorizePermissions('user:read'), - getAllGuards + userController.getAllGuards ); /** @@ -295,20 +310,20 @@ router.get( loadUser, authorizeRoles(ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.BRANCH_ADMIN), authorizePermissions('user:read', { any: true }), - listUsers + userController.listUsers ); /** * @swagger - * /api/v1/users/{id}: + * /api/v1/users/{userId}: * get: - * summary: Admin – get another user's profile + * summary: Admin get another user's profile * tags: [Users] * security: * - bearerAuth: [] * parameters: * - in: path - * name: id + * name: userId * schema: * type: string * required: true @@ -316,20 +331,20 @@ router.get( * responses: * 200: * description: Successfully retrieved user. - * 404: - * description: User not found * 401: * description: Unauthorized * 403: * description: Forbidden + * 404: + * description: User not found * put: - * summary: Admin – update another user's profile + * summary: Admin update another user's profile * tags: [Users] * security: * - bearerAuth: [] * parameters: * - in: path - * name: id + * name: userId * required: true * schema: * type: string @@ -347,22 +362,45 @@ router.get( * email: * type: string * example: jane.smith@example.com - * password: - * type: string - * example: AdminUpdated123 * role: * type: string - * enum: [user, admin] * example: admin + * phone: + * type: string + * example: "+61400111222" + * address: + * type: string + * example: 25 Collins Street * responses: * 200: * description: Successfully updated user. + * 401: + * description: Unauthorized + * 403: + * description: Forbidden * 404: * description: User not found + * delete: + * summary: Admin delete a user + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: string + * description: User ID + * responses: + * 200: + * description: User deleted successfully. * 401: * description: Unauthorized * 403: * description: Forbidden + * 404: + * description: User not found */ router .route('/:userId') @@ -371,7 +409,7 @@ router loadUser, authorizeRoles(ROLES.SUPER_ADMIN, ROLES.ADMIN), authorizePermissions('user:read'), - adminGetUserProfile + userController.adminGetUserProfile ) .put( auth, @@ -379,14 +417,14 @@ router authorizeRoles(ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.BRANCH_ADMIN), authorizePermissions('user:write'), requireSameBranchAsTargetUser({ paramKey: 'userId' }), - adminUpdateUserProfile + userController.adminUpdateUserProfile ) .delete( auth, loadUser, authorizeRoles(ROLES.SUPER_ADMIN, ROLES.ADMIN), authorizePermissions('user:delete'), - deleteUser + userController.deleteUser ); -export default router; +export default router; \ No newline at end of file diff --git a/app-backend/src/services/user.service.js b/app-backend/src/services/user.service.js new file mode 100644 index 000000000..702ac44a1 --- /dev/null +++ b/app-backend/src/services/user.service.js @@ -0,0 +1,251 @@ +import { ACTIONS } from '../middleware/logger.js'; +import AppError from '../utils/AppError.js'; + +class UserService { + constructor(userRepository) { + this.userRepository = userRepository; + } + + sanitizeProfileUpdatePayload(payload = {}) { + const fieldsToUpdate = { ...payload }; + delete fieldsToUpdate.role; + delete fieldsToUpdate.password; + return fieldsToUpdate; + } + + sanitizeAdminProfileUpdatePayload(payload = {}) { + const fieldsToUpdate = { ...payload }; + delete fieldsToUpdate.password; + return fieldsToUpdate; + } + + async getMyProfile(userId) { + const user = await this.userRepository.findByIdWithoutPassword(userId); + + if (!user) { + throw new AppError('User not found', 404); + } + + return user; + } + + async listUsers() { + const users = await this.userRepository.findAllWithoutPassword(); + return { + total: users.length, + users, + }; + } + + async updateMyProfile(userId, payload) { + const fieldsToUpdate = this.sanitizeProfileUpdatePayload(payload); + + const updatedUser = await this.userRepository.updateByIdWithoutPassword( + userId, + fieldsToUpdate + ); + + if (!updatedUser) { + throw new AppError('User not found', 404); + } + + return updatedUser; + } + + async adminGetUserProfile(targetUserId) { + const user = await this.userRepository.findByIdWithoutPassword(targetUserId); + + if (!user) { + throw new AppError('User not found', 404); + } + + return user; + } + + async adminUpdateUserProfile(actorUserId, targetUserId, payload, auditLogger) { + const fieldsToUpdate = this.sanitizeAdminProfileUpdatePayload(payload); + + const updatedUser = await this.userRepository.updateByIdWithoutPassword( + targetUserId, + fieldsToUpdate + ); + + if (!updatedUser) { + throw new AppError('User not found', 404); + } + + if (auditLogger?.log) { + await auditLogger.log(actorUserId, ACTIONS.PROFILE_UPDATED, { + updatedUserId: targetUserId, + updatedFields: Object.keys(fieldsToUpdate), + }); + } + + return updatedUser; + } + + async getAllGuards() { + return this.userRepository.findGuardsWithoutPassword(); + } + + async deleteUser(actorUserId, targetUserId, auditLogger) { + const user = await this.userRepository.deleteById(targetUserId); + + if (!user) { + throw new AppError('User not found', 404); + } + + if (auditLogger?.log) { + await auditLogger.log(actorUserId, ACTIONS.USER_DELETED, { + deletedUserId: targetUserId, + deletedUserEmail: user.email, + }); + } + + return { message: 'User deleted successfully' }; + } + + async getEmployerProfile(currentUser) { + if (currentUser.role !== 'employer') { + throw new AppError('Access denied.', 403); + } + + const employer = await this.userRepository.findByIdWithoutPassword( + currentUser.id + ); + + if (!employer) { + throw new AppError('Employer not found', 404); + } + + return employer; + } + + async updateEmployerProfile(currentUser, payload) { + if (currentUser.role !== 'employer') { + throw new AppError('Access denied.', 403); + } + + const fieldsToUpdate = this.sanitizeProfileUpdatePayload(payload); + + const updatedEmployer = await this.userRepository.updateByIdWithoutPassword( + currentUser.id, + fieldsToUpdate + ); + + if (!updatedEmployer) { + throw new AppError('Employer not found', 404); + } + + return updatedEmployer; + } + + async registerPushToken(userId, payload) { + const { token, platform, deviceId } = payload; + + if (!token || typeof token !== 'string') { + throw new AppError('Push token is required.', 400); + } + + const user = await this.userRepository.findById(userId); + + if (!user) { + throw new AppError('User not found', 404); + } + + const existing = user.pushTokens?.find((item) => item.token === token); + + if (existing) { + existing.platform = platform ?? existing.platform; + existing.deviceId = deviceId ?? existing.deviceId; + existing.updatedAt = new Date(); + } else { + user.pushTokens = [ + ...(user.pushTokens ?? []), + { + token, + platform, + deviceId, + updatedAt: new Date(), + }, + ]; + } + + await this.userRepository.save(user); + + return { message: 'Push token registered.' }; + } + + async addFavouriteGuard(currentUser, guardId) { + if (currentUser.role !== 'employer') { + throw new AppError('Only employers can favourite guards.', 403); + } + + const guard = await this.userRepository.findById(guardId); + + if (!guard || guard.role !== 'guard') { + throw new AppError('Guard not found', 404); + } + + const employer = await this.userRepository.findById(currentUser.id); + + if (!employer) { + throw new AppError('Employer not found', 404); + } + + const alreadyExists = employer.favourites.some( + (id) => id.toString() === guardId + ); + + if (!alreadyExists) { + employer.favourites.push(guardId); + await this.userRepository.save(employer); + } + + return { + message: 'Guard added to favourites', + favourites: employer.favourites, + }; + } + + async removeFavouriteGuard(currentUser, guardId) { + if (currentUser.role !== 'employer') { + throw new AppError('Only employers can modify favourites.', 403); + } + + const employer = await this.userRepository.findById(currentUser.id); + + if (!employer) { + throw new AppError('Employer not found', 404); + } + + employer.favourites = employer.favourites.filter( + (id) => id.toString() !== guardId + ); + + await this.userRepository.save(employer); + + return { + message: 'Guard removed from favourites', + favourites: employer.favourites, + }; + } + + async getFavouriteGuards(currentUser) { + if (currentUser.role !== 'employer') { + throw new AppError('Only employers can view favourites.', 403); + } + + const employer = await this.userRepository.findEmployerFavourites( + currentUser.id + ); + + if (!employer) { + throw new AppError('Employer not found', 404); + } + + return employer.favourites; + } +} + +export default UserService; \ No newline at end of file diff --git a/app-backend/src/utils/AppError.js b/app-backend/src/utils/AppError.js new file mode 100644 index 000000000..63e4fe095 --- /dev/null +++ b/app-backend/src/utils/AppError.js @@ -0,0 +1,10 @@ +class AppError extends Error { + constructor(message, statusCode = 500) { + super(message); + this.name = 'AppError'; + this.statusCode = statusCode; + Error.captureStackTrace?.(this, this.constructor); + } +} + +export default AppError; \ No newline at end of file