diff --git a/README.md b/README.md index 23cb53a..378d950 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ base url: `http://localhost:3000` - Update a Department: `PUT /api/departments/:id` - Soft Delete a Department: `DELETE /api/departments/:id` - Restore a Department: `PATCH /api/departments/:id/restore` - + ### Team - Create a new team in a specific organization: `POST /api/organization/:organizationId/department/:departmentId/team` diff --git a/src/controllers/department.controller.js b/src/controllers/department.controller.js index 02de0ad..9485454 100644 --- a/src/controllers/department.controller.js +++ b/src/controllers/department.controller.js @@ -1,189 +1,339 @@ import prisma from '../config/prismaClient.js'; /** - * @desc Get all departments - * @route GET /api/departments - * @method GET - * @access Private (Admin or Manager) + * @desc Check if the user has permission as owner or admin to perform an action on an organization. + * @param {Object} user - The authenticated user object (must contain id and role). + * @param {string} organizationId - ID of the organization. + * @param {string} [action] - Action being attempted (e.g., 'create', 'update', 'delete', 'view'). + * @returns {Promise<{ success: boolean, message?: string, isOwner?: boolean, isAdmin?: boolean }>} + */ +export const checkOwnerAdminPermission = async ( + user, + organizationId, + action = '', +) => { + try { + if (!user?.id) { + return { + success: false, + message: 'User ID is required to check permissions.', + }; + } + + // Fetch ownership and user role info + const [isOwnerRecord, userRecord] = await Promise.all([ + prisma.organizationOwner.findFirst({ + where: { userId: user.id, organizationId }, + }), + prisma.user.findUnique({ + where: { id: user.id }, + select: { + role: true, + organizationId: true, + }, + }), + ]); + + const isOwner = Boolean(isOwnerRecord); + const isAdmin = + userRecord?.role === 'ADMIN' && + userRecord?.organizationId === organizationId; + + const hasAccess = isOwner || isAdmin; + + if (!hasAccess) { + const actionText = action + ? `to ${action} this resource` + : 'to perform this operation'; + return { + success: false, + message: `You do not have permission ${actionText}. Only owners and administrators can perform this action.`, + }; + } + + return { + success: true, + isOwner, + isAdmin, + }; + } catch (error) { + return { + success: false, + message: 'Internal error while checking permissions: ' + error.message, + }; + } +}; + +/** + * @desc Get all active departments (paginated) for the specified organization + * @route GET /api/organizations/:organizationId/departments + * @access Private (Admin/Owner only) */ export const getAllDepartments = async (req, res, next) => { try { + const { organizationId } = req.params; + const { id, role } = req.user; const page = parseInt(req.query.page) || 1; const limit = 10; const skip = (page - 1) * limit; - const totalDepartments = await prisma.department.count({ - where: { deletedAt: null }, - }); + // Check permission (Owner/Admin only) + const permission = await checkOwnerAdminPermission( + { id: id, role }, + organizationId, + 'view', + ); - const departments = await prisma.department.findMany({ - where: { deletedAt: null }, - skip, - take: limit, - orderBy: { createdAt: 'desc' }, - select: { - id: true, - name: true, - description: true, - createdAt: true, - updatedAt: true, - organization: { - select: { - id: true, - name: true, - }, - }, - manager: { - select: { - id: true, - firstName: true, - lastName: true, - }, + if (!permission.success) { + return res.status(403).json(permission); + } + + const [totalDepartments, departments] = await Promise.all([ + prisma.department.count({ where: { organizationId, deletedAt: null } }), + prisma.department.findMany({ + where: { organizationId, deletedAt: null }, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + name: true, + description: true, + createdAt: true, + updatedAt: true, + organization: { select: { id: true, name: true } }, + manager: { select: { id: true, firstName: true, lastName: true } }, }, - }, - }); + }), + ]); res.status(200).json({ + success: true, message: 'Departments retrieved successfully', - currentPage: page, - totalPages: Math.ceil(totalDepartments / limit), - totalDepartments, - departments, + data: { + departments, + pagination: { + page, + limit, + totalItems: totalDepartments, + totalPages: Math.ceil(totalDepartments / limit), + }, + }, }); } catch (error) { next(error); } }; -export const createDepartment = async (req, res, next) => { +/** + * @desc Get departments managed by current user in specified organization + * @route GET /api/organizations/:organizationId/departments/managed + * @access Private + */ +export const getCreatedDepartments = async (req, res, next) => { try { - const { name, description, organizationId, managerId } = req.body; - - // Check required fields - if (!name || !organizationId || !managerId) { - return res - .status(400) - .json({ message: 'Name, organizationId, and managerId are required' }); - } + const { id: userId } = req.user; + const { organizationId } = req.params; + const page = parseInt(req.query.page, 10) || 1; + const limit = parseInt(req.query.limit, 10) || 10; + const skip = (page - 1) * limit; - // Ensure department name is unique within the same organization - const existingDepartment = await prisma.department.findFirst({ - where: { - name, - organizationId, - deletedAt: null, - }, + // Verify organization exists + const organization = await prisma.organization.findUnique({ + where: { id: organizationId }, + select: { id: true }, }); - if (existingDepartment) { - return res.status(409).json({ - message: 'Department name already exists in this organization', + if (!organization) { + return res.status(404).json({ + success: false, + message: 'Organization not found', }); } - const Is_Owner = await prisma.organization.findFirst({ - where: { - id: organizationId, - ownerId: req.managerId, + // Find departments managed by this user + const whereClause = { + deletedAt: null, + organizationId, + managerId: userId, + }; + + const [totalDepartments, departments] = await Promise.all([ + prisma.department.count({ where: whereClause }), + prisma.department.findMany({ + where: whereClause, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + name: true, + description: true, + createdAt: true, + updatedAt: true, + organization: { select: { id: true, name: true } }, + _count: { + select: { + users: true, + teams: true, + }, + }, + }, + }), + ]); + + return res.status(200).json({ + success: true, + message: 'Managed departments retrieved successfully', + data: { + departments, + pagination: { + page, + limit, + totalItems: totalDepartments, + totalPages: Math.ceil(totalDepartments / limit), + }, }, }); - if (!Is_Owner) { - return res.status(403).json({ - message: - 'You do not have permission to create a department in this organization', + } catch (error) { + return next(error); + } +}; + +/** + * @desc Create a new department + * @route POST /api/organizations/:organizationId/departments/create + * @access Private (Owner/Admin only) + */ +export const createDepartment = async (req, res, next) => { + try { + const { organizationId } = req.params; + const { name, description } = req.body; + const { id: userId, role } = req.user; + // Verify organization exists + const organization = await prisma.organization.findUnique({ + where: { id: organizationId, deletedAt: null }, + select: { id: true }, + }); + + if (!organization) { + return res.status(404).json({ + success: false, + message: 'Organization not found', }); } + // Validate request body + if (!name || !description) { + return res.status(400).json({ + success: false, + message: 'Name and description are required', + }); + } + // Check permissions (Owner/Admin only) + const permission = await checkOwnerAdminPermission( + { id: userId, role }, + organizationId, + 'create', + ); + + if (!permission.success) { + return res.status(403).json(permission); + } - // Check if manager exists, is active, and belongs to the same organization - const manager = await prisma.user.findFirst({ + // Check for duplicate department name in organization + const existingDept = await prisma.department.findFirst({ where: { - id: managerId, - organizationId, // this line checks the manager is in the same org + name, + organizationId, deletedAt: null, }, }); - // Check if the manager is already assigned to another department - if (!manager) { - return res.status(404).json({ - message: 'Manager not found or does not belong to this organization', + if (existingDept) { + return res.status(409).json({ + success: false, + message: 'Department name already exists in this organization', }); } - // Create the department + // Create department const newDepartment = await prisma.department.create({ data: { name, description, organizationId, - managerId, + managerId: userId, }, include: { organization: { select: { id: true, name: true } }, - manager: { - select: { id: true, firstName: true, lastName: true, role: true }, - }, - }, - }); - - // Assign "MANAGER" role to the manager - await prisma.user.update({ - where: { id: managerId }, - data: { - departmentId: newDepartment.id, + manager: { select: { id: true, firstName: true, lastName: true } }, }, }); - return res.status(201).json({ + success: true, message: 'Department created successfully', - department: newDepartment, + data: newDepartment, }); } catch (error) { - next(error); + return next(error); } }; /** - * @desc Get a specific department by ID - * @route GET /api/departments/:id - * @access Private (OWNER, ADMIN, MANAGER) + * @desc Get department by ID with related data (users, teams) + * @route GET /api/organizations/:organizationId/departments/:id + * @access Private (Owner/Admin only) */ export const getDepartmentById = async (req, res, next) => { - const { id } = req.params; - try { + const { organizationId, id } = req.params; + const { id: userId, role } = req.user; + + // Verify organization exists + const organization = await prisma.organization.findFirst({ + where: { id: organizationId }, + select: { id: true }, + }); + + if (!organization) { + return res.status(404).json({ + success: false, + message: 'Organization not found', + }); + } + const department = await prisma.department.findFirst({ - where: { id }, + where: { + id, + organizationId, + }, include: { + organization: { select: { id: true, name: true } }, manager: { select: { id: true, firstName: true, lastName: true, email: true, - role: true, - }, - }, - teams: { - select: { - id: true, - name: true, + jobTitle: true, }, }, users: { - where: { deletedAt: null }, // Only active users + where: { deletedAt: null }, select: { id: true, firstName: true, lastName: true, email: true, - role: true, + jobTitle: true, }, }, - organization: { + teams: { + where: { deletedAt: null }, select: { id: true, name: true, + description: true, }, }, }, @@ -196,48 +346,75 @@ export const getDepartmentById = async (req, res, next) => { }); } + // Check permission - only Owner/Admin or if user is the department manager + const isManager = department.manager.id === userId; + + if (!isManager) { + const permission = await checkOwnerAdminPermission( + { id: userId, role }, + organizationId, + 'view', + ); + + if (!permission.success) { + return res.status(403).json(permission); + } + } + return res.status(200).json({ success: true, message: 'Department retrieved successfully', - data: { - id: department.id, - name: department.name, - description: department.description, - deletedAt: department.deletedAt, - createdAt: department.createdAt, - updatedAt: department.updatedAt, - manager: department.manager, - teams: department.teams, - users: department.users, - organization: { - id: department.organizationId, - name: department.organization.name, - }, - }, + data: department, }); } catch (error) { - next(error); + return next(error); } }; /** - * @desc Update a department - * @route PUT /api/department/:id - * @access Private (Admin or Manager with department access) + * @desc Update department details + * @route PUT /api/organizations/:organizationId/departments/:id + * @access Private (Owner/Admin only) */ export const updateDepartment = async (req, res, next) => { try { - const { id } = req.params; - const { name, description, addUsers, removeUsers } = req.body; - const { userId, role } = req.user; + const { organizationId, id } = req.params; + const { name, description, managerId } = req.body; + const { id: userId, role } = req.user; + // Validate request body + if (!name && !description && !managerId) { + return res.status(400).json({ + success: false, + message: + 'At least one field (name, description, managerId) is required to update the department', + }); + } + + // Verify organization exists + const organization = await prisma.organization.findUnique({ + where: { id: organizationId }, + select: { id: true }, + }); - // Check if department exists with organization and manager info + if (!organization) { + return res.status(404).json({ + success: false, + message: 'Organization not found', + }); + } + + // Check if department exists const department = await prisma.department.findFirst({ - where: { id, deletedAt: null }, - include: { - organization: true, - manager: true, - users: { where: { deletedAt: null } }, + where: { + id, + organizationId, + }, + select: { + id: true, + name: true, + description: true, + organizationId: true, + managerId: true, }, }); @@ -248,58 +425,23 @@ export const updateDepartment = async (req, res, next) => { }); } - // Permission check - if (role === 'MANAGER') { - if (!department.managerId || department.managerId !== userId) { - return res.status(403).json({ - success: false, - message: 'You can only update departments you manage', - }); - } - } else if (role === 'ADMIN' || role === 'OWNER') { - const userInOrg = await prisma.user.findFirst({ - where: { - id: userId, - organizationId: department.organizationId, // Fixed this line - deletedAt: null, - }, - select: { - id: true, - role: true, - organizationId: true, - departmentId: true, - firstName: true, - lastName: true, - email: true, - }, - }); - - if (!userInOrg) { - return res.status(403).json({ - success: false, - message: 'User does not belong to this organization', - }); - } + // Owner/Admin permission check + const permission = await checkOwnerAdminPermission( + { id: userId, role }, + organizationId, + 'update', + ); - if (userInOrg.role !== 'ADMIN' && userInOrg.role !== 'OWNER') { - return res.status(403).json({ - success: false, - message: 'You do not have the required role to perform this action', - }); - } - } else { - return res.status(403).json({ - success: false, - message: 'You do not have permission to update departments', - }); + if (!permission.success) { + return res.status(403).json(permission); } - // If name is being changed, check for uniqueness + // Check for duplicate name if changing if (name && name !== department.name) { const existingDept = await prisma.department.findFirst({ where: { name, - organizationId: department.organizationId, + organizationId, NOT: { id }, deletedAt: null, }, @@ -313,103 +455,37 @@ export const updateDepartment = async (req, res, next) => { } } - // Prepare transaction for multiple operations - const transaction = []; - - // Update department basic info if needed - if (name || description !== undefined) { - transaction.push( - prisma.department.update({ - where: { id }, - data: { - name: name || department.name, - description: - description !== undefined ? description : department.description, - }, - }), - ); - } - - // Handle user additions only if addUsers exists and has items - if (addUsers?.length > 0) { - // Verify users exist and belong to same organization - const existingUsers = await prisma.user.findMany({ + // If changing manager, verify new manager exists in organization + if (managerId && managerId !== department.managerId) { + const manager = await prisma.user.findFirst({ where: { - id: { in: addUsers }, - organizationId: department.organizationId, + id: managerId, + organizationId, deletedAt: null, }, }); - if (existingUsers.length !== addUsers.length) { - return res.status(400).json({ - success: false, - message: 'Some users not found or not in the same organization', - }); - } - - transaction.push( - prisma.user.updateMany({ - where: { id: { in: addUsers } }, - data: { departmentId: id }, - }), - ); - } - - // Handle user removals only if removeUsers exists and has items - if (removeUsers?.length > 0) { - // Don't allow removing the manager - if (removeUsers.includes(department.managerId)) { - return res.status(400).json({ + if (!manager) { + return res.status(404).json({ success: false, - message: 'Cannot remove department manager this way', + message: 'New manager not found in this organization', }); } - - transaction.push( - prisma.user.updateMany({ - where: { - id: { in: removeUsers }, - departmentId: id, - }, - data: { departmentId: null }, - }), - ); } - // Only execute transaction if there are operations to perform - if (transaction.length > 0) { - await prisma.$transaction(transaction); - } else { - return res.status(400).json({ - success: false, - message: 'No valid fields provided for update', - }); - } - - // Fetch updated department with all relations - const updatedDepartment = await prisma.department.findUnique({ + // Update department + const updatedDepartment = await prisma.department.update({ where: { id }, + data: { + name: name !== undefined ? name : department.name, + description: + description !== undefined ? description : department.description, + managerId: managerId !== undefined ? managerId : department.managerId, + }, include: { organization: { select: { id: true, name: true } }, manager: { - select: { id: true, firstName: true, lastName: true, role: true }, - }, - users: { - where: { deletedAt: null }, - select: { - id: true, - firstName: true, - lastName: true, - email: true, - role: true, - }, - }, - teams: { - select: { - id: true, - name: true, - }, + select: { id: true, firstName: true, lastName: true, email: true }, }, }, }); @@ -420,27 +496,40 @@ export const updateDepartment = async (req, res, next) => { data: updatedDepartment, }); } catch (error) { - next(error); + return next(error); } }; /** - * @desc Soft delete a department - * @route DELETE /api/department/:id - * @access Private (Owner or Admin only) + * @desc Soft delete a department by setting its `deletedAt` field to the current timestamp. + * @route DELETE /api/organizations/:organizationId/departments/:id + * @access Private (Owner/Admin only) */ export const softDeleteDepartment = async (req, res, next) => { try { - const { id } = req.params; + const { organizationId, id } = req.params; + const { id: userId, role } = req.user; - // 1. First check if department exists and isn't deleted - const department = await prisma.department.findUnique({ - where: { id }, - select: { - id: true, - deletedAt: true, - managerId: true, // Important for maintaining referential integrity + // Verify organization exists + const organization = await prisma.organization.findUnique({ + where: { id: organizationId }, + select: { id: true }, + }); + + if (!organization) { + return res.status(404).json({ + success: false, + message: 'Organization not found', + }); + } + + // Check if department exists + const department = await prisma.department.findFirst({ + where: { + id, + organizationId, }, + select: { id: true, organizationId: true, deletedAt: true }, }); if (!department) { @@ -457,41 +546,65 @@ export const softDeleteDepartment = async (req, res, next) => { }); } - // 2. Perform all operations in a transaction - await prisma.$transaction([ - // Finally: Soft delete the department itself - prisma.department.update({ + // Owner/Admin permission check + const permission = await checkOwnerAdminPermission( + { id: userId, role }, + organizationId, + 'delete', + ); + + if (!permission.success) { + return res.status(403).json(permission); + } + + // Soft delete department and update users + await prisma.$transaction(async (prismaClient) => { + // Soft delete the department + await prismaClient.department.update({ where: { id }, data: { deletedAt: new Date() }, - }), - ]); + }); + }); return res.status(200).json({ success: true, - message: 'Department soft deleted successfully', + message: 'Department deleted successfully', }); } catch (error) { - next(error); + return next(error); } }; /** - * @desc Restore a soft-deleted department - * @route PATCH /api/department/:id/restore - * @access Private (Owner or Admin only) + * @desc Restore a soft-deleted department by setting its `deletedAt` field to `null`. + * @route PATCH /api/organizations/:organizationId/departments/:id/restore + * @access Private (Owner/Admin only) */ export const restoreDepartment = async (req, res, next) => { try { - const { id } = req.params; - const { userId } = req.user; + const { organizationId, id } = req.params; + const { id: userId, role } = req.user; - // 1. Check if department exists and is deleted - const department = await prisma.department.findUnique({ - where: { id }, - include: { - organization: true, - manager: true, + // Verify organization exists + const organization = await prisma.organization.findUnique({ + where: { id: organizationId }, + select: { id: true }, + }); + + if (!organization) { + return res.status(404).json({ + success: false, + message: 'Organization not found', + }); + } + + // Check if department exists + const department = await prisma.department.findFirst({ + where: { + id, + organizationId, }, + select: { id: true, organizationId: true, deletedAt: true }, }); if (!department) { @@ -508,61 +621,28 @@ export const restoreDepartment = async (req, res, next) => { }); } - // 2. Verify permissions (Owner or Admin of the organization) - // For ADMIN/OWNER, check if they belong to the department's organization - const userInOrg = await prisma.user.findFirst({ - where: { - id: userId, - organizationId: department.organizationId, - role: { in: ['ADMIN', 'OWNER'] }, - deletedAt: null, - }, - }); + // Owner/Admin permission check + const permission = await checkOwnerAdminPermission( + { id: userId, role }, + organizationId, + 'restore', + ); - if (!userInOrg) { - return res.status(403).json({ - success: false, - message: - 'You do not have permission to restore departments in this organization', - }); + if (!permission.success) { + return res.status(403).json(permission); } - // 3. Restore the department - const restoredDepartment = await prisma.department.update({ + // Restore department + await prisma.department.update({ where: { id }, - data: { - deletedAt: null, - }, - include: { - organization: { select: { id: true, name: true } }, - manager: { - select: { id: true, firstName: true, lastName: true, role: true }, - }, - users: { - where: { deletedAt: null }, - select: { - id: true, - firstName: true, - lastName: true, - email: true, - role: true, - }, - }, - teams: { - select: { - id: true, - name: true, - }, - }, - }, + data: { deletedAt: null }, }); return res.status(200).json({ success: true, message: 'Department restored successfully', - data: restoredDepartment, }); } catch (error) { - next(error); + return next(error); } }; diff --git a/src/controllers/organization.controller.js b/src/controllers/organization.controller.js index c450580..2f11eb2 100644 --- a/src/controllers/organization.controller.js +++ b/src/controllers/organization.controller.js @@ -878,6 +878,19 @@ export const addOwners = async (req, res, next) => { skipDuplicates: true, }); + await prisma.user.updateMany({ + where: { + id: { + in: newOwnerIds, + }, + }, + data: { + organizationId: organizationId, + isOwner: true, + }, + }); + // Send email notifications to new owners + return res.status(200).json({ success: true, message: `Successfully added ${newOwnerRecords.count} owner(s) to the organization`, diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index 29022e4..a5fdb1a 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -8,7 +8,7 @@ import { updatePasswordValidation, updateUserAccountValidation, } from '../validations/user.validation.js'; -/* eslint no-undef:off */ + /** * @desc Get all users with pagination * @route GET /api/users/all?page=1 @@ -174,7 +174,7 @@ export const updateUserAccount = async (req, res, next) => { updateData.lastName = value.lastName; } if (value.phoneNumber) { - updateData.phoneNumber = encrypt(value.phoneNumber); + updateData.phoneNumber = value.phoneNumber; } if (value.jobTitle) { updateData.jobTitle = value.jobTitle; @@ -301,12 +301,14 @@ export const softDeleteUser = async (req, res, next) => { try { const { id } = req.params; - const user = await prisma.user.findUnique({ where: { id } }); + const user = await prisma.user.findFirst({ where: { id } }); - if (!user || user.deletedAt) { - return res - .status(404) - .json({ message: 'User not found or already deleted' }); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + if (user.deletedAt) { + return res.status(400).json({ message: 'User already deleted' }); } await prisma.user.update({ diff --git a/src/docs/TODO.md b/src/docs/TODO.md index c675607..ff25f66 100644 --- a/src/docs/TODO.md +++ b/src/docs/TODO.md @@ -1,6 +1,7 @@ # Future TODO -| Kareem's Tasks | Dawoud's Tasks | -| -------------- | -------------- | -| Kareem Task 1 | Dawoud Task 1 | -| Kareem Task 2 | Dawoud Task 2 | +## TODO + +- [ ] API-LIMITER | to all routes +- [ ] route | all ids for organization or department or Manager +- [ ] logout | use refresh token not AccessToken diff --git a/src/docs/swagger.json b/src/docs/swagger.json index f9415ad..d87301b 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -1055,7 +1055,6 @@ } } }, - "/api/users/all": { "get": { "tags": ["Users"], @@ -1213,7 +1212,7 @@ } }, "/api/users/update-password/{id}": { - "put": { + "patch": { "tags": ["Users"], "summary": "Update user password", "description": "Update a user's password.", @@ -1435,15 +1434,38 @@ } } }, - "/api/departments/create": { + "/api/departments/organization/{organizationId}/manager/{managerId}": { "post": { "tags": ["Department"], "summary": "Create a new department", - "description": "Allows authorized users to create a new department.", + "description": "Allows authorized users to create a new department within a specific organization and assign a manager.", "operationId": "createDepartment", "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "description": "ID of the organization where the department will be created", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "managerId", + "in": "path", + "required": true, + "description": "ID of the user to be assigned as the department manager", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], "requestBody": { "required": true, + "description": "Details of the department to be created", "content": { "application/json": { "schema": { @@ -1464,10 +1486,44 @@ } }, "400": { - "$ref": "#/components/responses/BadRequestError" + "description": "Invalid input or validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, "409": { - "$ref": "#/components/responses/ConflictError" + "description": "Conflict - department with the same name already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } @@ -1501,11 +1557,11 @@ } } }, - "/api/departments/{departmentId}/update": { + "/api/departments/{departmentId}/": { "put": { "tags": ["Department"], - "summary": "Update department details", - "description": "Update the details of a specific department.", + "summary": "Create a new department", + "description": "Update the details of a specific department, including its name and description.", "operationId": "updateDepartment", "security": [{ "bearerAuth": [] }], "parameters": [ @@ -1521,17 +1577,76 @@ } ], "requestBody": { - "$ref": "#/components/requestBodies/UpdateDepartmentRequest" + "description": "Department update data", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateDepartmentRequest" + }, + "examples": { + "updateNameAndDescription": { + "value": { + "name": "Updated Department Name", + "description": "Updated description for the department" + } + }, + "updateNameOnly": { + "value": { + "name": "New Department Name" + } + } + } + } + } }, "responses": { "200": { - "$ref": "#/components/responses/DepartmentDetailResponse" + "description": "Department updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateDepartmentResponse" + } + } + } }, "400": { - "$ref": "#/components/responses/BadRequestError" + "description": "Invalid input or validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "validationError": { + "value": { + "success": false, + "message": "Validation failed", + "errors": ["\"name\" must be at least 2 characters long"] + } + } + } + } + } }, "404": { - "$ref": "#/components/responses/NotFoundError" + "description": "Department not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "notFound": { + "value": { + "success": false, + "message": "Department not found" + } + } + } + } + } } } } @@ -4510,7 +4625,10 @@ "type": "object", "properties": { "name": { "type": "string", "example": "Engineering" }, - "description": { "type": "string", "example": "Handles engineering tasks" }, + "description": { + "type": "string", + "example": "Handles engineering tasks" + }, "organizationId": { "type": "string", "format": "uuid" }, "managerId": { "type": "string", "format": "uuid" } }, @@ -4519,7 +4637,10 @@ "CreateDepartmentResponse": { "type": "object", "properties": { - "message": { "type": "string", "example": "Department created successfully" }, + "message": { + "type": "string", + "example": "Department created successfully" + }, "department": { "type": "object", "properties": { @@ -4535,7 +4656,10 @@ "GetAllDepartmentsResponse": { "type": "object", "properties": { - "message": { "type": "string", "example": "Departments retrieved successfully" }, + "message": { + "type": "string", + "example": "Departments retrieved successfully" + }, "departments": { "type": "array", "items": { @@ -4567,7 +4691,10 @@ "GetDepartmentResponse": { "type": "object", "properties": { - "message": { "type": "string", "example": "Department retrieved successfully" }, + "message": { + "type": "string", + "example": "Department retrieved successfully" + }, "department": { "type": "object", "properties": { @@ -4603,7 +4730,10 @@ "UpdateDepartmentResponse": { "type": "object", "properties": { - "message": { "type": "string", "example": "Department updated successfully" }, + "message": { + "type": "string", + "example": "Department updated successfully" + }, "department": { "type": "object", "properties": { @@ -4617,13 +4747,19 @@ "SoftDeleteDepartmentResponse": { "type": "object", "properties": { - "message": { "type": "string", "example": "Department soft deleted successfully" } + "message": { + "type": "string", + "example": "Department soft deleted successfully" + } } }, "RestoreDepartmentResponse": { "type": "object", "properties": { - "message": { "type": "string", "example": "Department restored successfully" } + "message": { + "type": "string", + "example": "Department restored successfully" + } } } } diff --git a/src/middlewares/verifyManagerPermission.middleware.js b/src/middlewares/verifyManagerPermission.middleware.js deleted file mode 100644 index 72afd22..0000000 --- a/src/middlewares/verifyManagerPermission.middleware.js +++ /dev/null @@ -1,11 +0,0 @@ -export const verifyManagerPermission = (req, res, next) => { - const allowedRoles = ['MANAGER', 'ADMIN', 'OWNER']; - - if (allowedRoles.includes(req.user.role)) { - return next(); - } - - return res.status(403).json({ - message: 'You do not have permission to perform this operation', - }); -}; diff --git a/src/middlewares/verifyOwnerOrAdmin.middleware.js b/src/middlewares/verifyOwnerOrAdmin.middleware.js deleted file mode 100644 index 904958f..0000000 --- a/src/middlewares/verifyOwnerOrAdmin.middleware.js +++ /dev/null @@ -1,12 +0,0 @@ -export const verifyOwnerOrAdmin = (req, res, next) => { - const { role } = req.user; - - if (role === 'OWNER' || role === 'ADMIN') { - return next(); - } - - return res.status(403).json({ - success: false, - message: 'Only organization owners or admins can perform this action', - }); -}; diff --git a/src/routes/department.routes.js b/src/routes/department.routes.js index f140e64..ada0773 100644 --- a/src/routes/department.routes.js +++ b/src/routes/department.routes.js @@ -3,62 +3,61 @@ import { createDepartment, getAllDepartments, getDepartmentById, - restoreDepartment, - softDeleteDepartment, updateDepartment, + softDeleteDepartment, + restoreDepartment, + getCreatedDepartments, } from '../controllers/department.controller.js'; -import { verifyManagerPermission } from '../middlewares/verifyManagerPermission.middleware.js'; import { verifyAccessToken } from '../middlewares/auth.middleware.js'; -import { - validateCreateDepartment, - validateUpdateDepartment, -} from '../validations/department.validation.js'; -import { verifyOwnerOrAdmin } from '../middlewares/verifyOwnerOrAdmin.middleware.js'; - const router = express.Router(); -// Admin, OWNER, or MANAGER can access these +// Routes for Organization Owners and Admins only router.get( - '/api/departments/all', + '/api/organizations/:organizationId/departments/all', verifyAccessToken, - verifyManagerPermission, getAllDepartments, ); +// Routes accessible by Department Managers +router.get( + '/api/organizations/:organizationId/departments/created', + verifyAccessToken, + getCreatedDepartments, +); + +// Create department - Admin/Owner only router.post( - '/api/departments/create', + '/api/organizations/:organizationId/departments/create', verifyAccessToken, - verifyManagerPermission, - validateCreateDepartment, createDepartment, ); +// Get department by ID router.get( - '/api/departments/:id', + '/api/organizations/:organizationId/departments/:id', verifyAccessToken, - verifyManagerPermission, getDepartmentById, ); +// Update department - Admin/Owner only router.put( - '/api/departments/:id', + '/api/organizations/:organizationId/departments/:id', verifyAccessToken, - verifyManagerPermission, - validateUpdateDepartment, updateDepartment, ); +// Soft delete department - Admin/Owner only router.delete( - '/api/departments/:id', + '/api/organizations/:organizationId/departments/:id', verifyAccessToken, - verifyOwnerOrAdmin, softDeleteDepartment, ); +// Restore department - Admin/Owner only router.patch( - '/api/departments/:id/restore', + '/api/organizations/:organizationId/departments/:id/restore', verifyAccessToken, - verifyOwnerOrAdmin, restoreDepartment, ); + export default router; diff --git a/src/routes/user.routes.js b/src/routes/user.routes.js index 55bd8dc..27e7dcc 100644 --- a/src/routes/user.routes.js +++ b/src/routes/user.routes.js @@ -38,7 +38,7 @@ router.put( updateUserAccount, ); -router.put( +router.patch( '/api/users/update-password/:id', verifyAccessToken, verifyUserPermission, @@ -46,13 +46,14 @@ router.put( ); router.delete( - '/users/:id', + '/api/users/:id', verifyAccessToken, verifyAdminPermission, softDeleteUser, ); + router.patch( - '/users/restore/:id', + '/api/users/restore/:id', // Added missing `/` at the beginning verifyAccessToken, verifyAdminPermission, restoreUser, @@ -72,4 +73,5 @@ router.delete( verifyUserPermission, deleteUserProfilePic, ); + export default router; diff --git a/src/validations/department.validation.js b/src/validations/department.validation.js index 2ccc2fb..8ca47b5 100644 --- a/src/validations/department.validation.js +++ b/src/validations/department.validation.js @@ -4,8 +4,6 @@ export const validateCreateDepartment = (req, res, next) => { const schema = Joi.object({ name: Joi.string().max(100).required(), description: Joi.string().optional().allow('', null), - organizationId: Joi.string().uuid().required(), - managerId: Joi.string().uuid().required(), }); const { error } = schema.validate(req.body);