refactor: nambahin role mentor di admin account dan ngeremove max_member di project
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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"),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user