diff --git a/src/app.js b/src/app.js index 111e8a14..92cad1c0 100644 --- a/src/app.js +++ b/src/app.js @@ -135,6 +135,8 @@ import messageRouter from "./routes/apps/chat-app/message.routes.js"; import todoRouter from "./routes/apps/todo/todo.routes.js"; +import projectRouter from "./routes/apps/project-management/project.routes.js"; + // * Kitchen sink routes import cookieRouter from "./routes/kitchen-sink/cookie.routes.js"; import httpmethodRouter from "./routes/kitchen-sink/httpmethod.routes.js"; @@ -192,6 +194,8 @@ app.use("/api/v1/chat-app/messages", messageRouter); app.use("/api/v1/todos", todoRouter); +app.use("/api/v1/project-management/project", projectRouter); + // * Kitchen sink apis app.use("/api/v1/kitchen-sink/http-methods", httpmethodRouter); app.use("/api/v1/kitchen-sink/status-codes", statuscodeRouter); diff --git a/src/controllers/apps/project-management/project.controllers.js b/src/controllers/apps/project-management/project.controllers.js new file mode 100644 index 00000000..38ff8dc3 --- /dev/null +++ b/src/controllers/apps/project-management/project.controllers.js @@ -0,0 +1,444 @@ +import mongoose from "mongoose"; + +import { getMongoosePaginationOptions } from "../../../utils/helpers.js"; + +import { Project } from "../../../models/apps/project-management/project.models.js"; +import { Member } from "../../../models/apps/project-management/member.models.js"; +import { User } from "../../../models/apps/auth/user.models.js"; + +import { ApiResponse } from "../../../utils/ApiResponse.js"; +import { asyncHandler } from "../../../utils/asyncHandler.js"; +import { ApiError } from "../../../utils/ApiError.js"; + +const createProject = asyncHandler(async (req, res) => { + const userId = req.user?.id; + const { name, description, tags } = req.body; + + const user = await User.findById(userId); + + // Create a new project + const project = await Project.create({ + name, + description, + tags, + ownerId: userId, + }); + + // Create a new member with owner role + const member = await Member.create({ + memberEmailId: user.email, + userId, + projectId: project._id, + invitationStatus: "accepted", + role: "owner", + }); + + // Add the member to the project's members array + await Project.updateOne( + { _id: new mongoose.Types.ObjectId(project._id) }, + { $push: { members: new mongoose.Types.ObjectId(member._id) } }, + { new: true } + ); + + return res + .status(201) + .json( + new ApiResponse(201, { project, member }, "Project created successfully") + ); +}); + +const getProjects = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { page = 1, limit = 10 } = req.query; + + // Pipeline for get projects from member list + const aggregationPipeline = [ + { $match: { userId: new mongoose.Types.ObjectId(userId) } }, + { + $lookup: { + from: "projects", + localField: "projectId", + foreignField: "_id", + as: "project", + }, + }, + { $unwind: "$project" }, + { + $addFields: { + memberId: "$_id", + }, + }, + { + $project: { + _id: "$project._id", + role: 1, + memberId: 1, + name: "$project.name", + description: "$project.description", + banner: "$project.banner", + }, + }, + ]; + + const projects = await Member.aggregatePaginate( + Member.aggregate(aggregationPipeline), + getMongoosePaginationOptions({ + page, + limit, + customLabels: { + totalDocs: "totalProjects", + docs: "projects", + }, + }) + ); + + return res + .status(200) + .json( + new ApiResponse(200, { ...projects }, "Projects fetched successfully") + ); +}); + +const updateProject = asyncHandler(async (req, res) => { + const userId = req.user?.id; + const { projectId } = req.params; + const { name, description, tags } = req.body; + + const project = await Project.findOneAndUpdate( + { _id: new mongoose.Types.ObjectId(projectId), ownerId: userId }, + { $set: { name, description, tags } }, + { new: true } + ); + + if (!project) { + throw new ApiError(404, "Project not found or not owned by the user"); + } + + return res + .status(200) + .json(new ApiResponse(200, { project }, "Project updated successfully")); +}); + +const getProject = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId } = req.params; + + // check for existing project + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project does not exist"); + } + + // check if user is a member of the project + const member = await Member.findOne({ + userId: new mongoose.Types.ObjectId(userId), + projectId: new mongoose.Types.ObjectId(projectId), + }); + + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + + const projectData = { + _id: project._id, + name: project.name, + description: project.description, + tags: project.tags, + banner: project.banner, + totalMembers: project.members.length, + role: member.role, + }; + + return res + .status(200) + .json( + new ApiResponse( + 200, + { project: projectData }, + "Project fetched successfully" + ) + ); +}); + +const deleteProject = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId } = req.params; + + // check for existing project + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project does not exist"); + } + + // check if user is a member of the project and is owner of the project + const member = await Member.findOne({ + userId: new mongoose.Types.ObjectId(userId), + projectId: new mongoose.Types.ObjectId(projectId), + }); + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + if (member.role !== "owner") { + throw new ApiError(403, "Only project owners can delete a project"); + } + + // delete all members of the project and their associated documents + project.members.forEach(async (member) => { + await Member.findByIdAndDelete(member); + }); + + // delete the project itself + await Project.findOneAndDelete({ + _id: new mongoose.Types.ObjectId(projectId), + ownerId: userId, + }); + + return res + .status(200) + .json(new ApiResponse(200, { projectId }, "Project deleted successfully")); +}); + +const inviteMember = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId, email, role } = req.body; + + // check for existing project + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project does not exist"); + } + + // check if user is a member of the project and is owner of the project + const member = await Member.findOne({ + userId: new mongoose.Types.ObjectId(userId), + projectId: new mongoose.Types.ObjectId(projectId), + }); + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + if (member.role === "member") { + throw new ApiError(403, "Only project owners and admins can add members"); + } + + // check if user is already a member of the project + const existingMember = await Member.findOne({ + memberEmailId: email, + projectId: new mongoose.Types.ObjectId(projectId), + }); + if (existingMember && existingMember.invitationStatus === "accepted") { + throw new ApiError(400, "User is already a member of the project"); + } + + // create a new member + const newMember = await Member.create({ + memberEmailId: email, + userId: null, + projectId: project._id, + invitationStatus: "pending", + role, + }); + + // Add the member to the project's members array + await Project.updateOne( + { _id: new mongoose.Types.ObjectId(projectId) }, + { $push: { members: new mongoose.Types.ObjectId(newMember._id) } }, + { new: true } + ); + + //TODO: send project invitation to this member (email notification) + + return res + .status(201) + .json( + new ApiResponse( + 201, + { member: newMember }, + "Member created successfully. An email notification has been sent for the project invitation." + ) + ); +}); + +const removeMember = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId, memberId } = req.body; + + // check for existing project + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project does not exist"); + } + + // check if user is a member of the project and is owner of the project + const member = await Member.findOne({ + userId: new mongoose.Types.ObjectId(userId), + projectId: new mongoose.Types.ObjectId(projectId), + }); + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + if (member.role === "member") { + throw new ApiError( + 403, + "Only project owners and admins can remove members" + ); + } + + // check if member exists in the project + const existingMember = await Member.findById(memberId); + if (!existingMember) { + throw new ApiError(404, "Member does not exist in the project"); + } + + // remove the member from the project's members array + await Project.updateOne( + { _id: new mongoose.Types.ObjectId(projectId) }, + { $pull: { members: new mongoose.Types.ObjectId(memberId) } }, + { new: true } + ); + + // delete the member from the database + await Member.findByIdAndDelete(memberId); + + return res + .status(200) + .json(new ApiResponse(200, { memberId }, "Member removed successfully")); +}); + +const getMembers = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId } = req.params; + const { page = 1, limit = 10 } = req.query; + + // check for existing project + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project does not exist"); + } + + // check if user is a member of the project + const member = await Member.findOne({ + userId: new mongoose.Types.ObjectId(userId), + projectId: new mongoose.Types.ObjectId(projectId), + }); + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + + // get members of the project + const aggregatePipeline = [ + { $match: { projectId: new mongoose.Types.ObjectId(projectId) } }, + { + $lookup: { + from: "users", + localField: "userId", + foreignField: "_id", + as: "user", + }, + }, + { $unwind: { path: "$user", preserveNullAndEmptyArrays: true } }, + { + $project: { + _id: 1, + memberEmailId: 1, + projectId: 1, + invitationStatus: 1, + role: 1, + user: { + avatar: 1, + username: 1, + email: 1, + }, + }, + }, + ]; + + const members = await Member.aggregatePaginate( + Member.aggregate(aggregatePipeline), + getMongoosePaginationOptions({ + page, + limit, + customLabels: { + totalDocs: "totalMembers", + docs: "members", + }, + }) + ); + + return res + .status(200) + .json(new ApiResponse(200, { ...members }, "Members fetched successfully")); +}); + +const acceptInvitation = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId, memberId } = req.body; + + const user = await User.findById(userId); + + // check for existing project + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project does not exist"); + } + + // check if user is a member of the project + const member = await Member.findById(memberId); + if (!member) { + throw new ApiError(404, "Member does not exist in the project"); + } + + if (member.invitationStatus === "accepted") { + throw new ApiError(400, "Member is already accepted invitation"); + } + + if (member.invitationStatus === "banned") { + throw new ApiError(403, "Member is banned"); + } + + if (member.memberEmailId !== user.email) { + throw new ApiError( + 403, + "Member email address does not match your registered email address" + ); + } + + // update the member's status to accepted + await Member.updateOne( + { _id: new mongoose.Types.ObjectId(memberId) }, + { $set: { invitationStatus: "accepted", userId } }, + { new: true } + ); + + return res + .status(200) + .json( + new ApiResponse(200, { memberId }, "Invitation accepted successfully") + ); +}); + +export { + createProject, + getProjects, + updateProject, + getProject, + deleteProject, + inviteMember, + removeMember, + getMembers, + acceptInvitation, +}; diff --git a/src/controllers/apps/project-management/task.controllers.js b/src/controllers/apps/project-management/task.controllers.js new file mode 100644 index 00000000..2c28e02c --- /dev/null +++ b/src/controllers/apps/project-management/task.controllers.js @@ -0,0 +1,342 @@ +import mongoose from "mongoose"; + +import { Project } from "../../../models/apps/project-management/project.models.js"; +import { Member } from "../../../models/apps/project-management/member.models.js"; +import { Task } from "../../../models/apps/project-management/task.models.js"; + +import { ApiResponse } from "../../../utils/ApiResponse.js"; +import { asyncHandler } from "../../../utils/asyncHandler.js"; +import { ApiError } from "../../../utils/ApiError.js"; + +const createTask = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const projectTask = req.body; + + const project = await Project.findById(projectTask.projectId); + if (!project) { + throw new ApiError(404, "Project not found"); + } + + const member = await Member.findOne({ + projectId: projectTask.projectId, + userId, + }); + if (!member) { + throw new ApiError(404, "Member not found in the project"); + } + + const newProjectTask = await Task.create({ + ...projectTask, + memberId: member._id, + }); + + return res + .status(201) + .json( + new ApiResponse( + 201, + { task: newProjectTask }, + "Task created successfully" + ) + ); +}); + +const getTasks = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId } = req.params; + const { + title, + priority, + sortByCreated, + assignedToMe, + onlyCompleted, + createdByMe, + } = req.query; + + // check if project is existing + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project not found"); + } + + // check if user is a member of the project + const member = await Member.findOne({ userId, projectId }); + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + + let searchQuery = new RegExp(title, "i"); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + let pipeline = [ + { + $match: { + projectId: new mongoose.Types.ObjectId(projectId), + ...(createdByMe && { + memberId: new mongoose.Types.ObjectId(member._id), + }), + title: { $regex: searchQuery }, + ...(priority && { priority }), + ...(assignedToMe && { members: { $in: [member._id] } }), + $or: [ + ...(onlyCompleted + ? [{ status: "completed" }] + : [ + { status: { $in: ["todo", "in_progress", "under_review"] } }, + { status: "completed", updatedAt: { $gte: today } }, + ]), + ], + }, + }, + { + $lookup: { + from: "members", + localField: "memberId", + foreignField: "_id", + as: "creator", + }, + }, + { $unwind: "$creator" }, + { + $lookup: { + from: "members", + localField: "members", + foreignField: "_id", + as: "membersDetails", + }, + }, + { + $addFields: { + members: { + $map: { + input: "$membersDetails", + as: "member", + in: { + _id: "$$member._id", + memberEmailId: "$$member.memberEmailId", + }, + }, + }, + }, + }, + { + $project: { + creator: { + _id: 0, + userId: 0, + projectId: 0, + invitationStatus: 0, + createdAt: 0, + updatedAt: 0, + __v: 0, + }, + membersDetails: 0, + }, + }, + { $sort: { createdAt: sortByCreated ? 1 : -1 } }, + ]; + + const tasks = await Task.aggregate(pipeline); + + return res + .status(200) + .json(new ApiResponse(200, { tasks }, "Task featching successfully")); +}); + +const updateTask = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const projectTask = req.body; + + const project = await Project.findById(projectTask.projectId); + if (!project) { + throw new ApiError(404, "Project not found"); + } + + const member = await Member.findOne({ + userId, + projectId: projectTask.projectId, + }); + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + + const task = await Task.findById(projectTask.taskId); + if (!task) { + throw new ApiError(404, "Task not found"); + } + console.log(task, member); + if ( + member.role !== "admin" && + member.role !== "owner" && + member._id.toString() !== task.memberId.toString() + ) { + throw new ApiError( + 403, + "Only project owners, admins and creator can update tasks" + ); + } + + const updatedTask = await Task.findByIdAndUpdate( + projectTask.taskId, + projectTask, + { new: true } + ); + + return res + .status(200) + .json( + new ApiResponse(200, { task: updatedTask }, "Task updated successfully") + ); +}); + +const deleteTask = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId, taskId } = req.body; + + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project not found"); + } + + const member = await Member.findOne({ + userId, + projectId, + }); + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + + if (member.role === "member") { + throw new ApiError(403, "Only project owners and admins can delete tasks"); + } + + const task = await Task.findById(taskId); + if (!task) { + throw new ApiError(404, "Task not found"); + } + + await Task.deleteOne({ _id: taskId }); + + return res + .status(200) + .json(new ApiError(404, { taskId }, "Task deleted successfully")); +}); + +const assignMemberToTask = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId, taskId, memberId } = req.body; + + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project not found"); + } + + const member = await Member.findOne({ + userId, + projectId, + }); + + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + + if (member.role === "member") { + throw new ApiError( + 403, + "Only project owners and admins can assign members" + ); + } + + const existingMember = await Member.findById(memberId); + if (!existingMember) { + throw new ApiError(404, "Member not found"); + } + + //TODO: check already assigned member and also send a notification + + await Task.findByIdAndUpdate( + taskId, + { $push: { members: memberId } }, + { new: true } + ); + + return res + .status(200) + .json( + new ApiResponse(200, { memberId }, "Assigned member to task successfully") + ); +}); + +const removeMemberFromTask = asyncHandler(async (req, res) => { + const userId = req.user?._id; + const { projectId, taskId, memberId } = req.body; + + const project = await Project.findById(projectId); + if (!project) { + throw new ApiError(404, "Project not found"); + } + + const member = await Member.findOne({ + userId, + projectId, + }); + + if (!member) { + throw new ApiError( + 403, + "Access denied. You are not a member of this project" + ); + } + + if (member.role === "member") { + throw new ApiError( + 403, + "Only project owners and admins can assign members" + ); + } + + const existingMember = await Member.findById(memberId); + if (!existingMember) { + throw new ApiError(404, "Member not found"); + } + + //TODO: check already assigned member and also send a notification + + await Task.findByIdAndUpdate( + taskId, + { $pull: { members: new mongoose.Types.ObjectId(memberId) } }, + { new: true } + ); + + return res + .status(200) + .json( + new ApiResponse( + 200, + { memberId }, + "Removed assigned member from task successfully" + ) + ); +}); + +export { + createTask, + getTasks, + updateTask, + deleteTask, + assignMemberToTask, + removeMemberFromTask, +}; diff --git a/src/models/apps/project-management/member.models.js b/src/models/apps/project-management/member.models.js new file mode 100644 index 00000000..45d24291 --- /dev/null +++ b/src/models/apps/project-management/member.models.js @@ -0,0 +1,41 @@ +import mongoose, { Schema } from "mongoose"; +import mongooseAggregatePaginate from "mongoose-aggregate-paginate-v2"; + +const memberSchema = new Schema( + { + memberEmailId: { + type: String, + require: true, + }, + + userId: { + type: Schema.Types.ObjectId, + ref: "User", + required: false, + }, + + projectId: { + type: Schema.Types.ObjectId, + ref: "Project", + required: true, + }, + + invitationStatus: { + type: String, + enum: ["pending", "accepted", "rejected", "banned"], + required: true, + }, + + role: { + type: String, + enum: ["admin", "member", "owner"], + required: true, + }, + }, + + { timestamps: true } +); + +memberSchema.plugin(mongooseAggregatePaginate); + +export const Member = mongoose.model("Member", memberSchema); diff --git a/src/models/apps/project-management/project.models.js b/src/models/apps/project-management/project.models.js new file mode 100644 index 00000000..3c473fb3 --- /dev/null +++ b/src/models/apps/project-management/project.models.js @@ -0,0 +1,41 @@ +import mongoose, { Schema } from "mongoose"; + +const projectSchema = new Schema( + { + name: { + type: String, + require: true, + }, + + description: { + type: String, + required: true, + }, + + ownerId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + + tags: { + type: [String], + required: true, + }, + + banner: { + type: String, + required: false, + default: `https://placehold.co/300x200/png`, + }, + + members: { + type: [mongoose.Schema.Types.ObjectId], + ref: "Member", + required: false, + }, + }, + { timestamps: true } +); + +export const Project = mongoose.model("Project", projectSchema); diff --git a/src/models/apps/project-management/task.models.js b/src/models/apps/project-management/task.models.js new file mode 100644 index 00000000..30509e61 --- /dev/null +++ b/src/models/apps/project-management/task.models.js @@ -0,0 +1,63 @@ +import mongoose, { Schema } from "mongoose"; + +const taskSchema = new Schema( + { + title: { + type: String, + required: true, + }, + description: { + type: String, + required: false, + }, + status: { + type: String, + enum: ["todo", "in_progress", "under_review", "completed"], + default: "todo", + }, + priority: { + type: String, + enum: ["low", "medium", "high"], + default: "low", + }, + projectId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Project", + required: true, + }, + memberId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Member", + required: true, + }, + startDate: { + type: Date, + required: true, + }, + endDate: { + type: Date, + required: true, + }, + tags: { + type: [String], + required: false, + }, + members: { + type: [mongoose.Schema.Types.ObjectId], + ref: "Member", + required: false, + }, + completedDate: { + type: Date, + default: null, + required: false, + }, + subtasks: { + type: [{ title: String, isCompleted: Boolean }], + required: false, + }, + }, + { timestamps: true } +); + +export const Task = mongoose.model("Task", taskSchema); diff --git a/src/routes/apps/project-management/project.routes.js b/src/routes/apps/project-management/project.routes.js new file mode 100644 index 00000000..7ce7ed50 --- /dev/null +++ b/src/routes/apps/project-management/project.routes.js @@ -0,0 +1,108 @@ +import { Router } from "express"; + +import { validate } from "../../../validators/validate.js"; +import { verifyJWT } from "../../../middlewares/auth.middlewares.js"; +import { mongoIdPathVariableValidator } from "../../../validators/common/mongodb.validators.js"; +import { + acceptInvitationValidator, + addMemberValidator, + createProjectValidator, + removeMemberValidator, + updateProjectValidator, +} from "../../../validators/apps/project-management/project.validators.js"; +import { + inviteMember, + createProject, + deleteProject, + getMembers, + getProject, + getProjects, + removeMember, + updateProject, + acceptInvitation, +} from "../../../controllers/apps/project-management/project.controllers.js"; +import { + assignMemberToTask, + createTask, + deleteTask, + getTasks, + removeMemberFromTask, + updateTask, +} from "../../../controllers/apps/project-management/task.controllers.js"; +import { + assignMemberToTaskValidator, + deleteTaskValidator, + taskIdValidator, + taskValidator, +} from "../../../validators/apps/project-management/task.validators.js"; + +const router = Router(); + +router + .route("/") + .get(verifyJWT, getProjects) + .post(verifyJWT, createProjectValidator, validate, createProject); + +router + .route("/member") + .post(verifyJWT, addMemberValidator, validate, inviteMember) + .delete(verifyJWT, removeMemberValidator, validate, removeMember) + .patch(verifyJWT, acceptInvitationValidator, validate, acceptInvitation); + +router + .route("/member/:projectId") + .get( + verifyJWT, + mongoIdPathVariableValidator("projectId"), + validate, + getMembers + ); + +router + .route("/task") + .post(verifyJWT, taskValidator, validate, createTask) + .patch( + verifyJWT, + [...taskValidator, ...taskIdValidator], + validate, + updateTask + ) + .delete(verifyJWT, deleteTaskValidator, validate, deleteTask); + +router.post( + "/task/assign-member-to-task", + [verifyJWT, assignMemberToTaskValidator, validate], + assignMemberToTask +); + +router.post( + "/task/remove-member-from-task", + [verifyJWT, assignMemberToTaskValidator, validate], + removeMemberFromTask +); + +router.route("/task/:projectId").get(verifyJWT, getTasks); + +router + .route("/:projectId") + .get( + verifyJWT, + mongoIdPathVariableValidator("projectId"), + validate, + getProject + ) + .patch( + verifyJWT, + mongoIdPathVariableValidator("projectId"), + updateProjectValidator, + validate, + updateProject + ) + .delete( + verifyJWT, + mongoIdPathVariableValidator("projectId"), + validate, + deleteProject + ); + +export default router; diff --git a/src/validators/apps/project-management/project.validators.js b/src/validators/apps/project-management/project.validators.js new file mode 100644 index 00000000..04c392b1 --- /dev/null +++ b/src/validators/apps/project-management/project.validators.js @@ -0,0 +1,55 @@ +import { body } from "express-validator"; + +const createProjectValidator = [ + body("name").trim().notEmpty().withMessage("Name is required"), + body("description").trim().notEmpty().withMessage("Description is required"), + body("tags") + .notEmpty() + .withMessage("Tags is required") + .isArray() + .withMessage("Tags must be array"), +]; + +const updateProjectValidator = [ + body("name").trim().notEmpty().withMessage("Name is required"), + body("description").trim().notEmpty().withMessage("Description is required"), + body("tags") + .notEmpty() + .withMessage("Tags is required") + .isArray() + .withMessage("Tags must be array"), +]; + +const addMemberValidator = [ + body("email") + .trim() + .notEmpty() + .withMessage("Email is required") + .isEmail() + .withMessage("Email must be a valid email address"), + body("role") + .trim() + .notEmpty() + .withMessage("Role is required") + .isIn(["owner", "admin", "member"]) + .withMessage("Role must be a valid role [owner, admin, member]"), + body("projectId").notEmpty().isMongoId().withMessage(`Invalid project ID`), +]; + +const removeMemberValidator = [ + body("projectId").notEmpty().isMongoId().withMessage("Invalid project ID"), + body("memberId").notEmpty().isMongoId().withMessage("Invalid member ID"), +]; + +const acceptInvitationValidator = [ + body("projectId").notEmpty().isMongoId().withMessage("Invalid project ID"), + body("memberId").notEmpty().isMongoId().withMessage("Invalid member ID"), +]; + +export { + createProjectValidator, + updateProjectValidator, + addMemberValidator, + removeMemberValidator, + acceptInvitationValidator, +}; diff --git a/src/validators/apps/project-management/task.validators.js b/src/validators/apps/project-management/task.validators.js new file mode 100644 index 00000000..4e263348 --- /dev/null +++ b/src/validators/apps/project-management/task.validators.js @@ -0,0 +1,73 @@ +import { body } from "express-validator"; + +const projectIdValidator = [ + body("projectId") + .notEmpty() + .withMessage("Project id is required") + .isMongoId() + .withMessage("Invalid project id"), +]; + +const taskIdValidator = [ + body("taskId") + .notEmpty() + .withMessage("Task id is required") + .isMongoId() + .withMessage("Invalid task id"), +]; + +const memberIdValidator = [ + body("memberId") + .notEmpty() + .withMessage("Member id is required") + .isMongoId() + .withMessage("Invalid member id"), +]; + +const taskValidator = [ + body("title").trim().notEmpty().withMessage("Title is required."), + + body("status") + .trim() + .notEmpty() + .isIn(["todo", "in_progress", "under_review", "completed"]) + .withMessage("Task status should be valid task status"), + + body("priority") + .trim() + .notEmpty() + .isIn(["low", "medium", "high"]) + .withMessage("Priority should be valid project priority"), + + body("startDate") + .notEmpty() + .withMessage("Start date should be required.") + .isISO8601() + .toDate() + .withMessage("Start date should be date type."), + + body("endDate") + .notEmpty() + .withMessage("End date should be required.") + .isISO8601() + .toDate() + .withMessage("End date should be date type."), + + ...projectIdValidator, +]; + +const deleteTaskValidator = [...projectIdValidator, ...taskIdValidator]; + +const assignMemberToTaskValidator = [ + ...projectIdValidator, + ...taskIdValidator, + ...memberIdValidator, +]; + +export { + taskValidator, + projectIdValidator, + taskIdValidator, + deleteTaskValidator, + assignMemberToTaskValidator, +};