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
+1 -1
View File
@@ -207,7 +207,6 @@ model Project {
description String @db.Text description String @db.Text
start_date DateTime @db.Date start_date DateTime @db.Date
end_date DateTime @db.Date end_date DateTime @db.Date
max_members Int @default(8)
status ProjectStatus @default(active) status ProjectStatus @default(active)
created_at DateTime @default(now()) created_at DateTime @default(now())
updated_at DateTime @updatedAt updated_at DateTime @updatedAt
@@ -277,6 +276,7 @@ model TaskSubmission {
enum Role { enum Role {
admin admin
mentor
intern intern
} }
+3 -3
View File
@@ -151,13 +151,13 @@ async function main() {
// 10. Project // 10. Project
const proj1 = await prisma.project.create({ const proj1 = await prisma.project.create({
data: { id_admin: admin1.id, project_icon: 'code', project_name: 'Internify Platform Dev', description: 'Upgrading the core Internify platform backend and web client.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), max_members: 5, status: 'active' } data: { id_admin: admin1.id, project_icon: 'code', project_name: 'Internify Platform Dev', description: 'Upgrading the core Internify platform backend and web client.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), status: 'active' }
}); });
const proj2 = await prisma.project.create({ const proj2 = await prisma.project.create({
data: { id_admin: admin2.id, project_icon: 'shield', project_name: 'Auth Gateway Module', description: 'Securing API gateway with unified OAuth2 authentication.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), max_members: 3, status: 'active' } data: { id_admin: admin2.id, project_icon: 'shield', project_name: 'Auth Gateway Module', description: 'Securing API gateway with unified OAuth2 authentication.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), status: 'active' }
}); });
const proj3 = await prisma.project.create({ const proj3 = await prisma.project.create({
data: { id_admin: admin1.id, project_icon: 'chart', project_name: 'Recruitment Analytics Dashboard', description: 'Developing analytical charts for recruitment metrics.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), max_members: 4, status: 'active' } data: { id_admin: admin1.id, project_icon: 'chart', project_name: 'Recruitment Analytics Dashboard', description: 'Developing analytical charts for recruitment metrics.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), status: 'active' }
}); });
// 11. LamaranMagang // 11. LamaranMagang
+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.', '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.', '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({ signature: joi.string().optional().allow('', null).messages({
+5 -5
View File
@@ -86,7 +86,7 @@ class AuthService {
async getCurrentUser(userId, role) { async getCurrentUser(userId, role) {
try { try {
if (role === 'admin') { if (role === 'admin' || role === 'mentor') {
const result = await adminService.getAdminById(userId); const result = await adminService.getAdminById(userId);
if (result.err) { if (result.err) {
@@ -97,7 +97,7 @@ class AuthService {
return data({ return data({
...adminWithoutPassword, ...adminWithoutPassword,
role: 'admin' role: adminWithoutPassword.role
}); });
} else if (role === 'intern') { } else if (role === 'intern') {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
@@ -151,7 +151,7 @@ class AuthService {
const existingAdmin = await prisma.admin.findUnique({ const existingAdmin = await prisma.admin.findUnique({
where: { email } where: { email }
}); });
if (existingAdmin && (role !== 'admin' || existingAdmin.id !== parseInt(userId))) { if (existingAdmin && ((role !== 'admin' && role !== 'mentor') || existingAdmin.id !== parseInt(userId))) {
if (imageFile?.filename) { if (imageFile?.filename) {
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper'); const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
await deleteFileIfExists(imageFile.filename).catch(() => { }); await deleteFileIfExists(imageFile.filename).catch(() => { });
@@ -176,7 +176,7 @@ class AuthService {
hashedPassword = await bcrypt.hash(password, 10); hashedPassword = await bcrypt.hash(password, 10);
} }
if (role === 'admin') { if (role === 'admin' || role === 'mentor') {
const adminId = parseInt(userId); const adminId = parseInt(userId);
const existingAdmin = await prisma.admin.findUnique({ const existingAdmin = await prisma.admin.findUnique({
where: { id: adminId } where: { id: adminId }
@@ -220,7 +220,7 @@ class AuthService {
const { password: _, ...adminWithoutPassword } = updatedAdmin; const { password: _, ...adminWithoutPassword } = updatedAdmin;
return data({ return data({
...adminWithoutPassword, ...adminWithoutPassword,
role: 'admin' role: adminWithoutPassword.role
}); });
} else if (role === 'intern') { } else if (role === 'intern') {
@@ -1,347 +1,417 @@
const lamaranMagangRepository = require('../repositories/lamaranMagang.repository'); const lamaranMagangRepository = require("../repositories/lamaranMagang.repository");
const mahasiswaRepository = require('../../mahasiswa/repositories/mahasiswa.repository'); const mahasiswaRepository = require("../../mahasiswa/repositories/mahasiswa.repository");
const lowonganMagangRepository = require('../../lowonganMagang/repositories/lowonganMagang.repository'); const lowonganMagangRepository = require("../../lowonganMagang/repositories/lowonganMagang.repository");
const batchRepository = require('../../batch/repositories/batch.repository'); const batchRepository = require("../../batch/repositories/batch.repository");
const bcrypt = require('bcrypt'); const bcrypt = require("bcrypt");
const prisma = require('../../../helpers/db/db_connection'); const prisma = require("../../../helpers/db/db_connection");
const crypto = require('crypto'); const crypto = require("crypto");
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper'); const imageUploadHelper = require("../../../helpers/utils/imageUpload.helper");
const { sendConfirmationEmail, sendStatusEmail } = require('../helpers/email.helper'); const {
const ExcelJS = require('exceljs'); sendConfirmationEmail,
const path = require('path'); sendStatusEmail,
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper'); } = require("../helpers/email.helper");
const { data, error, paginationData } = require('../../../helpers/utils/wrapper'); const ExcelJS = require("exceljs");
const { NotFoundError, ConflictError } = require('../../../helpers/error'); const path = require("path");
const { deleteFileIfExists } = require("../../../helpers/utils/fileHelper");
class LamaranMagangService { const {
async createLamaranMagang(id_lowongan_magang, mahasiswaFormData, uploadedFiles) { data,
try { error,
const lowonganMagang = await lowonganMagangRepository.findById(id_lowongan_magang); paginationData,
} = require("../../../helpers/utils/wrapper");
if (!lowonganMagang) { const { NotFoundError, ConflictError } = require("../../../helpers/error");
return error(new NotFoundError('Lowongan Magang Tidak Ditemukan'));
} class LamaranMagangService {
async createLamaranMagang(
const batchId = lowonganMagang.batch?.id || null; id_lowongan_magang,
mahasiswaFormData,
const existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaFormData.email); uploadedFiles,
) {
if (existingMahasiswa) { try {
const lowonganMagang =
const existingLamaran = await lamaranMagangRepository.findAnyLamaranByMahasiswa( await lowonganMagangRepository.findById(id_lowongan_magang);
existingMahasiswa.id
); if (!lowonganMagang) {
return error(new NotFoundError("Lowongan Magang Tidak Ditemukan"));
if (existingLamaran) { }
const posisiYangSudahDilamar = existingLamaran.lowongan_magang?.posisi || 'posisi tertentu';
return error(new ConflictError( const batchId = lowonganMagang.batch?.id || null;
`Anda sudah pernah melamar untuk posisi ${posisiYangSudahDilamar}. Setiap pengguna hanya dapat melamar satu posisi.`
)); const existingMahasiswa = await mahasiswaRepository.findByEmail(
} mahasiswaFormData.email,
);
const lamaranData = {
id_mahasiswa: existingMahasiswa.id, if (existingMahasiswa) {
id_lowongan_magang, const existingLamaran =
batch: batchId, await lamaranMagangRepository.findAnyLamaranByMahasiswa(
status: 'diproses', existingMahasiswa.id,
}; );
await lamaranMagangRepository.create(lamaranData); if (existingLamaran) {
const posisiYangSudahDilamar =
return data({ existingLamaran.lowongan_magang?.posisi || "posisi tertentu";
id_mahasiswa: existingMahasiswa.id, return error(
id_lowongan_magang, new ConflictError(
batch: batchId, `Anda sudah pernah melamar untuk posisi ${posisiYangSudahDilamar}. Setiap pengguna hanya dapat melamar satu posisi.`,
status: 'diproses', ),
}); );
} }
const cv_path = imageUploadHelper.uploadCV(uploadedFiles.cv); const lamaranData = {
const portofolio_path = imageUploadHelper.uploadPortofolio(uploadedFiles.portofolio); id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang,
const namaBelakangFinal = mahasiswaFormData.nama_belakang?.trim() || null; batch: batchId,
status: "diproses",
const mahasiswaData = { };
nama_depan: mahasiswaFormData.nama_depan,
nama_belakang: namaBelakangFinal, await lamaranMagangRepository.create(lamaranData);
email: mahasiswaFormData.email,
kontak: mahasiswaFormData.kontak, return data({
jurusan: mahasiswaFormData.jurusan, id_mahasiswa: existingMahasiswa.id,
universitas: mahasiswaFormData.universitas, id_lowongan_magang,
negara: mahasiswaFormData.negara, batch: batchId,
motivasi: mahasiswaFormData.motivasi, status: "diproses",
relevant_skills: mahasiswaFormData.relevant_skills, });
role: 'student', }
cv_path,
portofolio_path, const cv_path = imageUploadHelper.uploadCV(uploadedFiles.cv);
}; const portofolio_path = imageUploadHelper.uploadPortofolio(
uploadedFiles.portofolio,
const createdMahasiswa = await mahasiswaRepository.create(mahasiswaData); );
const id_mahasiswa = createdMahasiswa.id;
const namaBelakangFinal = mahasiswaFormData.nama_belakang?.trim() || null;
const lamaranData = {
id_mahasiswa, const mahasiswaData = {
id_lowongan_magang, nama_depan: mahasiswaFormData.nama_depan,
batch: batchId, nama_belakang: namaBelakangFinal,
status: 'diproses', email: mahasiswaFormData.email,
}; kontak: mahasiswaFormData.kontak,
jurusan: mahasiswaFormData.jurusan,
await lamaranMagangRepository.create(lamaranData); universitas: mahasiswaFormData.universitas,
negara: mahasiswaFormData.negara,
await sendConfirmationEmail(mahasiswaData, lowonganMagang); motivasi: mahasiswaFormData.motivasi,
relevant_skills: mahasiswaFormData.relevant_skills,
return data({ role: "student",
id_mahasiswa, cv_path,
id_lowongan_magang, portofolio_path,
batch: batchId, };
status: 'diproses',
}); const createdMahasiswa = await mahasiswaRepository.create(mahasiswaData);
} catch (err) { const id_mahasiswa = createdMahasiswa.id;
return error(err);
} const lamaranData = {
} id_mahasiswa,
id_lowongan_magang,
async getAllLamaranMagang(query = {}) { batch: batchId,
try { status: "diproses",
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 }); await lamaranMagangRepository.create(lamaranData);
const total = await lamaranMagangRepository.countAllLamaran({ status, posisi, universitas, id_lowongan });
await sendConfirmationEmail(mahasiswaData, lowonganMagang);
const totalPages = Math.ceil(total / limit);
const meta = { page: Number(page), limit: Number(limit), total, totalPages }; return data({
return paginationData(lamaranList, meta); id_mahasiswa,
} catch (err) { id_lowongan_magang,
return error(err); batch: batchId,
} status: "diproses",
} });
} catch (err) {
async getLamaranById(id_lamaran_magang) { return error(err);
try { }
const lamaran = await lamaranMagangRepository.findById(id_lamaran_magang); }
if (!lamaran) { async getAllLamaranMagang(query = {}) {
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan')); try {
} const {
page = 1,
return data(lamaran); limit = 10,
} catch (err) { posisi,
return error(err); status,
} universitas,
} id_lowongan,
} = query;
async getDetailLamaranById(id_lamaran) { const offset = (page - 1) * limit;
try { const lamaranList = await lamaranMagangRepository.findAll({
const lamaran = await lamaranMagangRepository.findDetailById(id_lamaran); offset,
limit,
if (!lamaran) { status,
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan')); posisi,
} universitas,
id_lowongan,
return data(lamaran); });
} catch (err) { const total = await lamaranMagangRepository.countAllLamaran({
return error(err); status,
} posisi,
} universitas,
id_lowongan,
async updateLamaranStatus(id_lamaran_magang, status) { });
try {
const checkResult = await this.getDetailLamaranById(id_lamaran_magang); const totalPages = Math.ceil(total / limit);
const meta = {
if (checkResult.err) { page: Number(page),
return checkResult; limit: Number(limit),
} total,
totalPages,
const result = await lamaranMagangRepository.updateStatus(id_lamaran_magang, status); };
return paginationData(lamaranList, meta);
let generatedPassword = null; } catch (err) {
if (status === 'diterima') { return error(err);
const existingUser = await prisma.user.findFirst({ }
where: { }
OR: [
{ id_mahasiswa: checkResult.data.id_mahasiswa }, async getLamaranById(id_lamaran_magang) {
{ id_lamaran_magang: parseInt(id_lamaran_magang) }, try {
{ email: checkResult.data.mahasiswa.email } const lamaran = await lamaranMagangRepository.findById(id_lamaran_magang);
]
} if (!lamaran) {
}); return error(new NotFoundError("Lamaran Magang Tidak Ditemukan"));
}
// Akan dibuat akunnya ketika sudah ACC ama mentor dengan password yang random [Rafi 08/06/2026 16:05]
if (!existingUser) { return data(lamaran);
generatedPassword = crypto.randomBytes(4).toString('hex'); } catch (err) {
const hashedPassword = await bcrypt.hash(generatedPassword, 10); return error(err);
}
const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? ' ' + checkResult.data.mahasiswa.nama_belakang : ''}`; }
await prisma.user.create({ async getDetailLamaranById(id_lamaran) {
data: { try {
id_mahasiswa: checkResult.data.id_mahasiswa, const lamaran = await lamaranMagangRepository.findDetailById(id_lamaran);
id_lamaran_magang: parseInt(id_lamaran_magang),
full_name: fullName, if (!lamaran) {
email: checkResult.data.mahasiswa.email, return error(new NotFoundError("Lamaran Magang Tidak Ditemukan"));
password: hashedPassword, }
role: 'intern',
is_active: true return data(lamaran);
} } catch (err) {
}); return error(err);
} }
} }
await sendStatusEmail(checkResult.data, status, generatedPassword); async updateLamaranStatus(id_lamaran_magang, status) {
try {
return data(result); const checkResult = await this.getDetailLamaranById(id_lamaran_magang);
} catch (err) {
return error(err); if (checkResult.err) {
} return checkResult;
} }
async deleteLamaran(id) { const result = await lamaranMagangRepository.updateStatus(
try { id_lamaran_magang,
const checkResult = await this.getDetailLamaranById(id); status,
if (checkResult.err) { );
return checkResult;
} let generatedPassword = null;
if (status === "diterima") {
const lamaran = checkResult.data; const existingUser = await prisma.user.findFirst({
where: {
if (lamaran.cv_path) { OR: [
const cvFileName = path.basename(lamaran.cv_path); { id_mahasiswa: checkResult.data.id_mahasiswa },
await deleteFileIfExists(cvFileName); { id_lamaran_magang: parseInt(id_lamaran_magang) },
} { email: checkResult.data.mahasiswa.email },
],
if (lamaran.portofolio_path) { },
const portofolioFileName = path.basename(lamaran.portofolio_path); });
await deleteFileIfExists(portofolioFileName);
} // Akan dibuat akunnya ketika sudah ACC ama mentor dengan password yang random [Rafi 08/06/2026 16:05]
if (!existingUser) {
const result = await lamaranMagangRepository.deleteById(id); generatedPassword = crypto.randomBytes(4).toString("hex");
return data(result); const hashedPassword = await bcrypt.hash(generatedPassword, 10);
} catch (err) {
return error(err); const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? " " + checkResult.data.mahasiswa.nama_belakang : ""}`;
}
} await prisma.user.create({
data: {
async exportToExcel() { id_mahasiswa: checkResult.data.id_mahasiswa,
try { id_lamaran_magang: parseInt(id_lamaran_magang),
const lamaranList = await lamaranMagangRepository.findAll({}); full_name: fullName,
email: checkResult.data.mahasiswa.email,
if (!lamaranList || lamaranList.length === 0) { password: hashedPassword,
return error(new NotFoundError("Tidak ada data lamaran untuk diekspor")); role: "intern",
} is_active: true,
},
const workbook = new ExcelJS.Workbook(); });
const worksheet = workbook.addWorksheet('Lamaran Magang'); }
}
worksheet.columns = [
{ header: 'ID', key: 'id', width: 10 }, await sendStatusEmail(checkResult.data, status, generatedPassword);
{ header: 'Nama Depan', key: 'nama_depan', width: 20 },
{ header: 'Nama Belakang', key: 'nama_belakang', width: 20 }, return data(result);
{ header: 'Posisi', key: 'posisi', width: 25 }, } catch (err) {
{ header: 'Kelompok Peminatan', key: 'kelompok_peminatan', width: 25 }, return error(err);
{ header: 'Status', key: 'status', width: 15 }, }
{ header: 'Tanggal Dibuat', key: 'created_at', width: 20 }, }
];
async deleteLamaran(id) {
lamaranList.forEach((lamaran) => { try {
worksheet.addRow({ const checkResult = await this.getDetailLamaranById(id);
id: lamaran.id, if (checkResult.err) {
nama_depan: lamaran.mahasiswa?.nama_depan || '', return checkResult;
nama_belakang: lamaran.mahasiswa?.nama_belakang || '', }
posisi: lamaran.lowongan_magang?.posisi || '',
kelompok_peminatan: lamaran.lowongan_magang?.kelompok_peminatan || '', const lamaran = checkResult.data;
status: lamaran.status,
created_at: lamaran.created_at, if (lamaran.cv_path) {
}); const cvFileName = path.basename(lamaran.cv_path);
}); await deleteFileIfExists(cvFileName);
}
worksheet.getRow(1).font = { bold: true };
worksheet.getRow(1).fill = { if (lamaran.portofolio_path) {
type: 'pattern', const portofolioFileName = path.basename(lamaran.portofolio_path);
pattern: 'solid', await deleteFileIfExists(portofolioFileName);
fgColor: { argb: 'FFD3D3D3' }, }
};
const result = await lamaranMagangRepository.deleteById(id);
return data(workbook); return data(result);
} catch (err) { } catch (err) {
return error(err); return error(err);
} }
} }
async getDashboardStatistics(query) { async exportToExcel() {
try { try {
const batch = await batchRepository.getBatchById(query.batch); const lamaranList = await lamaranMagangRepository.findAll({});
const batchDisplay = batch ? batch.batch_number : 'Data Seluruh Batch';
const stats = await lamaranMagangRepository.getStatistics({ query }); if (!lamaranList || lamaranList.length === 0) {
return error(
const acceptanceRate = stats.total > 0 ? ((stats.diterima / stats.total) * 100).toFixed(1) : 0; new NotFoundError("Tidak ada data lamaran untuk diekspor"),
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) { const workbook = new ExcelJS.Workbook();
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); const worksheet = workbook.addWorksheet("Lamaran Magang");
}
worksheet.columns = [
return data({ { header: "ID", key: "id", width: 10 },
total_pendaftar: stats.total, { header: "Nama Depan", key: "nama_depan", width: 20 },
total_diterima: stats.diterima, { header: "Nama Belakang", key: "nama_belakang", width: 20 },
total_ditolak: stats.ditolak, { header: "Posisi", key: "posisi", width: 25 },
sedang_diproses: stats.diproses, { header: "Kelompok Peminatan", key: "kelompok_peminatan", width: 25 },
presentase_diterima: `${acceptanceRate}%`, { header: "Status", key: "status", width: 15 },
presentase_ditolak: `${rejectionRate}%`, { header: "Tanggal Dibuat", key: "created_at", width: 20 },
presentase_diproses: `${pendingRate}%`, ];
batch: batchDisplay,
}); lamaranList.forEach((lamaran) => {
} catch (err) { worksheet.addRow({
return error(err); id: lamaran.id,
} nama_depan: lamaran.mahasiswa?.nama_depan || "",
} nama_belakang: lamaran.mahasiswa?.nama_belakang || "",
posisi: lamaran.lowongan_magang?.posisi || "",
async getStatisticsByPosition(query) { kelompok_peminatan: lamaran.lowongan_magang?.kelompok_peminatan || "",
try { status: lamaran.status,
const batch = await batchRepository.getBatchById(query.batch); created_at: lamaran.created_at,
const batchDisplay = batch ? batch.batch_number : 'semua batch'; });
const positions = await lamaranMagangRepository.getStatisticsByPosition({ query }); });
if (positions.length === 0) { worksheet.getRow(1).font = { bold: true };
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); worksheet.getRow(1).fill = {
} type: "pattern",
pattern: "solid",
return data(positions); fgColor: { argb: "FFD3D3D3" },
} catch (err) { };
return error(err);
} return data(workbook);
} } catch (err) {
return error(err);
async getStatisticsByCountry(query) { }
try { }
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch'; async getDashboardStatistics(query) {
const countries = await lamaranMagangRepository.getStatisticsByCountry({ query }); try {
if (countries.length === 0) { const batch = await batchRepository.getBatchById(query.batch);
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); const batchDisplay = batch ? batch.batch_number : "Data Seluruh Batch";
} const stats = await lamaranMagangRepository.getStatistics({ query });
return data(countries); const acceptanceRate =
} catch (err) { stats.total > 0 ? ((stats.diterima / stats.total) * 100).toFixed(1) : 0;
return error(err); 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;
async getStatisticsByUniversity(query) {
try { if (stats.total === 0) {
const batch = await batchRepository.getBatchById(query.batch); return error(
const batchDisplay = batch ? batch.batch_number : 'semua batch'; new NotFoundError(
const universities = await lamaranMagangRepository.getStatisticsByUniversity({ query }); `Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
if (universities.length === 0) { ),
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); );
} }
return data(universities);
} catch (err) { return data({
return error(err); 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}%`,
module.exports = new LamaranMagangService(); 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({ return prisma.project.findMany({
where, where,
orderBy: { orderBy: {
+19 -14
View File
@@ -14,9 +14,9 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required")); return error(new BadRequestError("Admin ID is required"));
} }
if (actor.role !== "admin") { if (actor.role !== "admin" && actor.role !== "mentor") {
return error( 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 || "", description: projectData.description || "",
start_date: projectData.start_date, start_date: projectData.start_date,
end_date: projectData.end_date, end_date: projectData.end_date,
max_members: 8,
status: "active", status: "active",
}; };
@@ -95,7 +94,6 @@ class ProjectService {
description: project.description, description: project.description,
start_date: project.start_date, start_date: project.start_date,
end_date: project.end_date, end_date: project.end_date,
max_members: project.max_members,
status: project.status, status: project.status,
admin: this.mapAdmin(project.admin), admin: this.mapAdmin(project.admin),
total_members: project.members?.length || 0, total_members: project.members?.length || 0,
@@ -135,9 +133,9 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required")); return error(new BadRequestError("Admin ID is required"));
} }
if (actor.role !== "admin") { if (actor.role !== "admin" && actor.role !== "mentor") {
return error( 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")); return error(new BadRequestError("Admin ID is required"));
} }
if (actor.role !== "admin") { if (actor.role !== "admin" && actor.role !== "mentor") {
return error( 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; 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") { if (actor.role === "intern") {
const membership = await projectRepository.checkActiveMember( const membership = await projectRepository.checkActiveMember(
idProject, idProject,
@@ -259,7 +267,6 @@ class ProjectService {
description: project.description, description: project.description,
start_date: project.start_date, start_date: project.start_date,
end_date: project.end_date, end_date: project.end_date,
max_members: project.max_members,
status: project.status, status: project.status,
admin: this.mapAdmin(project.admin), admin: this.mapAdmin(project.admin),
members: members:
@@ -328,7 +335,6 @@ class ProjectService {
description: project.description, description: project.description,
start_date: project.start_date, start_date: project.start_date,
end_date: project.end_date, end_date: project.end_date,
max_members: project.max_members,
status: project.status, status: project.status,
admin: this.mapAdmin(project.admin), admin: this.mapAdmin(project.admin),
total_members: project.members?.length || 0, total_members: project.members?.length || 0,
@@ -393,7 +399,7 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required")); return error(new BadRequestError("Admin ID is required"));
} }
if (actor.role !== "admin") { if (actor.role !== "admin" && actor.role !== "mentor") {
return error( return error(
new ForbiddenError("Access denied: only mentors/admins can view their created projects"), new ForbiddenError("Access denied: only mentors/admins can view their created projects"),
); );
@@ -408,7 +414,6 @@ class ProjectService {
description: project.description, description: project.description,
start_date: project.start_date, start_date: project.start_date,
end_date: project.end_date, end_date: project.end_date,
max_members: project.max_members,
status: project.status, status: project.status,
admin: this.mapAdmin(project.admin), admin: this.mapAdmin(project.admin),
total_members: project.members?.length || 0, total_members: project.members?.length || 0,
@@ -429,7 +434,7 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required")); return error(new BadRequestError("Admin ID is required"));
} }
if (actor.role !== "admin") { if (actor.role !== "admin" && actor.role !== "mentor") {
return error( return error(
new ForbiddenError("Access denied: only admin/mentor can access interns list"), 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")); return error(new BadRequestError("Admin ID is required"));
} }
if (actor.role !== "admin") { if (actor.role !== "admin" && actor.role !== "mentor") {
return error( return error(
new ForbiddenError("Access denied: only mentors/admins can assign members"), 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 { projectController } = require('../modules/project');
const { verifyJWT } = require('../middleware/verifyJWT'); const { verifyJWT } = require('../middleware/verifyJWT');
const isAdmin = require('../middleware/isAdmin'); const isAdmin = require('../middleware/isAdmin');
const isMentorOrAdmin = require('../middleware/isMentorOrAdmin');
const router = express.Router(); const router = express.Router();
@@ -33,9 +34,6 @@ const router = express.Router();
* type: string * type: string
* format: date * format: date
* example: 2026-08-10 * example: 2026-08-10
* max_members:
* type: integer
* example: 8
* status: * status:
* type: string * type: string
* enum: [active, completed, archived] * enum: [active, completed, archived]
@@ -103,6 +101,352 @@ const router = express.Router();
* type: string * type: string
* enum: [active, completed, archived] * enum: [active, completed, archived]
* example: completed * 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: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.post('/add', verifyJWT, isAdmin, projectController.createProject); router.post('/add', verifyJWT, isMentorOrAdmin, projectController.createProject);
/** /**
* @swagger * @swagger
@@ -238,7 +582,7 @@ router.get('/get/:id', verifyJWT, projectController.getProjectById);
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.patch('/update/:id', verifyJWT, isAdmin, projectController.updateProject); router.patch('/update/:id', verifyJWT, isMentorOrAdmin, projectController.updateProject);
/** /**
* @swagger * @swagger
@@ -269,7 +613,7 @@ router.patch('/update/:id', verifyJWT, isAdmin, projectController.updateProject)
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.delete('/delete/:id', verifyJWT, isAdmin, projectController.archiveProject); router.delete('/delete/:id', verifyJWT, isMentorOrAdmin, projectController.archiveProject);
/** /**
* @swagger * @swagger
@@ -332,7 +676,7 @@ router.get('/my-tasks', verifyJWT, projectController.getMyTasks);
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.get('/mentor-projects', verifyJWT, isAdmin, projectController.getMentorProjects); router.get('/mentor-projects', verifyJWT, isMentorOrAdmin, projectController.getMentorProjects);
/** /**
* @swagger * @swagger
@@ -391,7 +735,7 @@ router.get('/mentor-projects', verifyJWT, isAdmin, projectController.getMentorPr
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.get('/interns', verifyJWT, isAdmin, projectController.getInterns); router.get('/interns', verifyJWT, isMentorOrAdmin, projectController.getInterns);
/** /**
* @swagger * @swagger
@@ -434,6 +778,6 @@ router.get('/interns', verifyJWT, isAdmin, projectController.getInterns);
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.post('/assign-member', verifyJWT, isAdmin, projectController.assignMember); router.post('/assign-member', verifyJWT, isMentorOrAdmin, projectController.assignMember);
module.exports = router; module.exports = router;
+122
View File
@@ -0,0 +1,122 @@
const axios = require('axios');
const BASE_URL = 'http://localhost:9000';
async function runTests() {
console.log('=== STARTING INTEGRATION TESTS FOR ADMIN & MENTOR ROLES ===\n');
let adminToken = '';
let mentorToken = '';
let createdProjectId = null;
const mentorEmail = `mentor_${Date.now()}@internify.com`;
const mentorPassword = 'Password123';
// Helper config generator
const getHeader = (token) => ({
headers: { Authorization: `Bearer ${token}` }
});
try {
// 1. Login as Admin
console.log('1. Logging in as Admin (admin1@internify.com)...');
const loginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
email: 'admin1@internify.com',
password: 'password123'
});
adminToken = loginRes.data.data.token;
console.log(' Admin logged in successfully!\n');
// 2. Create Mentor Account via Admin API
console.log(`2. Creating a new Mentor account (${mentorEmail}) via Admin endpoint...`);
await axios.post(`${BASE_URL}/admin-api/add`, {
nama_depan: 'John',
nama_belakang: 'Mentor',
email: mentorEmail,
password: mentorPassword,
role: 'mentor'
}, getHeader(adminToken));
console.log(' Mentor account created successfully!\n');
// 3. Login as Mentor
console.log('3. Logging in as the newly created Mentor...');
const mentorLoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
email: mentorEmail,
password: mentorPassword
});
mentorToken = mentorLoginRes.data.data.token;
console.log(` Mentor logged in successfully! Role: ${mentorLoginRes.data.data.user.role}\n`);
// 4. Mentor creates a project
console.log('4. Creating a project as Mentor...');
const createProjectRes = await axios.post(`${BASE_URL}/project-api/add`, {
project_icon: 'code',
project_name: `Mentor's Custom Project - ${Date.now()}`,
description: 'A project created specifically to test mentor ownership isolation.',
start_date: '2026-07-01',
end_date: '2026-10-01',
member_emails: []
}, getHeader(mentorToken));
createdProjectId = createProjectRes.data.data.id;
console.log(` Project created successfully! ID: ${createdProjectId}\n`);
// 5. Mentor lists projects (should ONLY see their own created project)
console.log('5. Mentor lists their projects...');
const mentorProjectsRes = await axios.get(`${BASE_URL}/project-api/get`, getHeader(mentorToken));
const mentorProjects = mentorProjectsRes.data.data;
console.log(` Projects found for mentor: ${mentorProjects.length}`);
mentorProjects.forEach(p => console.log(` - [ID: ${p.id}] ${p.project_name} (Created by Admin/Mentor ID: ${p.admin.id})`));
const hasOtherProjects = mentorProjects.some(p => p.id !== createdProjectId);
if (hasOtherProjects) {
throw new Error('TEST FAILED: Mentor is seeing projects they did not create!');
}
console.log(' PASSED: Mentor only sees their own project.\n');
// 6. Admin lists projects (should see all projects, including the mentor\'s project and seeded projects)
console.log('6. Admin lists all projects...');
const adminProjectsRes = await axios.get(`${BASE_URL}/project-api/get`, getHeader(adminToken));
const adminProjects = adminProjectsRes.data.data;
console.log(` Projects found for admin: ${adminProjects.length}`);
const containsMentorProject = adminProjects.some(p => p.id === createdProjectId);
if (!containsMentorProject) {
throw new Error('TEST FAILED: Admin cannot see the project created by the mentor!');
}
console.log(' PASSED: Admin can see all projects in the system.\n');
// 7. Mentor tries to access another project details (e.g. Project 1, created by admin1)
console.log('7. Testing isolation: Mentor trying to access Project ID 1 (which they do not own)...');
try {
await axios.get(`${BASE_URL}/project-api/get/1`, getHeader(mentorToken));
throw new Error('TEST FAILED: Mentor successfully accessed another mentor/admin\'s project!');
} catch (err) {
if (err.response && err.response.status === 403) {
console.log(' PASSED: Access denied with 403 Forbidden as expected.\n');
} else {
throw err;
}
}
// 8. Admin accesses Project details of the mentor\'s project
console.log(`8. Admin accessing details of the Mentor's project (ID: ${createdProjectId})...`);
const adminDetailRes = await axios.get(`${BASE_URL}/project-api/get/${createdProjectId}`, getHeader(adminToken));
console.log(` PASSED: Admin retrieved project details successfully (Name: ${adminDetailRes.data.data.project_name}).\n`);
console.log('=== ALL INTEGRATION TESTS PASSED SUCCESSFULLY! ===');
} catch (err) {
console.error('=== TEST EXECUTION ENCOUNTERED AN ERROR ===');
if (err.response) {
console.error(`Status: ${err.response.status}`);
console.error('Response Data:', JSON.stringify(err.response.data, null, 2));
} else {
console.error(err.message);
}
process.exit(1);
}
}
runTests();