Merge pull request #10 from internship-humic/feat/admin-project-lms

Nambahin role mentor di admin account dan ngeremove max_member di project table
This commit is contained in:
Rafi Athallah
2026-06-22 16:46:37 +07:00
committed by GitHub
10 changed files with 591 additions and 384 deletions
+1 -1
View File
@@ -208,7 +208,6 @@ model Project {
description String @db.Text
start_date DateTime @db.Date
end_date DateTime @db.Date
max_members Int @default(8)
status ProjectStatus @default(active)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@ -279,6 +278,7 @@ model TaskSubmission {
enum Role {
admin
mentor
intern
}
+3 -3
View File
@@ -151,13 +151,13 @@ async function main() {
// 10. Project
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({
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({
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
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"),
);
}
@@ -86,7 +86,6 @@ class ProjectService {
description: projectData.description || "",
start_date: projectData.start_date,
end_date: projectData.end_date,
max_members: 8,
status: "active",
};
@@ -115,7 +114,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,
@@ -155,9 +153,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"),
);
}
@@ -208,9 +206,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"),
);
}
@@ -243,6 +241,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,
@@ -279,7 +287,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:
@@ -348,7 +355,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,
@@ -413,7 +419,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"),
);
@@ -428,7 +434,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,
@@ -449,7 +454,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"),
);
@@ -485,7 +490,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"),
);
+7 -12
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]
@@ -147,9 +145,6 @@ const router = express.Router();
* type: string
* format: date-time
* example: 2026-10-15T00:00:00.000Z
* max_members:
* type: integer
* example: 8
* status:
* type: string
* example: active
@@ -490,7 +485,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
@@ -611,7 +606,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
@@ -646,7 +641,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
@@ -721,7 +716,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
@@ -780,7 +775,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
@@ -827,6 +822,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;
+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();