diff --git a/src/controllers/organization.controller.js b/src/controllers/organization.controller.js index 4b65113..9d8ec6d 100644 --- a/src/controllers/organization.controller.js +++ b/src/controllers/organization.controller.js @@ -147,14 +147,14 @@ export const resendOTP = async (req, res, next) => { }); } - const org = await prisma.organization.findFirst({ + const org = await prisma.organization.findUnique({ where: { id: orgId }, }); if (!org) { return res.status(404).json({ success: false, - message: 'This organization not found', + message: 'Organization not found', }); } @@ -176,7 +176,7 @@ export const resendOTP = async (req, res, next) => { await sendEmail({ to: org.contactEmail, subject: 'Re-verify Your Organization Email', - text: `Organization name: ${org.name}\nYour verification code is: ${verificationOTP}. will expire in 10 min`, + text: `Organization name: ${org.name}\nYour verification code is: ${verificationOTP}. It will expire in 10 minutes`, }); } catch (emailError) { return res.status(500).json({ diff --git a/src/controllers/team.controller.js b/src/controllers/team.controller.js index 6ed8ae6..de56c7c 100644 --- a/src/controllers/team.controller.js +++ b/src/controllers/team.controller.js @@ -2,6 +2,7 @@ import prisma from '../config/prismaClient.js'; import { addTeamMemberValidation, createTeamValidation, + updateTeamValidation, } from '../validations/team.validation.js'; /** @@ -398,3 +399,122 @@ export const addTeamMember = async (req, res, next) => { next(error); } }; + +/** + * @desc Update a team + * @route /api/organization/:organizationId/department/:departmentId/team/:teamId + * @method PUT + * @access private - admins or organization owners only + */ +export const updateTeam = async (req, res, next) => { + try { + const { organizationId, departmentId, teamId } = req.params; + + if (!organizationId || !departmentId || !teamId) { + return res.status(400).json({ + success: false, + message: 'Organization ID, Department ID, and Team ID are required', + }); + } + + // Check if organization exists and is not deleted + const existingOrg = await prisma.organization.findFirst({ + where: { + id: organizationId, + deletedAt: null, + }, + include: { + owners: { + select: { + userId: true, + }, + }, + }, + }); + + if (!existingOrg) { + return res.status(404).json({ + success: false, + message: 'Organization not found', + }); + } + + // Check if department exists and is not deleted + const existingDep = await prisma.department.findFirst({ + where: { + id: departmentId, + deletedAt: null, + }, + select: { managerId: true }, + }); + + if (!existingDep) { + return res.status(404).json({ + success: false, + message: 'Department not found', + }); + } + + // Check if team exists and is not deleted + const team = await prisma.team.findFirst({ + where: { + id: teamId, + organizationId, + deletedAt: null, + }, + select: { + id: true, + name: true, + description: true, + createdBy: true, + }, + }); + if (!team) { + return res.status(404).json({ + success: false, + message: 'Team not found', + }); + } + + // TODO: Extract all permission checks into a helper function like hasTeamAddPermission(user, org, dep, team) to simplify controller logic. + // Check permissions - only admins and organization owners + const isAdmin = req.user.role === 'ADMIN'; + const isOwner = existingOrg.owners.some( + (owner) => owner.userId === req.user.id, + ); + const isDepManager = existingDep.managerId === req.user.id; + const isTeamManager = team.createdBy === req.user.id; + + if (!isAdmin && !isOwner && !isDepManager && !isTeamManager) { + return res.status(403).json({ + success: false, + message: + 'You do not have permission to update this team in this department', + }); + } + + // Validate input + const { error } = updateTeamValidation(req.body); + if (error) { + return res.status(400).json({ + success: false, + message: 'Validation failed', + errors: error.details.map((e) => e.message), + }); + } + + const { name, description, avatar } = req.body; + + const updatedTeam = await prisma.team.update({ + where: { id: teamId }, + data: { name, description, avatar }, + }); + + res.status(200).json({ + success: true, + team: updatedTeam, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 16dcaaa..57968bb 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -1866,6 +1866,159 @@ } } } + }, + "/api/organization/{organizationId}/department/{departmentId}/team/{teamId}": { + "put": { + "tags": ["Team"], + "summary": "Update a team", + "description": "Update team details. Requires admin privileges, organization ownership, department management, or team leadership rights.", + "operationId": "updateTeam", + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "description": "ID of the organization", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "departmentId", + "in": "path", + "required": true, + "description": "ID of the department", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "description": "ID of the team to update", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Team update data", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamRequest" + }, + "examples": { + "basicUpdate": { + "value": { + "name": "Updated Team Name", + "description": "Updated team description", + "avatar": "https://example.com/new-team-avatar.jpg" + } + }, + "nameOnly": { + "value": { + "name": "New Team Name" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Team updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamResponse" + } + } + } + }, + "400": { + "description": "Bad request - validation error or missing parameters", + "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"] + } + }, + "missingParams": { + "value": { + "success": false, + "message": "Organization ID, Department ID, and Team ID are required" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Organization, department or team not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "orgNotFound": { + "value": { + "success": false, + "message": "Organization not found" + } + }, + "depNotFound": { + "value": { + "success": false, + "message": "Department not found" + } + }, + "teamNotFound": { + "value": { + "success": false, + "message": "Team not found" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } } }, "components": { @@ -3402,6 +3555,80 @@ } } } + }, + "UpdateTeamRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 100, + "example": "Updated Team Name" + }, + "description": { + "type": "string", + "maxLength": 500, + "example": "Updated team description" + }, + "avatar": { + "type": "string", + "format": "uri", + "example": "https://example.com/new-team-avatar.jpg" + } + }, + "anyOf": [ + { "required": ["name"] }, + { "required": ["description"] }, + { "required": ["avatar"] } + ] + }, + "UpdateTeamResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "team": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "avatar": { + "type": "string", + "format": "uri" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "departmentId": { + "type": "string", + "format": "uuid" + }, + "createdBy": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + } + } } } } diff --git a/src/routes/team.routes.js b/src/routes/team.routes.js index f133852..850063f 100644 --- a/src/routes/team.routes.js +++ b/src/routes/team.routes.js @@ -1,6 +1,10 @@ import { Router } from 'express'; import { verifyAccessToken } from '../middlewares/auth.middleware.js'; -import { addTeamMember, createTeam } from '../controllers/team.controller.js'; +import { + addTeamMember, + createTeam, + updateTeam, +} from '../controllers/team.controller.js'; const router = Router(); @@ -16,4 +20,10 @@ router.post( addTeamMember, ); +router.put( + '/api/organization/:organizationId/department/:departmentId/team/:teamId', + verifyAccessToken, + updateTeam, +); + export default router; diff --git a/src/validations/team.validation.js b/src/validations/team.validation.js index f0b838c..22320a4 100644 --- a/src/validations/team.validation.js +++ b/src/validations/team.validation.js @@ -61,3 +61,24 @@ export const addTeamMemberValidation = (obj) => { return schema.validate(obj, { abortEarly: false }); }; + +export const updateTeamValidation = (obj) => { + const schema = Joi.object({ + name: Joi.string().min(2).max(100).optional().trim().messages({ + 'string.base': 'Team name must be a string', + 'string.empty': 'Team name is required if provided', + 'string.min': 'Team name must be at least {#limit} characters long', + 'string.max': 'Team name cannot exceed {#limit} characters', + }), + + description: Joi.string().allow('').optional().messages({ + 'string.base': 'Description must be a string', + }), + + avatar: Joi.string().allow(null, '').optional().messages({ + 'string.base': 'Avatar must be a string URL or file path', + }), + }); + + return schema.validate(obj, { abortEarly: false }); +};