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 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
@@ -279,6 +278,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,49 +1,66 @@
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");
const {
data,
error,
paginationData,
} = require("../../../helpers/utils/wrapper");
const { NotFoundError, ConflictError } = require("../../../helpers/error");
class LamaranMagangService { class LamaranMagangService {
async createLamaranMagang(id_lowongan_magang, mahasiswaFormData, uploadedFiles) { async createLamaranMagang(
id_lowongan_magang,
mahasiswaFormData,
uploadedFiles,
) {
try { try {
const lowonganMagang = await lowonganMagangRepository.findById(id_lowongan_magang); const lowonganMagang =
await lowonganMagangRepository.findById(id_lowongan_magang);
if (!lowonganMagang) { if (!lowonganMagang) {
return error(new NotFoundError('Lowongan Magang Tidak Ditemukan')); return error(new NotFoundError("Lowongan Magang Tidak Ditemukan"));
} }
const batchId = lowonganMagang.batch?.id || null; const batchId = lowonganMagang.batch?.id || null;
const existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaFormData.email); const existingMahasiswa = await mahasiswaRepository.findByEmail(
mahasiswaFormData.email,
);
if (existingMahasiswa) { if (existingMahasiswa) {
const existingLamaran =
const existingLamaran = await lamaranMagangRepository.findAnyLamaranByMahasiswa( await lamaranMagangRepository.findAnyLamaranByMahasiswa(
existingMahasiswa.id existingMahasiswa.id,
); );
if (existingLamaran) { if (existingLamaran) {
const posisiYangSudahDilamar = existingLamaran.lowongan_magang?.posisi || 'posisi tertentu'; const posisiYangSudahDilamar =
return error(new ConflictError( existingLamaran.lowongan_magang?.posisi || "posisi tertentu";
`Anda sudah pernah melamar untuk posisi ${posisiYangSudahDilamar}. Setiap pengguna hanya dapat melamar satu posisi.` return error(
)); new ConflictError(
`Anda sudah pernah melamar untuk posisi ${posisiYangSudahDilamar}. Setiap pengguna hanya dapat melamar satu posisi.`,
),
);
} }
const lamaranData = { const lamaranData = {
id_mahasiswa: existingMahasiswa.id, id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang, id_lowongan_magang,
batch: batchId, batch: batchId,
status: 'diproses', status: "diproses",
}; };
await lamaranMagangRepository.create(lamaranData); await lamaranMagangRepository.create(lamaranData);
@@ -52,12 +69,14 @@ class LamaranMagangService {
id_mahasiswa: existingMahasiswa.id, id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang, id_lowongan_magang,
batch: batchId, batch: batchId,
status: 'diproses', status: "diproses",
}); });
} }
const cv_path = imageUploadHelper.uploadCV(uploadedFiles.cv); const cv_path = imageUploadHelper.uploadCV(uploadedFiles.cv);
const portofolio_path = imageUploadHelper.uploadPortofolio(uploadedFiles.portofolio); const portofolio_path = imageUploadHelper.uploadPortofolio(
uploadedFiles.portofolio,
);
const namaBelakangFinal = mahasiswaFormData.nama_belakang?.trim() || null; const namaBelakangFinal = mahasiswaFormData.nama_belakang?.trim() || null;
@@ -71,7 +90,7 @@ class LamaranMagangService {
negara: mahasiswaFormData.negara, negara: mahasiswaFormData.negara,
motivasi: mahasiswaFormData.motivasi, motivasi: mahasiswaFormData.motivasi,
relevant_skills: mahasiswaFormData.relevant_skills, relevant_skills: mahasiswaFormData.relevant_skills,
role: 'student', role: "student",
cv_path, cv_path,
portofolio_path, portofolio_path,
}; };
@@ -83,7 +102,7 @@ class LamaranMagangService {
id_mahasiswa, id_mahasiswa,
id_lowongan_magang, id_lowongan_magang,
batch: batchId, batch: batchId,
status: 'diproses', status: "diproses",
}; };
await lamaranMagangRepository.create(lamaranData); await lamaranMagangRepository.create(lamaranData);
@@ -94,7 +113,7 @@ class LamaranMagangService {
id_mahasiswa, id_mahasiswa,
id_lowongan_magang, id_lowongan_magang,
batch: batchId, batch: batchId,
status: 'diproses', status: "diproses",
}); });
} catch (err) { } catch (err) {
return error(err); return error(err);
@@ -103,13 +122,37 @@ class LamaranMagangService {
async getAllLamaranMagang(query = {}) { async getAllLamaranMagang(query = {}) {
try { try {
const { page = 1, limit = 10, posisi, status, universitas, id_lowongan } = query; const {
page = 1,
limit = 10,
posisi,
status,
universitas,
id_lowongan,
} = query;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
const lamaranList = await lamaranMagangRepository.findAll({ offset, limit, status, posisi, universitas, id_lowongan }); const lamaranList = await lamaranMagangRepository.findAll({
const total = await lamaranMagangRepository.countAllLamaran({ status, posisi, universitas, id_lowongan }); offset,
limit,
status,
posisi,
universitas,
id_lowongan,
});
const total = await lamaranMagangRepository.countAllLamaran({
status,
posisi,
universitas,
id_lowongan,
});
const totalPages = Math.ceil(total / limit); const totalPages = Math.ceil(total / limit);
const meta = { page: Number(page), limit: Number(limit), total, totalPages }; const meta = {
page: Number(page),
limit: Number(limit),
total,
totalPages,
};
return paginationData(lamaranList, meta); return paginationData(lamaranList, meta);
} catch (err) { } catch (err) {
return error(err); return error(err);
@@ -121,7 +164,7 @@ class LamaranMagangService {
const lamaran = await lamaranMagangRepository.findById(id_lamaran_magang); const lamaran = await lamaranMagangRepository.findById(id_lamaran_magang);
if (!lamaran) { if (!lamaran) {
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan')); return error(new NotFoundError("Lamaran Magang Tidak Ditemukan"));
} }
return data(lamaran); return data(lamaran);
@@ -135,7 +178,7 @@ class LamaranMagangService {
const lamaran = await lamaranMagangRepository.findDetailById(id_lamaran); const lamaran = await lamaranMagangRepository.findDetailById(id_lamaran);
if (!lamaran) { if (!lamaran) {
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan')); return error(new NotFoundError("Lamaran Magang Tidak Ditemukan"));
} }
return data(lamaran); return data(lamaran);
@@ -152,26 +195,29 @@ class LamaranMagangService {
return checkResult; return checkResult;
} }
const result = await lamaranMagangRepository.updateStatus(id_lamaran_magang, status); const result = await lamaranMagangRepository.updateStatus(
id_lamaran_magang,
status,
);
let generatedPassword = null; let generatedPassword = null;
if (status === 'diterima') { if (status === "diterima") {
const existingUser = await prisma.user.findFirst({ const existingUser = await prisma.user.findFirst({
where: { where: {
OR: [ OR: [
{ id_mahasiswa: checkResult.data.id_mahasiswa }, { id_mahasiswa: checkResult.data.id_mahasiswa },
{ id_lamaran_magang: parseInt(id_lamaran_magang) }, { id_lamaran_magang: parseInt(id_lamaran_magang) },
{ email: checkResult.data.mahasiswa.email } { email: checkResult.data.mahasiswa.email },
] ],
} },
}); });
// Akan dibuat akunnya ketika sudah ACC ama mentor dengan password yang random [Rafi 08/06/2026 16:05] // Akan dibuat akunnya ketika sudah ACC ama mentor dengan password yang random [Rafi 08/06/2026 16:05]
if (!existingUser) { if (!existingUser) {
generatedPassword = crypto.randomBytes(4).toString('hex'); generatedPassword = crypto.randomBytes(4).toString("hex");
const hashedPassword = await bcrypt.hash(generatedPassword, 10); const hashedPassword = await bcrypt.hash(generatedPassword, 10);
const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? ' ' + checkResult.data.mahasiswa.nama_belakang : ''}`; const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? " " + checkResult.data.mahasiswa.nama_belakang : ""}`;
await prisma.user.create({ await prisma.user.create({
data: { data: {
@@ -180,9 +226,9 @@ class LamaranMagangService {
full_name: fullName, full_name: fullName,
email: checkResult.data.mahasiswa.email, email: checkResult.data.mahasiswa.email,
password: hashedPassword, password: hashedPassword,
role: 'intern', role: "intern",
is_active: true is_active: true,
} },
}); });
} }
} }
@@ -226,29 +272,31 @@ class LamaranMagangService {
const lamaranList = await lamaranMagangRepository.findAll({}); const lamaranList = await lamaranMagangRepository.findAll({});
if (!lamaranList || lamaranList.length === 0) { if (!lamaranList || lamaranList.length === 0) {
return error(new NotFoundError("Tidak ada data lamaran untuk diekspor")); return error(
new NotFoundError("Tidak ada data lamaran untuk diekspor"),
);
} }
const workbook = new ExcelJS.Workbook(); const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Lamaran Magang'); const worksheet = workbook.addWorksheet("Lamaran Magang");
worksheet.columns = [ worksheet.columns = [
{ header: 'ID', key: 'id', width: 10 }, { header: "ID", key: "id", width: 10 },
{ header: 'Nama Depan', key: 'nama_depan', width: 20 }, { header: "Nama Depan", key: "nama_depan", width: 20 },
{ header: 'Nama Belakang', key: 'nama_belakang', width: 20 }, { header: "Nama Belakang", key: "nama_belakang", width: 20 },
{ header: 'Posisi', key: 'posisi', width: 25 }, { header: "Posisi", key: "posisi", width: 25 },
{ header: 'Kelompok Peminatan', key: 'kelompok_peminatan', width: 25 }, { header: "Kelompok Peminatan", key: "kelompok_peminatan", width: 25 },
{ header: 'Status', key: 'status', width: 15 }, { header: "Status", key: "status", width: 15 },
{ header: 'Tanggal Dibuat', key: 'created_at', width: 20 }, { header: "Tanggal Dibuat", key: "created_at", width: 20 },
]; ];
lamaranList.forEach((lamaran) => { lamaranList.forEach((lamaran) => {
worksheet.addRow({ worksheet.addRow({
id: lamaran.id, id: lamaran.id,
nama_depan: lamaran.mahasiswa?.nama_depan || '', nama_depan: lamaran.mahasiswa?.nama_depan || "",
nama_belakang: lamaran.mahasiswa?.nama_belakang || '', nama_belakang: lamaran.mahasiswa?.nama_belakang || "",
posisi: lamaran.lowongan_magang?.posisi || '', posisi: lamaran.lowongan_magang?.posisi || "",
kelompok_peminatan: lamaran.lowongan_magang?.kelompok_peminatan || '', kelompok_peminatan: lamaran.lowongan_magang?.kelompok_peminatan || "",
status: lamaran.status, status: lamaran.status,
created_at: lamaran.created_at, created_at: lamaran.created_at,
}); });
@@ -256,9 +304,9 @@ class LamaranMagangService {
worksheet.getRow(1).font = { bold: true }; worksheet.getRow(1).font = { bold: true };
worksheet.getRow(1).fill = { worksheet.getRow(1).fill = {
type: 'pattern', type: "pattern",
pattern: 'solid', pattern: "solid",
fgColor: { argb: 'FFD3D3D3' }, fgColor: { argb: "FFD3D3D3" },
}; };
return data(workbook); return data(workbook);
@@ -270,15 +318,22 @@ class LamaranMagangService {
async getDashboardStatistics(query) { async getDashboardStatistics(query) {
try { try {
const batch = await batchRepository.getBatchById(query.batch); const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'Data Seluruh Batch'; const batchDisplay = batch ? batch.batch_number : "Data Seluruh Batch";
const stats = await lamaranMagangRepository.getStatistics({ query }); const stats = await lamaranMagangRepository.getStatistics({ query });
const acceptanceRate = stats.total > 0 ? ((stats.diterima / stats.total) * 100).toFixed(1) : 0; const acceptanceRate =
const rejectionRate = stats.total > 0 ? ((stats.ditolak / stats.total) * 100).toFixed(1) : 0; stats.total > 0 ? ((stats.diterima / stats.total) * 100).toFixed(1) : 0;
const pendingRate = stats.total > 0 ? ((stats.diproses / 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) { if (stats.total === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); return error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
} }
return data({ return data({
@@ -299,11 +354,17 @@ class LamaranMagangService {
async getStatisticsByPosition(query) { async getStatisticsByPosition(query) {
try { try {
const batch = await batchRepository.getBatchById(query.batch); const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch'; const batchDisplay = batch ? batch.batch_number : "semua batch";
const positions = await lamaranMagangRepository.getStatisticsByPosition({ query }); const positions = await lamaranMagangRepository.getStatisticsByPosition({
query,
});
if (positions.length === 0) { if (positions.length === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); return error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
} }
return data(positions); return data(positions);
@@ -315,10 +376,16 @@ class LamaranMagangService {
async getStatisticsByCountry(query) { async getStatisticsByCountry(query) {
try { try {
const batch = await batchRepository.getBatchById(query.batch); const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch'; const batchDisplay = batch ? batch.batch_number : "semua batch";
const countries = await lamaranMagangRepository.getStatisticsByCountry({ query }); const countries = await lamaranMagangRepository.getStatisticsByCountry({
query,
});
if (countries.length === 0) { if (countries.length === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); return error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
} }
return data(countries); return data(countries);
@@ -330,10 +397,15 @@ class LamaranMagangService {
async getStatisticsByUniversity(query) { async getStatisticsByUniversity(query) {
try { try {
const batch = await batchRepository.getBatchById(query.batch); const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch'; const batchDisplay = batch ? batch.batch_number : "semua batch";
const universities = await lamaranMagangRepository.getStatisticsByUniversity({ query }); const universities =
await lamaranMagangRepository.getStatisticsByUniversity({ query });
if (universities.length === 0) { if (universities.length === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); return error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
} }
return data(universities); return data(universities);
} catch (err) { } catch (err) {
@@ -342,6 +414,4 @@ class LamaranMagangService {
} }
} }
module.exports = new LamaranMagangService(); 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"),
); );
} }
@@ -86,7 +86,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",
}; };
@@ -115,7 +114,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,
@@ -155,9 +153,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"),
); );
} }
@@ -208,9 +206,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"),
); );
} }
@@ -243,6 +241,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,
@@ -279,7 +287,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:
@@ -348,7 +355,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,
@@ -413,7 +419,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"),
); );
@@ -428,7 +434,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,
@@ -449,7 +454,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"),
); );
@@ -485,7 +490,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"),
); );
+7 -12
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]
@@ -147,9 +145,6 @@ const router = express.Router();
* type: string * type: string
* format: date-time * format: date-time
* example: 2026-10-15T00:00:00.000Z * example: 2026-10-15T00:00:00.000Z
* max_members:
* type: integer
* example: 8
* status: * status:
* type: string * type: string
* example: active * example: active
@@ -490,7 +485,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
@@ -611,7 +606,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
@@ -646,7 +641,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
@@ -721,7 +716,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
@@ -780,7 +775,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
@@ -827,6 +822,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();