Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions src/controllers/team.controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import prisma from '../config/prismaClient.js';
import { uploadToCloudinary } from '../utils/cloudinary.utils.js';
import {
addTeamMemberValidation,
createTeamValidation,
Expand Down Expand Up @@ -520,3 +521,118 @@ export const updateTeam = async (req, res, next) => {
next(error);
}
};

/**
* @desc Upload team avatar
* @route /api/organization/:organizationId/department/:departmentId/team/:teamId/avatar/upload
* @method POST
* @access private - admins or organization owners only
*/
export const uploadTeamAvatar = 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',
Copy link

Copilot AI Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message refers to updating a team rather than uploading an avatar; updating the message to reflect the correct action can improve clarity.

Suggested change
'You do not have permission to update this team in this department',
'You do not have permission to upload the team avatar in this department',

Copilot uses AI. Check for mistakes.
});
}

if (!req.file) {
return res.status(400).json({ message: 'No file uploaded' });
}

const avatar = await uploadToCloudinary(req.file.buffer, 'team_avatar');

// Upload the team avatar
const updatedTeam = await prisma.team.update({
where: { id: teamId },
data: { avatar },
});

res.status(200).json({
success: true,
message: 'Team avatar uploaded successfully',
team: updatedTeam,
});
} catch (error) {
next(error);
}
};
196 changes: 196 additions & 0 deletions src/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2019,6 +2019,150 @@
}
}
}
},
"/api/organization/{organizationId}/department/{departmentId}/team/{teamId}/avatar/upload": {
"post": {
"tags": ["Team"],
"summary": "Upload team avatar",
"description": "Upload an avatar image for a team. Requires admin privileges, organization ownership, department management, or team leadership rights.",
"operationId": "uploadTeamAvatar",
"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",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"image": {
"type": "string",
"format": "binary",
"description": "Avatar image file (JPG/PNG)"
}
},
"required": ["image"]
}
}
}
},
"responses": {
"200": {
"description": "Team avatar uploaded successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TeamAvatarUploadResponse"
}
}
}
},
"400": {
"description": "Bad request - missing parameters or no file uploaded",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"examples": {
"missingParams": {
"value": {
"success": false,
"message": "Organization ID, Department ID, and Team ID are required"
}
},
"noFile": {
"value": {
"success": false,
"message": "No file uploaded"
}
}
}
}
}
},
"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 - failed to upload image",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
}
},
"components": {
Expand Down Expand Up @@ -3629,6 +3773,58 @@
}
}
}
},
"TeamAvatarUploadResponse": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"message": {
"type": "string",
"example": "Team avatar uploaded successfully"
},
"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"
}
}
}
}
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/routes/team.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
addTeamMember,
createTeam,
updateTeam,
uploadTeamAvatar,
} from '../controllers/team.controller.js';
import upload from '../middlewares/upload.middleware.js';

const router = Router();

Expand All @@ -26,4 +28,11 @@ router.put(
updateTeam,
);

router.post(
'/api/organization/:organizationId/department/:departmentId/team/:teamId/avatar/upload',
verifyAccessToken,
upload.single('image'),
uploadTeamAvatar,
);

export default router;