refactor: nambahin role mentor di admin account dan ngeremove max_member di project

This commit is contained in:
Rafi Athallah
2026-06-14 20:26:53 +07:00
parent edc287968d
commit 94bbc18224
10 changed files with 937 additions and 381 deletions
+11
View File
@@ -0,0 +1,11 @@
const { UnauthorizedError } = require('../helpers/error')
const { response } = require('../helpers/utils/wrapper');
const { ERROR } = require('../helpers/http-status/status_code');
module.exports = function isMentorOrAdmin(req, res, next) {
if (req.role && (req.role === 'admin' || req.role === 'mentor')) {
return next();
}
return response(res, 'fail', { err: new UnauthorizedError('Akses ditolak: Hanya admin atau mentor yang dapat mengakses ini') }, 'Akses ditolak: Hanya admin atau mentor yang dapat mengakses ini', ERROR.UNAUTHORIZED);
}
+2 -2
View File
@@ -30,9 +30,9 @@ const createAdminModel = joi.object().keys({
'any.required': 'Password wajib diisi.',
}),
role: joi.string().valid('admin').default('admin').messages({
role: joi.string().valid('admin', 'mentor').default('mentor').messages({
'string.base': 'Role harus berupa teks.',
'any.only': 'Role hanya boleh "admin".',
'any.only': 'Role hanya boleh "admin" atau "mentor".',
}),
signature: joi.string().optional().allow('', null).messages({
+5 -5
View File
@@ -86,7 +86,7 @@ class AuthService {
async getCurrentUser(userId, role) {
try {
if (role === 'admin') {
if (role === 'admin' || role === 'mentor') {
const result = await adminService.getAdminById(userId);
if (result.err) {
@@ -97,7 +97,7 @@ class AuthService {
return data({
...adminWithoutPassword,
role: 'admin'
role: adminWithoutPassword.role
});
} else if (role === 'intern') {
const user = await prisma.user.findUnique({
@@ -151,7 +151,7 @@ class AuthService {
const existingAdmin = await prisma.admin.findUnique({
where: { email }
});
if (existingAdmin && (role !== 'admin' || existingAdmin.id !== parseInt(userId))) {
if (existingAdmin && ((role !== 'admin' && role !== 'mentor') || existingAdmin.id !== parseInt(userId))) {
if (imageFile?.filename) {
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
await deleteFileIfExists(imageFile.filename).catch(() => { });
@@ -176,7 +176,7 @@ class AuthService {
hashedPassword = await bcrypt.hash(password, 10);
}
if (role === 'admin') {
if (role === 'admin' || role === 'mentor') {
const adminId = parseInt(userId);
const existingAdmin = await prisma.admin.findUnique({
where: { id: adminId }
@@ -220,7 +220,7 @@ class AuthService {
const { password: _, ...adminWithoutPassword } = updatedAdmin;
return data({
...adminWithoutPassword,
role: 'admin'
role: adminWithoutPassword.role
});
} else if (role === 'intern') {
@@ -1,347 +1,417 @@
const lamaranMagangRepository = require('../repositories/lamaranMagang.repository');
const mahasiswaRepository = require('../../mahasiswa/repositories/mahasiswa.repository');
const lowonganMagangRepository = require('../../lowonganMagang/repositories/lowonganMagang.repository');
const batchRepository = require('../../batch/repositories/batch.repository');
const bcrypt = require('bcrypt');
const prisma = require('../../../helpers/db/db_connection');
const crypto = require('crypto');
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
const { sendConfirmationEmail, sendStatusEmail } = require('../helpers/email.helper');
const ExcelJS = require('exceljs');
const path = require('path');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const { data, error, paginationData } = require('../../../helpers/utils/wrapper');
const { NotFoundError, ConflictError } = require('../../../helpers/error');
class LamaranMagangService {
async createLamaranMagang(id_lowongan_magang, mahasiswaFormData, uploadedFiles) {
try {
const lowonganMagang = await lowonganMagangRepository.findById(id_lowongan_magang);
if (!lowonganMagang) {
return error(new NotFoundError('Lowongan Magang Tidak Ditemukan'));
}
const batchId = lowonganMagang.batch?.id || null;
const existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaFormData.email);
if (existingMahasiswa) {
const existingLamaran = await lamaranMagangRepository.findAnyLamaranByMahasiswa(
existingMahasiswa.id
);
if (existingLamaran) {
const posisiYangSudahDilamar = existingLamaran.lowongan_magang?.posisi || 'posisi tertentu';
return error(new ConflictError(
`Anda sudah pernah melamar untuk posisi ${posisiYangSudahDilamar}. Setiap pengguna hanya dapat melamar satu posisi.`
));
}
const lamaranData = {
id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
};
await lamaranMagangRepository.create(lamaranData);
return data({
id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
});
}
const cv_path = imageUploadHelper.uploadCV(uploadedFiles.cv);
const portofolio_path = imageUploadHelper.uploadPortofolio(uploadedFiles.portofolio);
const namaBelakangFinal = mahasiswaFormData.nama_belakang?.trim() || null;
const mahasiswaData = {
nama_depan: mahasiswaFormData.nama_depan,
nama_belakang: namaBelakangFinal,
email: mahasiswaFormData.email,
kontak: mahasiswaFormData.kontak,
jurusan: mahasiswaFormData.jurusan,
universitas: mahasiswaFormData.universitas,
negara: mahasiswaFormData.negara,
motivasi: mahasiswaFormData.motivasi,
relevant_skills: mahasiswaFormData.relevant_skills,
role: 'student',
cv_path,
portofolio_path,
};
const createdMahasiswa = await mahasiswaRepository.create(mahasiswaData);
const id_mahasiswa = createdMahasiswa.id;
const lamaranData = {
id_mahasiswa,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
};
await lamaranMagangRepository.create(lamaranData);
await sendConfirmationEmail(mahasiswaData, lowonganMagang);
return data({
id_mahasiswa,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
});
} catch (err) {
return error(err);
}
}
async getAllLamaranMagang(query = {}) {
try {
const { page = 1, limit = 10, posisi, status, universitas, id_lowongan } = query;
const offset = (page - 1) * limit;
const lamaranList = await lamaranMagangRepository.findAll({ offset, limit, status, posisi, universitas, id_lowongan });
const total = await lamaranMagangRepository.countAllLamaran({ status, posisi, universitas, id_lowongan });
const totalPages = Math.ceil(total / limit);
const meta = { page: Number(page), limit: Number(limit), total, totalPages };
return paginationData(lamaranList, meta);
} catch (err) {
return error(err);
}
}
async getLamaranById(id_lamaran_magang) {
try {
const lamaran = await lamaranMagangRepository.findById(id_lamaran_magang);
if (!lamaran) {
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan'));
}
return data(lamaran);
} catch (err) {
return error(err);
}
}
async getDetailLamaranById(id_lamaran) {
try {
const lamaran = await lamaranMagangRepository.findDetailById(id_lamaran);
if (!lamaran) {
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan'));
}
return data(lamaran);
} catch (err) {
return error(err);
}
}
async updateLamaranStatus(id_lamaran_magang, status) {
try {
const checkResult = await this.getDetailLamaranById(id_lamaran_magang);
if (checkResult.err) {
return checkResult;
}
const result = await lamaranMagangRepository.updateStatus(id_lamaran_magang, status);
let generatedPassword = null;
if (status === 'diterima') {
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ id_mahasiswa: checkResult.data.id_mahasiswa },
{ id_lamaran_magang: parseInt(id_lamaran_magang) },
{ email: checkResult.data.mahasiswa.email }
]
}
});
// Akan dibuat akunnya ketika sudah ACC ama mentor dengan password yang random [Rafi 08/06/2026 16:05]
if (!existingUser) {
generatedPassword = crypto.randomBytes(4).toString('hex');
const hashedPassword = await bcrypt.hash(generatedPassword, 10);
const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? ' ' + checkResult.data.mahasiswa.nama_belakang : ''}`;
await prisma.user.create({
data: {
id_mahasiswa: checkResult.data.id_mahasiswa,
id_lamaran_magang: parseInt(id_lamaran_magang),
full_name: fullName,
email: checkResult.data.mahasiswa.email,
password: hashedPassword,
role: 'intern',
is_active: true
}
});
}
}
await sendStatusEmail(checkResult.data, status, generatedPassword);
return data(result);
} catch (err) {
return error(err);
}
}
async deleteLamaran(id) {
try {
const checkResult = await this.getDetailLamaranById(id);
if (checkResult.err) {
return checkResult;
}
const lamaran = checkResult.data;
if (lamaran.cv_path) {
const cvFileName = path.basename(lamaran.cv_path);
await deleteFileIfExists(cvFileName);
}
if (lamaran.portofolio_path) {
const portofolioFileName = path.basename(lamaran.portofolio_path);
await deleteFileIfExists(portofolioFileName);
}
const result = await lamaranMagangRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
async exportToExcel() {
try {
const lamaranList = await lamaranMagangRepository.findAll({});
if (!lamaranList || lamaranList.length === 0) {
return error(new NotFoundError("Tidak ada data lamaran untuk diekspor"));
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Lamaran Magang');
worksheet.columns = [
{ header: 'ID', key: 'id', width: 10 },
{ header: 'Nama Depan', key: 'nama_depan', width: 20 },
{ header: 'Nama Belakang', key: 'nama_belakang', width: 20 },
{ header: 'Posisi', key: 'posisi', width: 25 },
{ header: 'Kelompok Peminatan', key: 'kelompok_peminatan', width: 25 },
{ header: 'Status', key: 'status', width: 15 },
{ header: 'Tanggal Dibuat', key: 'created_at', width: 20 },
];
lamaranList.forEach((lamaran) => {
worksheet.addRow({
id: lamaran.id,
nama_depan: lamaran.mahasiswa?.nama_depan || '',
nama_belakang: lamaran.mahasiswa?.nama_belakang || '',
posisi: lamaran.lowongan_magang?.posisi || '',
kelompok_peminatan: lamaran.lowongan_magang?.kelompok_peminatan || '',
status: lamaran.status,
created_at: lamaran.created_at,
});
});
worksheet.getRow(1).font = { bold: true };
worksheet.getRow(1).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFD3D3D3' },
};
return data(workbook);
} catch (err) {
return error(err);
}
}
async getDashboardStatistics(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'Data Seluruh Batch';
const stats = await lamaranMagangRepository.getStatistics({ query });
const acceptanceRate = stats.total > 0 ? ((stats.diterima / stats.total) * 100).toFixed(1) : 0;
const rejectionRate = stats.total > 0 ? ((stats.ditolak / stats.total) * 100).toFixed(1) : 0;
const pendingRate = stats.total > 0 ? ((stats.diproses / stats.total) * 100).toFixed(1) : 0;
if (stats.total === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`));
}
return data({
total_pendaftar: stats.total,
total_diterima: stats.diterima,
total_ditolak: stats.ditolak,
sedang_diproses: stats.diproses,
presentase_diterima: `${acceptanceRate}%`,
presentase_ditolak: `${rejectionRate}%`,
presentase_diproses: `${pendingRate}%`,
batch: batchDisplay,
});
} catch (err) {
return error(err);
}
}
async getStatisticsByPosition(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch';
const positions = await lamaranMagangRepository.getStatisticsByPosition({ query });
if (positions.length === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`));
}
return data(positions);
} catch (err) {
return error(err);
}
}
async getStatisticsByCountry(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch';
const countries = await lamaranMagangRepository.getStatisticsByCountry({ query });
if (countries.length === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`));
}
return data(countries);
} catch (err) {
return error(err);
}
}
async getStatisticsByUniversity(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch';
const universities = await lamaranMagangRepository.getStatisticsByUniversity({ query });
if (universities.length === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`));
}
return data(universities);
} catch (err) {
return error(err);
}
}
}
module.exports = new LamaranMagangService();
const lamaranMagangRepository = require("../repositories/lamaranMagang.repository");
const mahasiswaRepository = require("../../mahasiswa/repositories/mahasiswa.repository");
const lowonganMagangRepository = require("../../lowonganMagang/repositories/lowonganMagang.repository");
const batchRepository = require("../../batch/repositories/batch.repository");
const bcrypt = require("bcrypt");
const prisma = require("../../../helpers/db/db_connection");
const crypto = require("crypto");
const imageUploadHelper = require("../../../helpers/utils/imageUpload.helper");
const {
sendConfirmationEmail,
sendStatusEmail,
} = require("../helpers/email.helper");
const ExcelJS = require("exceljs");
const path = require("path");
const { deleteFileIfExists } = require("../../../helpers/utils/fileHelper");
const {
data,
error,
paginationData,
} = require("../../../helpers/utils/wrapper");
const { NotFoundError, ConflictError } = require("../../../helpers/error");
class LamaranMagangService {
async createLamaranMagang(
id_lowongan_magang,
mahasiswaFormData,
uploadedFiles,
) {
try {
const lowonganMagang =
await lowonganMagangRepository.findById(id_lowongan_magang);
if (!lowonganMagang) {
return error(new NotFoundError("Lowongan Magang Tidak Ditemukan"));
}
const batchId = lowonganMagang.batch?.id || null;
const existingMahasiswa = await mahasiswaRepository.findByEmail(
mahasiswaFormData.email,
);
if (existingMahasiswa) {
const existingLamaran =
await lamaranMagangRepository.findAnyLamaranByMahasiswa(
existingMahasiswa.id,
);
if (existingLamaran) {
const posisiYangSudahDilamar =
existingLamaran.lowongan_magang?.posisi || "posisi tertentu";
return error(
new ConflictError(
`Anda sudah pernah melamar untuk posisi ${posisiYangSudahDilamar}. Setiap pengguna hanya dapat melamar satu posisi.`,
),
);
}
const lamaranData = {
id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang,
batch: batchId,
status: "diproses",
};
await lamaranMagangRepository.create(lamaranData);
return data({
id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang,
batch: batchId,
status: "diproses",
});
}
const cv_path = imageUploadHelper.uploadCV(uploadedFiles.cv);
const portofolio_path = imageUploadHelper.uploadPortofolio(
uploadedFiles.portofolio,
);
const namaBelakangFinal = mahasiswaFormData.nama_belakang?.trim() || null;
const mahasiswaData = {
nama_depan: mahasiswaFormData.nama_depan,
nama_belakang: namaBelakangFinal,
email: mahasiswaFormData.email,
kontak: mahasiswaFormData.kontak,
jurusan: mahasiswaFormData.jurusan,
universitas: mahasiswaFormData.universitas,
negara: mahasiswaFormData.negara,
motivasi: mahasiswaFormData.motivasi,
relevant_skills: mahasiswaFormData.relevant_skills,
role: "student",
cv_path,
portofolio_path,
};
const createdMahasiswa = await mahasiswaRepository.create(mahasiswaData);
const id_mahasiswa = createdMahasiswa.id;
const lamaranData = {
id_mahasiswa,
id_lowongan_magang,
batch: batchId,
status: "diproses",
};
await lamaranMagangRepository.create(lamaranData);
await sendConfirmationEmail(mahasiswaData, lowonganMagang);
return data({
id_mahasiswa,
id_lowongan_magang,
batch: batchId,
status: "diproses",
});
} catch (err) {
return error(err);
}
}
async getAllLamaranMagang(query = {}) {
try {
const {
page = 1,
limit = 10,
posisi,
status,
universitas,
id_lowongan,
} = query;
const offset = (page - 1) * limit;
const lamaranList = await lamaranMagangRepository.findAll({
offset,
limit,
status,
posisi,
universitas,
id_lowongan,
});
const total = await lamaranMagangRepository.countAllLamaran({
status,
posisi,
universitas,
id_lowongan,
});
const totalPages = Math.ceil(total / limit);
const meta = {
page: Number(page),
limit: Number(limit),
total,
totalPages,
};
return paginationData(lamaranList, meta);
} catch (err) {
return error(err);
}
}
async getLamaranById(id_lamaran_magang) {
try {
const lamaran = await lamaranMagangRepository.findById(id_lamaran_magang);
if (!lamaran) {
return error(new NotFoundError("Lamaran Magang Tidak Ditemukan"));
}
return data(lamaran);
} catch (err) {
return error(err);
}
}
async getDetailLamaranById(id_lamaran) {
try {
const lamaran = await lamaranMagangRepository.findDetailById(id_lamaran);
if (!lamaran) {
return error(new NotFoundError("Lamaran Magang Tidak Ditemukan"));
}
return data(lamaran);
} catch (err) {
return error(err);
}
}
async updateLamaranStatus(id_lamaran_magang, status) {
try {
const checkResult = await this.getDetailLamaranById(id_lamaran_magang);
if (checkResult.err) {
return checkResult;
}
const result = await lamaranMagangRepository.updateStatus(
id_lamaran_magang,
status,
);
let generatedPassword = null;
if (status === "diterima") {
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ id_mahasiswa: checkResult.data.id_mahasiswa },
{ id_lamaran_magang: parseInt(id_lamaran_magang) },
{ email: checkResult.data.mahasiswa.email },
],
},
});
// Akan dibuat akunnya ketika sudah ACC ama mentor dengan password yang random [Rafi 08/06/2026 16:05]
if (!existingUser) {
generatedPassword = crypto.randomBytes(4).toString("hex");
const hashedPassword = await bcrypt.hash(generatedPassword, 10);
const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? " " + checkResult.data.mahasiswa.nama_belakang : ""}`;
await prisma.user.create({
data: {
id_mahasiswa: checkResult.data.id_mahasiswa,
id_lamaran_magang: parseInt(id_lamaran_magang),
full_name: fullName,
email: checkResult.data.mahasiswa.email,
password: hashedPassword,
role: "intern",
is_active: true,
},
});
}
}
await sendStatusEmail(checkResult.data, status, generatedPassword);
return data(result);
} catch (err) {
return error(err);
}
}
async deleteLamaran(id) {
try {
const checkResult = await this.getDetailLamaranById(id);
if (checkResult.err) {
return checkResult;
}
const lamaran = checkResult.data;
if (lamaran.cv_path) {
const cvFileName = path.basename(lamaran.cv_path);
await deleteFileIfExists(cvFileName);
}
if (lamaran.portofolio_path) {
const portofolioFileName = path.basename(lamaran.portofolio_path);
await deleteFileIfExists(portofolioFileName);
}
const result = await lamaranMagangRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
async exportToExcel() {
try {
const lamaranList = await lamaranMagangRepository.findAll({});
if (!lamaranList || lamaranList.length === 0) {
return error(
new NotFoundError("Tidak ada data lamaran untuk diekspor"),
);
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Lamaran Magang");
worksheet.columns = [
{ header: "ID", key: "id", width: 10 },
{ header: "Nama Depan", key: "nama_depan", width: 20 },
{ header: "Nama Belakang", key: "nama_belakang", width: 20 },
{ header: "Posisi", key: "posisi", width: 25 },
{ header: "Kelompok Peminatan", key: "kelompok_peminatan", width: 25 },
{ header: "Status", key: "status", width: 15 },
{ header: "Tanggal Dibuat", key: "created_at", width: 20 },
];
lamaranList.forEach((lamaran) => {
worksheet.addRow({
id: lamaran.id,
nama_depan: lamaran.mahasiswa?.nama_depan || "",
nama_belakang: lamaran.mahasiswa?.nama_belakang || "",
posisi: lamaran.lowongan_magang?.posisi || "",
kelompok_peminatan: lamaran.lowongan_magang?.kelompok_peminatan || "",
status: lamaran.status,
created_at: lamaran.created_at,
});
});
worksheet.getRow(1).font = { bold: true };
worksheet.getRow(1).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFD3D3D3" },
};
return data(workbook);
} catch (err) {
return error(err);
}
}
async getDashboardStatistics(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : "Data Seluruh Batch";
const stats = await lamaranMagangRepository.getStatistics({ query });
const acceptanceRate =
stats.total > 0 ? ((stats.diterima / stats.total) * 100).toFixed(1) : 0;
const rejectionRate =
stats.total > 0 ? ((stats.ditolak / stats.total) * 100).toFixed(1) : 0;
const pendingRate =
stats.total > 0 ? ((stats.diproses / stats.total) * 100).toFixed(1) : 0;
if (stats.total === 0) {
return error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
}
return data({
total_pendaftar: stats.total,
total_diterima: stats.diterima,
total_ditolak: stats.ditolak,
sedang_diproses: stats.diproses,
presentase_diterima: `${acceptanceRate}%`,
presentase_ditolak: `${rejectionRate}%`,
presentase_diproses: `${pendingRate}%`,
batch: batchDisplay,
});
} catch (err) {
return error(err);
}
}
async getStatisticsByPosition(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : "semua batch";
const positions = await lamaranMagangRepository.getStatisticsByPosition({
query,
});
if (positions.length === 0) {
return error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
}
return data(positions);
} catch (err) {
return error(err);
}
}
async getStatisticsByCountry(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : "semua batch";
const countries = await lamaranMagangRepository.getStatisticsByCountry({
query,
});
if (countries.length === 0) {
return error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
}
return data(countries);
} catch (err) {
return error(err);
}
}
async getStatisticsByUniversity(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : "semua batch";
const universities =
await lamaranMagangRepository.getStatisticsByUniversity({ query });
if (universities.length === 0) {
return error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
}
return data(universities);
} catch (err) {
return error(err);
}
}
}
module.exports = new LamaranMagangService();
@@ -74,6 +74,10 @@ class ProjectRepository {
};
}
if (actor?.role === "mentor") {
where.id_admin = parseInt(actor.id);
}
return prisma.project.findMany({
where,
orderBy: {
+19 -14
View File
@@ -14,9 +14,9 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only admin can create project"),
new ForbiddenError("Access denied: only admin or mentor can create project"),
);
}
@@ -65,7 +65,6 @@ class ProjectService {
description: projectData.description || "",
start_date: projectData.start_date,
end_date: projectData.end_date,
max_members: 8,
status: "active",
};
@@ -95,7 +94,6 @@ class ProjectService {
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,
@@ -135,9 +133,9 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only admin can update project"),
new ForbiddenError("Access denied: only admin or mentor can update project"),
);
}
@@ -188,9 +186,9 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only admin can archive project"),
new ForbiddenError("Access denied: only admin or mentor can archive project"),
);
}
@@ -223,6 +221,16 @@ class ProjectService {
return null;
}
if (actor.role === "mentor") {
const project = await projectRepository.findById(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 projectRepository.checkActiveMember(
idProject,
@@ -259,7 +267,6 @@ class ProjectService {
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:
@@ -328,7 +335,6 @@ class ProjectService {
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,
@@ -393,7 +399,7 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only mentors/admins can view their created projects"),
);
@@ -408,7 +414,6 @@ class ProjectService {
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,
@@ -429,7 +434,7 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only admin/mentor can access interns list"),
);
@@ -465,7 +470,7 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only mentors/admins can assign members"),
);
+353 -9
View File
@@ -2,6 +2,7 @@ const express = require('express');
const { projectController } = require('../modules/project');
const { verifyJWT } = require('../middleware/verifyJWT');
const isAdmin = require('../middleware/isAdmin');
const isMentorOrAdmin = require('../middleware/isMentorOrAdmin');
const router = express.Router();
@@ -33,9 +34,6 @@ const router = express.Router();
* type: string
* format: date
* example: 2026-08-10
* max_members:
* type: integer
* example: 8
* status:
* type: string
* enum: [active, completed, archived]
@@ -103,6 +101,352 @@ const router = express.Router();
* type: string
* enum: [active, completed, archived]
* example: completed
* ProjectAdminSummary:
* type: object
* properties:
* id:
* type: integer
* example: 1
* full_name:
* type: string
* example: Admin One
* email:
* type: string
* example: admin1@internify.com
* profile_picture:
* type: string
* nullable: true
* example: /uploads/admin-profile.png
* professional_bio:
* type: string
* nullable: true
* example: Senior Program Manager at Internify.
* ProjectListItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* project_icon:
* type: string
* example: code
* project_name:
* type: string
* example: Internify Platform Dev
* description:
* type: string
* example: Upgrading the core Internify platform backend and web client.
* start_date:
* type: string
* format: date-time
* example: 2026-07-15T00:00:00.000Z
* end_date:
* type: string
* format: date-time
* example: 2026-10-15T00:00:00.000Z
* status:
* type: string
* example: active
* admin:
* $ref: '#/components/schemas/ProjectAdminSummary'
* total_members:
* type: integer
* example: 5
* total_tasks:
* type: integer
* example: 3
* created_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* ProjectMemberUser:
* type: object
* properties:
* id:
* type: integer
* example: 1
* full_name:
* type: string
* example: Rafi Athallah
* email:
* type: string
* example: rafi@student.com
* professional_bio:
* type: string
* nullable: true
* example: Backend developer intern
* position:
* type: string
* nullable: true
* example: Web Developer
* kelompok_peminatan:
* type: string
* nullable: true
* example: Software Engineering
* ProjectMemberItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* id_user:
* type: integer
* example: 1
* status:
* type: string
* example: active
* user:
* $ref: '#/components/schemas/ProjectMemberUser'
* created_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* ProjectTaskItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* title:
* type: string
* example: Database Setup
* description:
* type: string
* example: Setup database schema and write initial seed script.
* deadline_at:
* type: string
* format: date-time
* example: 2026-08-01T00:00:00.000Z
* submission_type:
* type: string
* enum: [file_upload, url_link]
* example: url_link
* created_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* ProjectDetail:
* allOf:
* - $ref: '#/components/schemas/ProjectListItem'
* - type: object
* properties:
* members:
* type: array
* items:
* $ref: '#/components/schemas/ProjectMemberItem'
* tasks:
* type: array
* items:
* $ref: '#/components/schemas/ProjectTaskItem'
* ProjectListResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/ProjectListItem'
* message:
* type: string
* example: Projects retrieved successfully
* code:
* type: integer
* example: 200
* ProjectDetailResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* $ref: '#/components/schemas/ProjectDetail'
* message:
* type: string
* example: Project detail retrieved successfully
* code:
* type: integer
* example: 200
* InternListItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* name:
* type: string
* example: Rafi Athallah
* email:
* type: string
* example: rafi@student.com
* projectName:
* type: string
* example: Internify Platform Dev
* projectId:
* type: integer
* nullable: true
* example: 1
* role:
* type: string
* example: Web Developer
* isAssignedByMentor:
* type: boolean
* example: true
* avatar:
* type: string
* nullable: true
* example: null
* InternListResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/InternListItem'
* message:
* type: string
* example: Interns retrieved successfully
* code:
* type: integer
* example: 200
* MyTaskItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* id_project:
* type: integer
* example: 1
* project_name:
* type: string
* example: Internify Platform Dev
* project_icon:
* type: string
* example: code
* title:
* type: string
* example: Database Setup
* description:
* type: string
* example: Setup database schema and write initial seed script.
* deadline_at:
* type: string
* format: date-time
* example: 2026-08-01T00:00:00.000Z
* submission_type:
* type: string
* enum: [file_upload, url_link]
* example: url_link
* submission_status:
* type: string
* enum: [submitted, not_submitted]
* example: submitted
* submission_details:
* type: object
* nullable: true
* properties:
* id:
* type: integer
* example: 1
* file_path:
* type: string
* nullable: true
* example: null
* url_link:
* type: string
* nullable: true
* example: https://github.com/rafiathallah3
* submitted_at:
* type: string
* format: date-time
* example: 2026-07-20T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-07-20T10:00:00.000Z
* MyTaskListResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/MyTaskItem'
* message:
* type: string
* example: Intern tasks retrieved successfully
* code:
* type: integer
* example: 200
* AssignMemberResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* type: object
* properties:
* id:
* type: integer
* example: 1
* id_project:
* type: integer
* example: 1
* id_user:
* type: integer
* example: 2
* status:
* type: string
* example: active
* created_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* message:
* type: string
* example: Member assigned successfully
* code:
* type: integer
* example: 200
* ErrorResponse:
* type: object
* properties:
* status:
* type: boolean
* example: false
* data:
* nullable: true
* example: null
* message:
* type: string
* example: Validation error
* code:
* type: integer
* example: 417
*/
/**
@@ -136,7 +480,7 @@ const router = express.Router();
* 500:
* description: Internal server error
*/
router.post('/add', verifyJWT, isAdmin, projectController.createProject);
router.post('/add', verifyJWT, isMentorOrAdmin, projectController.createProject);
/**
* @swagger
@@ -238,7 +582,7 @@ router.get('/get/:id', verifyJWT, projectController.getProjectById);
* 500:
* description: Internal server error
*/
router.patch('/update/:id', verifyJWT, isAdmin, projectController.updateProject);
router.patch('/update/:id', verifyJWT, isMentorOrAdmin, projectController.updateProject);
/**
* @swagger
@@ -269,7 +613,7 @@ router.patch('/update/:id', verifyJWT, isAdmin, projectController.updateProject)
* 500:
* description: Internal server error
*/
router.delete('/delete/:id', verifyJWT, isAdmin, projectController.archiveProject);
router.delete('/delete/:id', verifyJWT, isMentorOrAdmin, projectController.archiveProject);
/**
* @swagger
@@ -332,7 +676,7 @@ router.get('/my-tasks', verifyJWT, projectController.getMyTasks);
* 500:
* description: Internal server error
*/
router.get('/mentor-projects', verifyJWT, isAdmin, projectController.getMentorProjects);
router.get('/mentor-projects', verifyJWT, isMentorOrAdmin, projectController.getMentorProjects);
/**
* @swagger
@@ -391,7 +735,7 @@ router.get('/mentor-projects', verifyJWT, isAdmin, projectController.getMentorPr
* 500:
* description: Internal server error
*/
router.get('/interns', verifyJWT, isAdmin, projectController.getInterns);
router.get('/interns', verifyJWT, isMentorOrAdmin, projectController.getInterns);
/**
* @swagger
@@ -434,6 +778,6 @@ router.get('/interns', verifyJWT, isAdmin, projectController.getInterns);
* 500:
* description: Internal server error
*/
router.post('/assign-member', verifyJWT, isAdmin, projectController.assignMember);
router.post('/assign-member', verifyJWT, isMentorOrAdmin, projectController.assignMember);
module.exports = router;