feat: menambahkan API update dan delete submission

This commit is contained in:
Muhammad Zhafran Ilham
2026-06-10 02:56:24 +07:00
parent 25f7d23690
commit 662e9abc51
6 changed files with 371 additions and 3 deletions
@@ -8,6 +8,7 @@ const {
createTaskModel,
updateTaskModel,
submitTaskModel,
updateSubmissionModel,
} = require('../models/task.model');
const getActor = (req) => ({
@@ -198,6 +199,70 @@ class TaskController {
);
}
}
async updateSubmission(req, res) {
try {
const validatePayload = isValidPayload(req.body, updateSubmissionModel);
if (validatePayload.err) {
cleanupUploadedFile(req.file);
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Invalid submission data',
ERROR.EXPECTATION_FAILED
);
}
const actor = getActor(req);
const result = await taskService.updateSubmission(
req.params.id,
validatePayload.data,
req.file,
actor
);
if (result.err) {
cleanupUploadedFile(req.file);
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Submission updated successfully', SUCCESS.OK);
} catch (err) {
cleanupUploadedFile(req.file);
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Unexpected error occurred',
ERROR.INTERNAL_ERROR
);
}
}
async deleteSubmission(req, res) {
try {
const actor = getActor(req);
const result = await taskService.deleteSubmission(req.params.id, actor);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Submission deleted successfully', SUCCESS.OK);
} catch (err) {
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Unexpected error occurred',
ERROR.INTERNAL_ERROR
);
}
}
}
module.exports = new TaskController();
@@ -4,7 +4,7 @@ const fs = require('fs');
const baseUploadDir = process.env.NODE_ENV === 'production'
? '/tmp/uploads'
: path.join(__dirname, '../../uploads');
: path.join(__dirname, '../../../uploads');
const taskUploadDir = path.join(baseUploadDir, 'task-submissions');
@@ -43,7 +43,7 @@ const taskUpload = multer({
storage,
fileFilter,
limits: {
fileSize: 2 * 1024 * 1024, //2MB
fileSize: 10 * 1024 * 1024, //10MB
},
});
@@ -53,7 +53,7 @@ const taskUploadErrorHandler = (err, _req, res, next) => {
return res.status(413).json({
status: false,
data: null,
message: 'File size is too large. Maximum size is 2MB',
message: 'File size is too large. Maximum size is 10MB',
code: 413,
});
}
@@ -92,4 +92,5 @@ const taskUploadErrorHandler = (err, _req, res, next) => {
module.exports = {
taskUpload,
taskUploadErrorHandler,
taskUploadDir
};
+8
View File
@@ -67,8 +67,16 @@ const submitTaskModel = joi.object().keys({
}),
});
const updateSubmissionModel = joi.object().keys({
url_link: joi.string().uri().optional().messages({
'string.base': 'URL link must be a string.',
'string.uri': 'URL link must be a valid URL.',
}),
});
module.exports = {
createTaskModel,
updateTaskModel,
submitTaskModel,
updateSubmissionModel,
};
@@ -140,6 +140,38 @@ class TaskRepository {
});
}
async findSubmissionById(idSubmission) {
return prisma.taskSubmission.findUnique({
where: {
id: parseInt(idSubmission),
},
include: {
task: {
select: {
id: true,
id_project: true,
title: true,
submission_type: true,
deadline_at: true,
project: {
select: {
id: true,
project_name: true,
},
},
},
},
user: {
select: {
id: true,
full_name: true,
email: true,
},
},
},
});
}
async createSubmission(submissionData) {
return prisma.taskSubmission.create({
data: submissionData,
@@ -154,6 +186,14 @@ class TaskRepository {
data: submissionData,
});
}
async deleteSubmission(idSubmission) {
return prisma.taskSubmission.delete({
where: {
id: parseInt(idSubmission),
},
});
}
}
module.exports = new TaskRepository();
+134
View File
@@ -1,4 +1,7 @@
const fs = require('fs');
const path = require('path');
const taskRepository = require('../repositories/task.repository');
const { taskUploadDir } = require('../helpers/taskUpload.helper');
const { data, error } = require('../../../helpers/utils/wrapper');
const {
BadRequestError,
@@ -261,6 +264,7 @@ class TaskService {
let submission;
if (existingSubmission) {
this.deleteLocalSubmissionFile(existingSubmission.file_path);
submission = await taskRepository.updateSubmission(existingSubmission.id, submissionData);
} else {
submission = await taskRepository.createSubmission(submissionData);
@@ -272,6 +276,115 @@ class TaskService {
}
}
async updateSubmission(idSubmission, payload, file, actor = {}) {
try {
if (!this.isPositiveInteger(idSubmission)) {
return error(new BadRequestError('Submission ID must be a valid number'));
}
if (!actor?.id || actor.role !== 'intern') {
return error(new ForbiddenError('Access denied: only intern can update submission'));
}
const submission = await taskRepository.findSubmissionById(idSubmission);
if (!submission) {
return error(new NotFoundError('Submission not found'));
}
if (submission.id_user !== parseInt(actor.id)) {
return error(new ForbiddenError('Access denied: you can only update your own submission'));
}
const membership = await taskRepository.checkActiveProjectMember(
submission.task.id_project,
actor.id
);
if (!membership) {
return error(new ForbiddenError('Access denied: you are not a member of this project'));
}
const updateData = {};
if (submission.task.submission_type === 'file_upload') {
if (!file) {
return error(new BadRequestError('Submission file is required for this task'));
}
if (payload.url_link) {
return error(new BadRequestError('URL link is not allowed for file upload task'));
}
updateData.file_path = `/uploads/task-submissions/${file.filename}`;
updateData.url_link = null;
}
if (submission.task.submission_type === 'url_link') {
if (file) {
return error(new BadRequestError('File upload is not allowed for URL link task'));
}
if (!payload.url_link) {
return error(new BadRequestError('URL link is required for this task'));
}
updateData.file_path = null;
updateData.url_link = payload.url_link;
}
this.deleteLocalSubmissionFile(submission.file_path);
const updatedSubmission = await taskRepository.updateSubmission(
idSubmission,
updateData
);
return data(this.mapSubmission(updatedSubmission));
} catch (err) {
return error(err);
}
}
async deleteSubmission(idSubmission, actor = {}) {
try {
if (!this.isPositiveInteger(idSubmission)) {
return error(new BadRequestError('Submission ID must be a valid number'));
}
if (!actor?.id || actor.role !== 'intern') {
return error(new ForbiddenError('Access denied: only intern can delete submission'));
}
const submission = await taskRepository.findSubmissionById(idSubmission);
if (!submission) {
return error(new NotFoundError('Submission not found'));
}
if (submission.id_user !== parseInt(actor.id)) {
return error(new ForbiddenError('Access denied: you can only delete your own submission'));
}
const membership = await taskRepository.checkActiveProjectMember(
submission.task.id_project,
actor.id
);
if (!membership) {
return error(new ForbiddenError('Access denied: you are not a member of this project'));
}
await taskRepository.deleteSubmission(idSubmission);
this.deleteLocalSubmissionFile(submission.file_path);
return data(null);
} catch (err) {
return error(err);
}
}
async validateProjectAccess(idProject, actor = {}) {
if (!actor?.id || !actor?.role) {
return new ForbiddenError('Access denied: authentication data is missing');
@@ -432,6 +545,27 @@ class TaskService {
};
}
deleteLocalSubmissionFile(filePath) {
if (!filePath || typeof filePath !== 'string') {
return;
}
if (!filePath.startsWith('/uploads/task-submissions/')) {
return;
}
if (!taskUploadDir || typeof taskUploadDir !== 'string') {
return;
}
const filename = path.basename(filePath);
const absolutePath = path.join(taskUploadDir, filename);
if (fs.existsSync(absolutePath)) {
fs.unlinkSync(absolutePath);
}
}
isPositiveInteger(value) {
const number = Number(value);
return Number.isInteger(number) && number > 0;