feat(task): support mentor access for task management

This commit is contained in:
Muhammad Zhafran Ilham
2026-06-23 10:18:30 +07:00
parent 3ec431177a
commit a5593e11cc
3 changed files with 129 additions and 20 deletions
@@ -30,14 +30,6 @@ class ProjectService {
const normalizedEmails = this.normalizeEmails(member_emails || []); const normalizedEmails = this.normalizeEmails(member_emails || []);
if (normalizedEmails.length > 8) {
return error(
new BadRequestError(
"Project member limit exceeded. Maximum member is 8",
),
);
}
const users = const users =
normalizedEmails.length > 0 normalizedEmails.length > 0
? await projectRepository.findUsersByEmails(normalizedEmails) ? await projectRepository.findUsersByEmails(normalizedEmails)
+38 -6
View File
@@ -17,8 +17,8 @@ class TaskService {
return error(new BadRequestError('Project ID must be a valid number')); return error(new BadRequestError('Project ID must be a valid number'));
} }
if (!actor?.id || actor.role !== 'admin') { if (!actor?.id || !this.isMentorOrAdmin(actor)) {
return error(new ForbiddenError('Access denied: only admin can create task')); return error(new ForbiddenError('Access denied: only admin or mentor can create task'));
} }
const project = await taskRepository.findProjectById(idProject); const project = await taskRepository.findProjectById(idProject);
@@ -27,6 +27,12 @@ class TaskService {
return error(new NotFoundError('Project not found')); return error(new NotFoundError('Project not found'));
} }
const accessError = await this.validateProjectAccess(idProject, actor);
if (accessError) {
return error(accessError);
}
const deadlineAt = this.buildDeadlineAt(payload.deadline_date, payload.specific_time); const deadlineAt = this.buildDeadlineAt(payload.deadline_date, payload.specific_time);
const task = await taskRepository.createTask({ const task = await taskRepository.createTask({
@@ -128,8 +134,8 @@ class TaskService {
return error(new BadRequestError('Task ID must be a valid number')); return error(new BadRequestError('Task ID must be a valid number'));
} }
if (!actor?.id || actor.role !== 'admin') { if (!actor?.id || !this.isMentorOrAdmin(actor)) {
return error(new ForbiddenError('Access denied: only admin can update task')); return error(new ForbiddenError('Access denied: only admin or mentor can update task'));
} }
if (Object.keys(payload).length === 0) { if (Object.keys(payload).length === 0) {
@@ -142,6 +148,12 @@ class TaskService {
return error(new NotFoundError('Task not found')); return error(new NotFoundError('Task not found'));
} }
const accessError = await this.validateProjectAccess(existingTask.id_project, actor);
if (accessError) {
return error(accessError);
}
const submissionCount = await taskRepository.countSubmissionsByTask(idTask); const submissionCount = await taskRepository.countSubmissionsByTask(idTask);
if ( if (
@@ -182,8 +194,8 @@ class TaskService {
return error(new BadRequestError('Task ID must be a valid number')); return error(new BadRequestError('Task ID must be a valid number'));
} }
if (!actor?.id || actor.role !== 'admin') { if (!actor?.id || !this.isMentorOrAdmin(actor)) {
return error(new ForbiddenError('Access denied: only admin can delete task')); return error(new ForbiddenError('Access denied: only admin or mentor can delete task'));
} }
const task = await taskRepository.findTaskById(idTask); const task = await taskRepository.findTaskById(idTask);
@@ -192,6 +204,12 @@ class TaskService {
return error(new NotFoundError('Task not found')); return error(new NotFoundError('Task not found'));
} }
const accessError = await this.validateProjectAccess(task.id_project, actor);
if (accessError) {
return error(accessError);
}
const submissionCount = await taskRepository.countSubmissionsByTask(idTask); const submissionCount = await taskRepository.countSubmissionsByTask(idTask);
if (submissionCount > 0) { if (submissionCount > 0) {
@@ -394,6 +412,16 @@ class TaskService {
return null; return null;
} }
if (actor.role === 'mentor') {
const project = await taskRepository.findProjectById(idProject);
if (!project || project.id_admin !== parseInt(actor.id)) {
return new ForbiddenError('Access denied: you are not the mentor of this project');
}
return null;
}
if (actor.role === 'intern') { if (actor.role === 'intern') {
const membership = await taskRepository.checkActiveProjectMember(idProject, actor.id); const membership = await taskRepository.checkActiveProjectMember(idProject, actor.id);
@@ -407,6 +435,10 @@ class TaskService {
return new ForbiddenError('Access denied: invalid role'); return new ForbiddenError('Access denied: invalid role');
} }
isMentorOrAdmin(actor) {
return actor?.role === 'admin' || actor?.role === 'mentor';
}
buildDeadlineAt(deadlineDate, specificTime) { buildDeadlineAt(deadlineDate, specificTime) {
const dateOnly = this.formatDateOnly(new Date(deadlineDate)); const dateOnly = this.formatDateOnly(new Date(deadlineDate));
return new Date(`${dateOnly}T${specificTime}:00`); return new Date(`${dateOnly}T${specificTime}:00`);
+91 -6
View File
@@ -2,6 +2,7 @@ const express = require('express');
const { taskController } = require('../modules/task'); const { taskController } = require('../modules/task');
const { verifyJWT } = require('../middleware/verifyJWT'); const { verifyJWT } = require('../middleware/verifyJWT');
const isAdmin = require('../middleware/isAdmin'); const isAdmin = require('../middleware/isAdmin');
const isMentorOrAdmin = require('../middleware/isMentorOrAdmin');
const { const {
taskUpload, taskUpload,
taskUploadErrorHandler, taskUploadErrorHandler,
@@ -344,7 +345,7 @@ const router = express.Router();
* /task-api/projects/{id_project}/tasks: * /task-api/projects/{id_project}/tasks:
* post: * post:
* summary: Create task in project * summary: Create task in project
* description: Create a new task inside a project. Only admin can access this endpoint. * description: Create a new task inside a project. Only admin or mentor can access this endpoint. Mentors can only create tasks in projects they own.
* tags: [Task] * tags: [Task]
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
@@ -380,7 +381,7 @@ const router = express.Router();
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.post('/projects/:id_project/tasks', verifyJWT, isAdmin, taskController.createTask); router.post('/projects/:id_project/tasks', verifyJWT, isMentorOrAdmin, taskController.createTask);
/** /**
* @swagger * @swagger
@@ -437,6 +438,90 @@ router.get('/projects/:id_project/tasks', verifyJWT, taskController.getTasksByPr
* responses: * responses:
* 200: * 200:
* description: Task detail retrieved successfully * description: Task detail retrieved successfully
* content:
* application/json:
* schema:
* oneOf:
* - $ref: '#/components/schemas/AdminTaskDetailResponse'
* - $ref: '#/components/schemas/InternTaskDetailResponse'
* examples:
* admin:
* summary: Admin task detail response
* value:
* status: true
* data:
* id: 1
* id_project: 1
* title: "Laporan Tugas 2"
* description: "Berisi design UI/UX dan user flow dari aplikasi Internify."
* deadline_at: "2026-05-28T23:59:00.000Z"
* submission_type: "file_upload"
* created_at: "2026-06-09T10:00:00.000Z"
* updated_at: "2026-06-09T10:00:00.000Z"
* project:
* id: 1
* project_name: "Internify Project"
* project_icon: "code"
* submission_summary:
* total_members: 3
* total_submitted: 2
* total_done: 1
* total_pending: 0
* total_overdue: 2
* submissions:
* - id_user: 1
* full_name: "Rafi Athallah"
* email: "rafi@student.com"
* profile_picture: null
* professional_bio: "Backend developer intern"
* submitted_at: "2026-05-25T10:00:00.000Z"
* display_status: "overdue"
* submission:
* id: 1
* id_task: 1
* id_user: 1
* file_path: "/uploads/task-submissions/report.pdf"
* url_link: null
* submitted_at: "2026-05-25T10:00:00.000Z"
* updated_at: "2026-05-25T10:00:00.000Z"
* - id_user: 2
* full_name: "Alice Smith"
* email: "alice@student.com"
* profile_picture: null
* professional_bio: null
* submitted_at: null
* display_status: "overdue"
* submission: null
* message: "Task detail retrieved successfully"
* code: 200
* intern:
* summary: Intern task detail response
* value:
* status: true
* data:
* id: 1
* id_project: 1
* title: "Laporan Tugas 2"
* description: "Berisi design UI/UX dan user flow dari aplikasi Internify."
* deadline_at: "2026-05-28T23:59:00.000Z"
* submission_type: "file_upload"
* created_at: "2026-06-09T10:00:00.000Z"
* updated_at: "2026-06-09T10:00:00.000Z"
* project:
* id: 1
* project_name: "Internify Project"
* project_icon: "code"
* display_status: "pending"
* my_submission:
* id: 1
* id_task: 1
* id_user: 1
* file_path: "/uploads/task-submissions/report.pdf"
* url_link: null
* submitted_at: "2026-05-25T10:00:00.000Z"
* updated_at: "2026-05-25T10:00:00.000Z"
* message: "Task detail retrieved successfully"
* code: 200
* 401: * 401:
* description: Unauthorized * description: Unauthorized
* 403: * 403:
@@ -453,7 +538,7 @@ router.get('/tasks/:id', verifyJWT, taskController.getTaskById);
* /task-api/tasks/{id}: * /task-api/tasks/{id}:
* patch: * patch:
* summary: Update task * summary: Update task
* description: Update task information. Submission type cannot be changed if the task already has submissions. Only admin can access this endpoint. * description: Update task information. Submission type cannot be changed if the task already has submissions. Only admin or mentor can access this endpoint. Mentors can only update tasks in projects they own.
* tags: [Task] * tags: [Task]
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
@@ -491,14 +576,14 @@ router.get('/tasks/:id', verifyJWT, taskController.getTaskById);
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.patch('/tasks/:id', verifyJWT, isAdmin, taskController.updateTask); router.patch('/tasks/:id', verifyJWT, isMentorOrAdmin, taskController.updateTask);
/** /**
* @swagger * @swagger
* /task-api/tasks/{id}: * /task-api/tasks/{id}:
* delete: * delete:
* summary: Delete task * summary: Delete task
* description: Delete task only if it does not have submissions. Only admin can access this endpoint. * description: Delete task only if it does not have submissions. Only admin or mentor can access this endpoint. Mentors can only delete tasks in projects they own.
* tags: [Task] * tags: [Task]
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
@@ -524,7 +609,7 @@ router.patch('/tasks/:id', verifyJWT, isAdmin, taskController.updateTask);
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.delete('/tasks/:id', verifyJWT, isAdmin, taskController.deleteTask); router.delete('/tasks/:id', verifyJWT, isMentorOrAdmin, taskController.deleteTask);
/** /**
* @swagger * @swagger