feat: menambahkan API update dan delete submission
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user