Merge pull request #11 from internship-humic/feat/implement-mentor-on-task
feat(task): support mentor access for task management
This commit is contained in:
@@ -30,14 +30,6 @@ class ProjectService {
|
||||
|
||||
const normalizedEmails = this.normalizeEmails(member_emails || []);
|
||||
|
||||
if (normalizedEmails.length > 8) {
|
||||
return error(
|
||||
new BadRequestError(
|
||||
"Project member limit exceeded. Maximum member is 8",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const users =
|
||||
normalizedEmails.length > 0
|
||||
? await projectRepository.findUsersByEmails(normalizedEmails)
|
||||
|
||||
@@ -17,8 +17,8 @@ class TaskService {
|
||||
return error(new BadRequestError('Project ID must be a valid number'));
|
||||
}
|
||||
|
||||
if (!actor?.id || actor.role !== 'admin') {
|
||||
return error(new ForbiddenError('Access denied: only admin can create task'));
|
||||
if (!actor?.id || !this.isMentorOrAdmin(actor)) {
|
||||
return error(new ForbiddenError('Access denied: only admin or mentor can create task'));
|
||||
}
|
||||
|
||||
const project = await taskRepository.findProjectById(idProject);
|
||||
@@ -27,6 +27,12 @@ class TaskService {
|
||||
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 task = await taskRepository.createTask({
|
||||
@@ -128,8 +134,8 @@ class TaskService {
|
||||
return error(new BadRequestError('Task ID must be a valid number'));
|
||||
}
|
||||
|
||||
if (!actor?.id || actor.role !== 'admin') {
|
||||
return error(new ForbiddenError('Access denied: only admin can update task'));
|
||||
if (!actor?.id || !this.isMentorOrAdmin(actor)) {
|
||||
return error(new ForbiddenError('Access denied: only admin or mentor can update task'));
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length === 0) {
|
||||
@@ -142,6 +148,12 @@ class TaskService {
|
||||
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);
|
||||
|
||||
if (
|
||||
@@ -182,8 +194,8 @@ class TaskService {
|
||||
return error(new BadRequestError('Task ID must be a valid number'));
|
||||
}
|
||||
|
||||
if (!actor?.id || actor.role !== 'admin') {
|
||||
return error(new ForbiddenError('Access denied: only admin can delete task'));
|
||||
if (!actor?.id || !this.isMentorOrAdmin(actor)) {
|
||||
return error(new ForbiddenError('Access denied: only admin or mentor can delete task'));
|
||||
}
|
||||
|
||||
const task = await taskRepository.findTaskById(idTask);
|
||||
@@ -192,6 +204,12 @@ class TaskService {
|
||||
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);
|
||||
|
||||
if (submissionCount > 0) {
|
||||
@@ -394,6 +412,16 @@ class TaskService {
|
||||
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') {
|
||||
const membership = await taskRepository.checkActiveProjectMember(idProject, actor.id);
|
||||
|
||||
@@ -407,6 +435,10 @@ class TaskService {
|
||||
return new ForbiddenError('Access denied: invalid role');
|
||||
}
|
||||
|
||||
isMentorOrAdmin(actor) {
|
||||
return actor?.role === 'admin' || actor?.role === 'mentor';
|
||||
}
|
||||
|
||||
buildDeadlineAt(deadlineDate, specificTime) {
|
||||
const dateOnly = this.formatDateOnly(new Date(deadlineDate));
|
||||
return new Date(`${dateOnly}T${specificTime}:00`);
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const { taskController } = require('../modules/task');
|
||||
const { verifyJWT } = require('../middleware/verifyJWT');
|
||||
const isAdmin = require('../middleware/isAdmin');
|
||||
const isMentorOrAdmin = require('../middleware/isMentorOrAdmin');
|
||||
const {
|
||||
taskUpload,
|
||||
taskUploadErrorHandler,
|
||||
@@ -344,7 +345,7 @@ const router = express.Router();
|
||||
* /task-api/projects/{id_project}/tasks:
|
||||
* post:
|
||||
* 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]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
@@ -380,7 +381,7 @@ const router = express.Router();
|
||||
* 500:
|
||||
* 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
|
||||
@@ -437,6 +438,90 @@ router.get('/projects/:id_project/tasks', verifyJWT, taskController.getTasksByPr
|
||||
* responses:
|
||||
* 200:
|
||||
* 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:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
@@ -453,7 +538,7 @@ router.get('/tasks/:id', verifyJWT, taskController.getTaskById);
|
||||
* /task-api/tasks/{id}:
|
||||
* patch:
|
||||
* 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]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
@@ -491,14 +576,14 @@ router.get('/tasks/:id', verifyJWT, taskController.getTaskById);
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.patch('/tasks/:id', verifyJWT, isAdmin, taskController.updateTask);
|
||||
router.patch('/tasks/:id', verifyJWT, isMentorOrAdmin, taskController.updateTask);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /task-api/tasks/{id}:
|
||||
* delete:
|
||||
* 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]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
@@ -524,7 +609,7 @@ router.patch('/tasks/:id', verifyJWT, isAdmin, taskController.updateTask);
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.delete('/tasks/:id', verifyJWT, isAdmin, taskController.deleteTask);
|
||||
router.delete('/tasks/:id', verifyJWT, isMentorOrAdmin, taskController.deleteTask);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
|
||||
Reference in New Issue
Block a user