feat: CRUD untuk feature project intern
This commit is contained in:
@@ -63,6 +63,10 @@ const swaggerOptions = {
|
|||||||
{
|
{
|
||||||
name: "Feedback",
|
name: "Feedback",
|
||||||
description: "Internship feedback and testimonial management endpoints"
|
description: "Internship feedback and testimonial management endpoints"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Project",
|
||||||
|
description: "Internify LMS project management endpoints"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
+83
-78
@@ -1,78 +1,83 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const adminRoutes = require('./routes/admin.routes');
|
const adminRoutes = require('./routes/admin.routes');
|
||||||
const authRoutes = require('./routes/auth.routes');
|
const authRoutes = require('./routes/auth.routes');
|
||||||
const lowonganMagangRoutes = require('./routes/lowonganMagang.routes');
|
const lowonganMagangRoutes = require('./routes/lowonganMagang.routes');
|
||||||
const lamaranMagangRoutes = require('./routes/lamaranMagang.routes');
|
const lamaranMagangRoutes = require('./routes/lamaranMagang.routes');
|
||||||
const mahasiswaRoutes = require('./routes/mahasiswa.routes');
|
const mahasiswaRoutes = require('./routes/mahasiswa.routes');
|
||||||
const partnershipRoutes = require('./routes/partnership.routes');
|
const partnershipRoutes = require('./routes/partnership.routes');
|
||||||
const hasilResearchRoutes = require('./routes/hasilResearch.routes');
|
const hasilResearchRoutes = require('./routes/hasilResearch.routes');
|
||||||
const faqRoutes = require('./routes/faq.routes');
|
const faqRoutes = require('./routes/faq.routes');
|
||||||
const feedbackRoutes = require('./routes/feedback.routes');
|
const feedbackRoutes = require('./routes/feedback.routes');
|
||||||
const batchRoutes = require('./routes/batch.routes');
|
const batchRoutes = require('./routes/batch.routes');
|
||||||
const setupSwaggerDocs = require('./docs/swagger');
|
const projectRoutes = require('./routes/project.routes');
|
||||||
|
const setupSwaggerDocs = require('./docs/swagger');
|
||||||
const PORT = process.env.PORT;
|
|
||||||
const app = express();
|
const PORT = process.env.PORT;
|
||||||
|
const app = express();
|
||||||
const corsOptions = {
|
|
||||||
origin: true,
|
const corsOptions = {
|
||||||
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"],
|
origin: true,
|
||||||
credentials: true,
|
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"],
|
||||||
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept"],
|
credentials: true,
|
||||||
exposedHeaders: ["Content-Range", "X-Content-Range"],
|
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept"],
|
||||||
optionsSuccessStatus: 200,
|
exposedHeaders: ["Content-Range", "X-Content-Range"],
|
||||||
};
|
optionsSuccessStatus: 200,
|
||||||
|
};
|
||||||
const uploadDir = process.env.NODE_ENV === 'production'
|
|
||||||
? '/tmp/uploads'
|
const uploadDir = process.env.NODE_ENV === 'production'
|
||||||
: path.join(__dirname, 'uploads');
|
? '/tmp/uploads'
|
||||||
|
: path.join(__dirname, 'uploads');
|
||||||
app.options('*', cors(corsOptions));
|
|
||||||
|
app.options('*', cors(corsOptions));
|
||||||
app.use(express.json());
|
|
||||||
app.use(cors(corsOptions));
|
app.use(express.json());
|
||||||
|
app.use(cors(corsOptions));
|
||||||
app.use("/uploads", express.static(uploadDir), (req, res, next) => {
|
|
||||||
if (!res.headersSent) {
|
app.use("/uploads", express.static(uploadDir), (req, res, next) => {
|
||||||
res.status(404).json({
|
if (!res.headersSent) {
|
||||||
status: false,
|
res.status(404).json({
|
||||||
message: `File not found: ${req.path}`,
|
status: false,
|
||||||
code: 404
|
message: `File not found: ${req.path}`,
|
||||||
});
|
code: 404
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
app.use("/admin-api", adminRoutes);
|
});
|
||||||
app.use("/auth-api", authRoutes);
|
app.use("/admin-api", adminRoutes);
|
||||||
app.use("/lowongan-magang-api", lowonganMagangRoutes);
|
app.use("/auth-api", authRoutes);
|
||||||
app.use("/lamaran-magang-api", lamaranMagangRoutes);
|
app.use("/lowongan-magang-api", lowonganMagangRoutes);
|
||||||
app.use("/mahasiswa-api", mahasiswaRoutes);
|
app.use("/lamaran-magang-api", lamaranMagangRoutes);
|
||||||
app.use("/partnership-api", partnershipRoutes);
|
app.use("/mahasiswa-api", mahasiswaRoutes);
|
||||||
app.use("/hasil-research-api", hasilResearchRoutes);
|
app.use("/partnership-api", partnershipRoutes);
|
||||||
app.use("/faq-api", faqRoutes);
|
app.use("/hasil-research-api", hasilResearchRoutes);
|
||||||
app.use("/feedback-api", feedbackRoutes);
|
app.use("/faq-api", faqRoutes);
|
||||||
app.use("/batch-api", batchRoutes);
|
app.use("/feedback-api", feedbackRoutes);
|
||||||
|
app.use("/batch-api", batchRoutes);
|
||||||
setupSwaggerDocs(app);
|
app.use("/project-api", projectRoutes);
|
||||||
|
// app.use("/project-member-api", projectMemberRoutes);
|
||||||
app.use((req, res) => {
|
// app.use("/task-api", taskRoutes);
|
||||||
res.status(404).json({
|
|
||||||
status: false,
|
|
||||||
message: `Route not found: ${req.method} ${req.path}`,
|
setupSwaggerDocs(app);
|
||||||
code: 404
|
|
||||||
});
|
app.use((req, res) => {
|
||||||
});
|
res.status(404).json({
|
||||||
|
status: false,
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
message: `Route not found: ${req.method} ${req.path}`,
|
||||||
app.listen(PORT, () => {
|
code: 404
|
||||||
console.log(`listening at http://localhost:${PORT}`);
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
module.exports = app;
|
app.listen(PORT, () => {
|
||||||
|
console.log(`listening at http://localhost:${PORT}`);
|
||||||
/*
|
});
|
||||||
NOTE : saran saya untuk penamaan variabel untuk setiap response dibuat konsisten menjadi inggris, tolong komunikasi aja ke fe 😁
|
}
|
||||||
*/
|
|
||||||
|
module.exports = app;
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOTE : saran saya untuk penamaan variabel untuk setiap response dibuat konsisten menjadi inggris, tolong komunikasi aja ke fe 😁
|
||||||
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
const projectService = require('../services/project.service');
|
||||||
|
const { response, error } = require('../../../helpers/utils/wrapper');
|
||||||
|
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||||
|
const { InternalServerError } = require('../../../helpers/error');
|
||||||
|
const { isValidPayload } = require('../../../helpers/utils/validator');
|
||||||
|
const {
|
||||||
|
createProjectModel,
|
||||||
|
updateProjectModel,
|
||||||
|
getAllProjectsModel,
|
||||||
|
} = require('../models/project.model');
|
||||||
|
|
||||||
|
const getActor = (req) => ({
|
||||||
|
id: req.id,
|
||||||
|
role: req.role,
|
||||||
|
email: req.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
class ProjectController {
|
||||||
|
async createProject(req, res) {
|
||||||
|
try {
|
||||||
|
const validatePayload = isValidPayload(req.body, createProjectModel);
|
||||||
|
|
||||||
|
if (validatePayload.err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
{ err: validatePayload.err, data: null },
|
||||||
|
'Invalid project data',
|
||||||
|
ERROR.EXPECTATION_FAILED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = getActor(req);
|
||||||
|
const result = await projectService.createProject(validatePayload.data, actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Project created successfully', SUCCESS.CREATED);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllProjects(req, res) {
|
||||||
|
try {
|
||||||
|
const validatePayload = isValidPayload(req.query, getAllProjectsModel);
|
||||||
|
|
||||||
|
if (validatePayload.err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
{ err: validatePayload.err, data: null },
|
||||||
|
'Invalid query data',
|
||||||
|
ERROR.EXPECTATION_FAILED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = getActor(req);
|
||||||
|
const result = await projectService.getAllProjects(validatePayload.data, actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Projects retrieved successfully', SUCCESS.OK);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectById(req, res) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const actor = getActor(req);
|
||||||
|
|
||||||
|
const result = await projectService.getProjectById(id, actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Project detail retrieved successfully', SUCCESS.OK);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProject(req, res) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const validatePayload = isValidPayload(req.body, updateProjectModel);
|
||||||
|
|
||||||
|
if (validatePayload.err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
{ err: validatePayload.err, data: null },
|
||||||
|
'Invalid project data',
|
||||||
|
ERROR.EXPECTATION_FAILED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = getActor(req);
|
||||||
|
const result = await projectService.updateProject(id, validatePayload.data, actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Project updated successfully', SUCCESS.OK);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async archiveProject(req, res) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const actor = getActor(req);
|
||||||
|
|
||||||
|
const result = await projectService.archiveProject(id, actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Project archived successfully', SUCCESS.OK);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new ProjectController();
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const projectController = require("./controllers/project.controller");
|
||||||
|
const projectRepository = require("./repositories/project.repository");
|
||||||
|
const projectService = require("./services/project.service");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
projectController,
|
||||||
|
projectService,
|
||||||
|
projectRepository
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
const joi = require('joi');
|
||||||
|
|
||||||
|
const PROJECT_ICON_VALUES = [
|
||||||
|
'code',
|
||||||
|
'chart',
|
||||||
|
'cloud',
|
||||||
|
'mobile',
|
||||||
|
'gear',
|
||||||
|
'users',
|
||||||
|
'clipboard',
|
||||||
|
'speedometer',
|
||||||
|
'lightbulb',
|
||||||
|
'shield',
|
||||||
|
];
|
||||||
|
|
||||||
|
const PROJECT_STATUS_VALUES = [
|
||||||
|
'active',
|
||||||
|
'completed',
|
||||||
|
'archived',
|
||||||
|
];
|
||||||
|
|
||||||
|
const createProjectModel = joi.object().keys({
|
||||||
|
project_icon: joi.string().valid(...PROJECT_ICON_VALUES).required().messages({
|
||||||
|
'string.base': 'Project icon must be a string.',
|
||||||
|
'string.empty': 'Project icon cannot be empty.',
|
||||||
|
'any.only': `Project icon must be one of: ${PROJECT_ICON_VALUES.join(', ')}.`,
|
||||||
|
'any.required': 'Project icon is required.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
project_name: joi.string().max(255).required().messages({
|
||||||
|
'string.base': 'Project name must be a string.',
|
||||||
|
'string.empty': 'Project name cannot be empty.',
|
||||||
|
'string.max': 'Project name must not exceed {#limit} characters.',
|
||||||
|
'any.required': 'Project name is required.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
description: joi.string().optional().allow('', null).messages({
|
||||||
|
'string.base': 'Project description must be a string.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
start_date: joi.date().required().messages({
|
||||||
|
'date.base': 'Start date must be a valid date.',
|
||||||
|
'any.required': 'Start date is required.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
end_date: joi.date().min(joi.ref('start_date')).required().messages({
|
||||||
|
'date.base': 'End date must be a valid date.',
|
||||||
|
'date.min': 'End date must be greater than or equal to start date.',
|
||||||
|
'any.required': 'End date is required.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
member_emails: joi.array().items(
|
||||||
|
joi.string().email({ tlds: { allow: false } }).messages({
|
||||||
|
'string.email': 'Member email format is invalid.',
|
||||||
|
})
|
||||||
|
).optional().default([]).messages({
|
||||||
|
'array.base': 'Member emails must be an array.',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateProjectModel = joi.object().keys({
|
||||||
|
project_icon: joi.string().valid(...PROJECT_ICON_VALUES).optional().messages({
|
||||||
|
'string.base': 'Project icon must be a string.',
|
||||||
|
'any.only': `Project icon must be one of: ${PROJECT_ICON_VALUES.join(', ')}.`,
|
||||||
|
}),
|
||||||
|
|
||||||
|
project_name: joi.string().max(255).optional().messages({
|
||||||
|
'string.base': 'Project name must be a string.',
|
||||||
|
'string.max': 'Project name must not exceed {#limit} characters.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
description: joi.string().optional().allow('', null).messages({
|
||||||
|
'string.base': 'Project description must be a string.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
start_date: joi.date().optional().messages({
|
||||||
|
'date.base': 'Start date must be a valid date.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
end_date: joi.date().optional().messages({
|
||||||
|
'date.base': 'End date must be a valid date.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
status: joi.string().valid(...PROJECT_STATUS_VALUES).optional().messages({
|
||||||
|
'string.base': 'Project status must be a string.',
|
||||||
|
'any.only': `Project status must be one of: ${PROJECT_STATUS_VALUES.join(', ')}.`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAllProjectsModel = joi.object().keys({
|
||||||
|
status: joi.string().valid(...PROJECT_STATUS_VALUES).optional().messages({
|
||||||
|
'string.base': 'Project status must be a string.',
|
||||||
|
'any.only': `Project status must be one of: ${PROJECT_STATUS_VALUES.join(', ')}.`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createProjectModel,
|
||||||
|
updateProjectModel,
|
||||||
|
getAllProjectsModel,
|
||||||
|
};
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
const prisma = require("../../../helpers/db/db_connection");
|
||||||
|
|
||||||
|
class ProjectRepository {
|
||||||
|
async findAdminById(id) {
|
||||||
|
return prisma.admin.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findUsersByEmails(emails) {
|
||||||
|
return prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
email: {
|
||||||
|
in: emails,
|
||||||
|
},
|
||||||
|
is_active: true,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
lamaran_magang: {
|
||||||
|
include: {
|
||||||
|
lowongan_magang: {
|
||||||
|
select: {
|
||||||
|
posisi: true,
|
||||||
|
kelompok_peminatan: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWithMembers(projectData, userIds = []) {
|
||||||
|
return prisma.$transaction(async (tx) => {
|
||||||
|
const project = await tx.project.create({
|
||||||
|
data: projectData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
await tx.projectMember.createMany({
|
||||||
|
data: userIds.map((id_user) => ({
|
||||||
|
id_project: project.id,
|
||||||
|
id_user,
|
||||||
|
status: "active",
|
||||||
|
})),
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.project.findUnique({
|
||||||
|
where: {
|
||||||
|
id: project.id,
|
||||||
|
},
|
||||||
|
include: this.projectDetailInclude(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll({ status, actor }) {
|
||||||
|
const where = {};
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
where.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor?.role === "intern") {
|
||||||
|
where.members = {
|
||||||
|
some: {
|
||||||
|
id_user: parseInt(actor.id),
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.project.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: {
|
||||||
|
created_at: "desc",
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
admin: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
nama_depan: true,
|
||||||
|
nama_belakang: true,
|
||||||
|
email: true,
|
||||||
|
profile_picture: true,
|
||||||
|
professional_bio: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
members: {
|
||||||
|
where: {
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id) {
|
||||||
|
return prisma.project.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(id),
|
||||||
|
},
|
||||||
|
include: this.projectDetailInclude(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateById(id, projectData) {
|
||||||
|
return prisma.project.update({
|
||||||
|
where: {
|
||||||
|
id: parseInt(id),
|
||||||
|
},
|
||||||
|
data: projectData,
|
||||||
|
include: this.projectDetailInclude(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async archiveById(id) {
|
||||||
|
return prisma.project.update({
|
||||||
|
where: {
|
||||||
|
id: parseInt(id),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "archived",
|
||||||
|
},
|
||||||
|
include: this.projectDetailInclude(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkActiveMember(idProject, idUser) {
|
||||||
|
return prisma.projectMember.findFirst({
|
||||||
|
where: {
|
||||||
|
id_project: parseInt(idProject),
|
||||||
|
id_user: parseInt(idUser),
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
projectDetailInclude() {
|
||||||
|
return {
|
||||||
|
admin: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
nama_depan: true,
|
||||||
|
nama_belakang: true,
|
||||||
|
email: true,
|
||||||
|
profile_picture: true,
|
||||||
|
professional_bio: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
members: {
|
||||||
|
where: {
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
full_name: true,
|
||||||
|
email: true,
|
||||||
|
professional_bio: true,
|
||||||
|
is_active: true,
|
||||||
|
lamaran_magang: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
lowongan_magang: {
|
||||||
|
select: {
|
||||||
|
posisi: true,
|
||||||
|
kelompok_peminatan: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
deadline_at: true,
|
||||||
|
submission_type: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
deadline_at: "asc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new ProjectRepository();
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
const projectRepository = require("../repositories/project.repository");
|
||||||
|
const { data, error } = require("../../../helpers/utils/wrapper");
|
||||||
|
const {
|
||||||
|
BadRequestError,
|
||||||
|
NotFoundError,
|
||||||
|
ForbiddenError,
|
||||||
|
} = require("../../../helpers/error");
|
||||||
|
|
||||||
|
class ProjectService {
|
||||||
|
async createProject(projectPayload, actor) {
|
||||||
|
try {
|
||||||
|
if (!actor?.id) {
|
||||||
|
return error(new BadRequestError("Admin ID is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.role !== "admin") {
|
||||||
|
return error(
|
||||||
|
new ForbiddenError("Access denied: only admin can create project"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { member_emails, ...projectData } = projectPayload;
|
||||||
|
|
||||||
|
const admin = await projectRepository.findAdminById(actor.id);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return error(new NotFoundError("Admin not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (normalizedEmails.length > 0) {
|
||||||
|
const foundEmails = users.map((user) => user.email.toLowerCase());
|
||||||
|
const missingEmails = normalizedEmails.filter(
|
||||||
|
(email) => !foundEmails.includes(email),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingEmails.length > 0) {
|
||||||
|
return error(
|
||||||
|
new NotFoundError(
|
||||||
|
`User with these emails not found: ${missingEmails.join(", ")}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProjectData = {
|
||||||
|
id_admin: parseInt(actor.id),
|
||||||
|
project_icon: projectData.project_icon,
|
||||||
|
project_name: projectData.project_name,
|
||||||
|
description: projectData.description || "",
|
||||||
|
start_date: projectData.start_date,
|
||||||
|
end_date: projectData.end_date,
|
||||||
|
max_members: 8,
|
||||||
|
status: "active",
|
||||||
|
};
|
||||||
|
|
||||||
|
const userIds = users.map((user) => user.id);
|
||||||
|
const project = await projectRepository.createWithMembers(
|
||||||
|
newProjectData,
|
||||||
|
userIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
return data(this.mapProjectDetail(project));
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllProjects(query = {}, actor = {}) {
|
||||||
|
try {
|
||||||
|
const projects = await projectRepository.findAll({
|
||||||
|
status: query.status,
|
||||||
|
actor,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mappedProjects = projects.map((project) => ({
|
||||||
|
id: project.id,
|
||||||
|
project_icon: project.project_icon,
|
||||||
|
project_name: project.project_name,
|
||||||
|
description: project.description,
|
||||||
|
start_date: project.start_date,
|
||||||
|
end_date: project.end_date,
|
||||||
|
max_members: project.max_members,
|
||||||
|
status: project.status,
|
||||||
|
admin: this.mapAdmin(project.admin),
|
||||||
|
total_members: project.members?.length || 0,
|
||||||
|
total_tasks: project.tasks?.length || 0,
|
||||||
|
created_at: project.created_at,
|
||||||
|
updated_at: project.updated_at,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return data(mappedProjects);
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectById(id, actor = {}) {
|
||||||
|
try {
|
||||||
|
const project = await projectRepository.findById(id);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return error(new NotFoundError("Project not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessError = await this.validateProjectAccess(project.id, actor);
|
||||||
|
if (accessError) {
|
||||||
|
return error(accessError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data(this.mapProjectDetail(project));
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProject(id, updatePayload, actor = {}) {
|
||||||
|
try {
|
||||||
|
if (!actor?.id) {
|
||||||
|
return error(new BadRequestError("Admin ID is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.role !== "admin") {
|
||||||
|
return error(
|
||||||
|
new ForbiddenError("Access denied: only admin can update project"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingProject = await projectRepository.findById(id);
|
||||||
|
|
||||||
|
if (!existingProject) {
|
||||||
|
return error(new NotFoundError("Project not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updatePayload).length === 0) {
|
||||||
|
return error(
|
||||||
|
new BadRequestError(
|
||||||
|
"At least one field is required to update project",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = { ...updatePayload };
|
||||||
|
|
||||||
|
if (updateData.start_date || updateData.end_date) {
|
||||||
|
const startDate = updateData.start_date || existingProject.start_date;
|
||||||
|
const endDate = updateData.end_date || existingProject.end_date;
|
||||||
|
|
||||||
|
if (new Date(endDate) < new Date(startDate)) {
|
||||||
|
return error(
|
||||||
|
new BadRequestError(
|
||||||
|
"End date must be greater than or equal to start date",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateData.description === null) {
|
||||||
|
updateData.description = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProject = await projectRepository.updateById(id, updateData);
|
||||||
|
|
||||||
|
return data(this.mapProjectDetail(updatedProject));
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async archiveProject(id, actor = {}) {
|
||||||
|
try {
|
||||||
|
if (!actor?.id) {
|
||||||
|
return error(new BadRequestError("Admin ID is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.role !== "admin") {
|
||||||
|
return error(
|
||||||
|
new ForbiddenError("Access denied: only admin can archive project"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingProject = await projectRepository.findById(id);
|
||||||
|
|
||||||
|
if (!existingProject) {
|
||||||
|
return error(new NotFoundError("Project not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingProject.status === "archived") {
|
||||||
|
return data(this.mapProjectDetail(existingProject));
|
||||||
|
}
|
||||||
|
|
||||||
|
const archivedProject = await projectRepository.archiveById(id);
|
||||||
|
|
||||||
|
return data(this.mapProjectDetail(archivedProject));
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateProjectAccess(idProject, actor = {}) {
|
||||||
|
if (!actor?.id || !actor?.role) {
|
||||||
|
return new ForbiddenError(
|
||||||
|
"Access denied: authentication data is missing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.role === "admin") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.role === "intern") {
|
||||||
|
const membership = await projectRepository.checkActiveMember(
|
||||||
|
idProject,
|
||||||
|
actor.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
return new ForbiddenError(
|
||||||
|
"Access denied: you are not a member of this project",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ForbiddenError("Access denied: invalid role");
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeEmails(emails = []) {
|
||||||
|
return [
|
||||||
|
...new Set(
|
||||||
|
emails.filter(Boolean).map((email) => email.trim().toLowerCase()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
mapProjectDetail(project) {
|
||||||
|
if (!project) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: project.id,
|
||||||
|
project_icon: project.project_icon,
|
||||||
|
project_name: project.project_name,
|
||||||
|
description: project.description,
|
||||||
|
start_date: project.start_date,
|
||||||
|
end_date: project.end_date,
|
||||||
|
max_members: project.max_members,
|
||||||
|
status: project.status,
|
||||||
|
admin: this.mapAdmin(project.admin),
|
||||||
|
members:
|
||||||
|
project.members?.map((member) => this.mapProjectMember(member)) || [],
|
||||||
|
tasks: project.tasks || [],
|
||||||
|
total_members: project.members?.length || 0,
|
||||||
|
total_tasks: project.tasks?.length || 0,
|
||||||
|
created_at: project.created_at,
|
||||||
|
updated_at: project.updated_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mapAdmin(admin) {
|
||||||
|
if (!admin) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: admin.id,
|
||||||
|
full_name: `${admin.nama_depan}${admin.nama_belakang ? ` ${admin.nama_belakang}` : ""}`,
|
||||||
|
email: admin.email,
|
||||||
|
profile_picture: admin.profile_picture || null,
|
||||||
|
professional_bio: admin.professional_bio || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mapProjectMember(member) {
|
||||||
|
if (!member) return null;
|
||||||
|
|
||||||
|
const user = member.user;
|
||||||
|
const lowongan = user?.lamaran_magang?.lowongan_magang;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: member.id,
|
||||||
|
id_user: member.id_user,
|
||||||
|
status: member.status,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
full_name: user.full_name,
|
||||||
|
email: user.email,
|
||||||
|
professional_bio: user.professional_bio || null,
|
||||||
|
position: lowongan?.posisi || null,
|
||||||
|
kelompok_peminatan: lowongan?.kelompok_peminatan || null,
|
||||||
|
},
|
||||||
|
created_at: member.created_at,
|
||||||
|
updated_at: member.updated_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new ProjectService();
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { projectController } = require('../modules/project');
|
||||||
|
const { verifyJWT } = require('../middleware/verifyJWT');
|
||||||
|
const isAdmin = require('../middleware/isAdmin');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* Project:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* project_icon:
|
||||||
|
* type: string
|
||||||
|
* enum: [code, chart, cloud, mobile, gear, users, clipboard, speedometer, lightbulb, shield]
|
||||||
|
* example: code
|
||||||
|
* project_name:
|
||||||
|
* type: string
|
||||||
|
* example: Internify LMS
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* example: Project for developing Internify LMS features
|
||||||
|
* start_date:
|
||||||
|
* type: string
|
||||||
|
* format: date
|
||||||
|
* example: 2026-06-10
|
||||||
|
* end_date:
|
||||||
|
* type: string
|
||||||
|
* format: date
|
||||||
|
* example: 2026-08-10
|
||||||
|
* max_members:
|
||||||
|
* type: integer
|
||||||
|
* example: 8
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* enum: [active, completed, archived]
|
||||||
|
* example: active
|
||||||
|
* total_members:
|
||||||
|
* type: integer
|
||||||
|
* example: 5
|
||||||
|
* total_tasks:
|
||||||
|
* type: integer
|
||||||
|
* example: 3
|
||||||
|
* ProjectCreateRequest:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - project_icon
|
||||||
|
* - project_name
|
||||||
|
* - start_date
|
||||||
|
* - end_date
|
||||||
|
* properties:
|
||||||
|
* project_icon:
|
||||||
|
* type: string
|
||||||
|
* enum: [code, chart, cloud, mobile, gear, users, clipboard, speedometer, lightbulb, shield]
|
||||||
|
* example: code
|
||||||
|
* project_name:
|
||||||
|
* type: string
|
||||||
|
* example: Internify LMS
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* example: Project for developing Internify LMS features
|
||||||
|
* start_date:
|
||||||
|
* type: string
|
||||||
|
* format: date
|
||||||
|
* example: 2026-06-10
|
||||||
|
* end_date:
|
||||||
|
* type: string
|
||||||
|
* format: date
|
||||||
|
* example: 2026-08-10
|
||||||
|
* member_emails:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* example: ["intern1@example.com", "intern2@example.com"]
|
||||||
|
* ProjectUpdateRequest:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* project_icon:
|
||||||
|
* type: string
|
||||||
|
* enum: [code, chart, cloud, mobile, gear, users, clipboard, speedometer, lightbulb, shield]
|
||||||
|
* example: cloud
|
||||||
|
* project_name:
|
||||||
|
* type: string
|
||||||
|
* example: Internify LMS Updated
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* example: Updated project description
|
||||||
|
* start_date:
|
||||||
|
* type: string
|
||||||
|
* format: date
|
||||||
|
* example: 2026-06-15
|
||||||
|
* end_date:
|
||||||
|
* type: string
|
||||||
|
* format: date
|
||||||
|
* example: 2026-08-20
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* enum: [active, completed, archived]
|
||||||
|
* example: completed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /project-api/add:
|
||||||
|
* post:
|
||||||
|
* summary: Create a new project
|
||||||
|
* description: Create a new Internify LMS project. Admin can optionally invite interns by email.
|
||||||
|
* tags: [Project]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/ProjectCreateRequest'
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: Project created successfully
|
||||||
|
* 400:
|
||||||
|
* description: Bad request
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden
|
||||||
|
* 404:
|
||||||
|
* description: Admin or invited user not found
|
||||||
|
* 417:
|
||||||
|
* description: Validation error
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.post('/add', verifyJWT, isAdmin, projectController.createProject);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /project-api/get:
|
||||||
|
* get:
|
||||||
|
* summary: Get all projects
|
||||||
|
* description: Admin can see all projects. Intern can only see projects where they are active members.
|
||||||
|
* tags: [Project]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: status
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* enum: [active, completed, archived]
|
||||||
|
* required: false
|
||||||
|
* description: Filter projects by status
|
||||||
|
* example: active
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Projects retrieved successfully
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 417:
|
||||||
|
* description: Validation error
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.get('/get', verifyJWT, projectController.getAllProjects);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /project-api/get/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: Get project detail
|
||||||
|
* description: Admin can access any project. Intern can only access project where they are active members.
|
||||||
|
* tags: [Project]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: Project ID
|
||||||
|
* example: 1
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Project detail retrieved successfully
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden
|
||||||
|
* 404:
|
||||||
|
* description: Project not found
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.get('/get/:id', verifyJWT, projectController.getProjectById);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /project-api/update/{id}:
|
||||||
|
* patch:
|
||||||
|
* summary: Update project
|
||||||
|
* description: Update project information. Only admin can access this endpoint.
|
||||||
|
* tags: [Project]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: Project ID
|
||||||
|
* example: 1
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/ProjectUpdateRequest'
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Project updated successfully
|
||||||
|
* 400:
|
||||||
|
* description: Bad request
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden
|
||||||
|
* 404:
|
||||||
|
* description: Project not found
|
||||||
|
* 417:
|
||||||
|
* description: Validation error
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.patch('/update/:id', verifyJWT, isAdmin, projectController.updateProject);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /project-api/delete/{id}:
|
||||||
|
* delete:
|
||||||
|
* summary: Archive project
|
||||||
|
* description: Archive project by changing its status to archived. This endpoint does not hard delete the project.
|
||||||
|
* tags: [Project]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: Project ID
|
||||||
|
* example: 1
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Project archived successfully
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden
|
||||||
|
* 404:
|
||||||
|
* description: Project not found
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.delete('/delete/:id', verifyJWT, isAdmin, projectController.archiveProject);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user