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

This commit is contained in:
Rafi Athallah
2026-06-14 20:26:53 +07:00
parent edc287968d
commit 94bbc18224
10 changed files with 937 additions and 381 deletions
+1 -1
View File
@@ -207,7 +207,6 @@ model Project {
description String @db.Text
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
@@ -277,6 +276,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,49 +1,66 @@
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');
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) {
async createLamaranMagang(
id_lowongan_magang,
mahasiswaFormData,
uploadedFiles,
) {
try {
const lowonganMagang = await lowonganMagangRepository.findById(id_lowongan_magang);
const lowonganMagang =
await lowonganMagangRepository.findById(id_lowongan_magang);
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 existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaFormData.email);
const existingMahasiswa = await mahasiswaRepository.findByEmail(
mahasiswaFormData.email,
);
if (existingMahasiswa) {
const existingLamaran = await lamaranMagangRepository.findAnyLamaranByMahasiswa(
existingMahasiswa.id
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 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',
status: "diproses",
};
await lamaranMagangRepository.create(lamaranData);
@@ -52,12 +69,14 @@ class LamaranMagangService {
id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
status: "diproses",
});
}
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;
@@ -71,7 +90,7 @@ class LamaranMagangService {
negara: mahasiswaFormData.negara,
motivasi: mahasiswaFormData.motivasi,
relevant_skills: mahasiswaFormData.relevant_skills,
role: 'student',
role: "student",
cv_path,
portofolio_path,
};
@@ -83,7 +102,7 @@ class LamaranMagangService {
id_mahasiswa,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
status: "diproses",
};
await lamaranMagangRepository.create(lamaranData);
@@ -94,7 +113,7 @@ class LamaranMagangService {
id_mahasiswa,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
status: "diproses",
});
} catch (err) {
return error(err);
@@ -103,13 +122,37 @@ class LamaranMagangService {
async getAllLamaranMagang(query = {}) {
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 lamaranList = await lamaranMagangRepository.findAll({ offset, limit, status, posisi, universitas, id_lowongan });
const total = await lamaranMagangRepository.countAllLamaran({ status, posisi, universitas, id_lowongan });
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 };
const meta = {
page: Number(page),
limit: Number(limit),
total,
totalPages,
};
return paginationData(lamaranList, meta);
} catch (err) {
return error(err);
@@ -121,7 +164,7 @@ class LamaranMagangService {
const lamaran = await lamaranMagangRepository.findById(id_lamaran_magang);
if (!lamaran) {
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan'));
return error(new NotFoundError("Lamaran Magang Tidak Ditemukan"));
}
return data(lamaran);
@@ -135,7 +178,7 @@ class LamaranMagangService {
const lamaran = await lamaranMagangRepository.findDetailById(id_lamaran);
if (!lamaran) {
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan'));
return error(new NotFoundError("Lamaran Magang Tidak Ditemukan"));
}
return data(lamaran);
@@ -152,26 +195,29 @@ class LamaranMagangService {
return checkResult;
}
const result = await lamaranMagangRepository.updateStatus(id_lamaran_magang, status);
const result = await lamaranMagangRepository.updateStatus(
id_lamaran_magang,
status,
);
let generatedPassword = null;
if (status === 'diterima') {
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 }
]
}
{ 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');
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 : ''}`;
const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? " " + checkResult.data.mahasiswa.nama_belakang : ""}`;
await prisma.user.create({
data: {
@@ -180,9 +226,9 @@ class LamaranMagangService {
full_name: fullName,
email: checkResult.data.mahasiswa.email,
password: hashedPassword,
role: 'intern',
is_active: true
}
role: "intern",
is_active: true,
},
});
}
}
@@ -226,29 +272,31 @@ class LamaranMagangService {
const lamaranList = await lamaranMagangRepository.findAll({});
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 worksheet = workbook.addWorksheet('Lamaran Magang');
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 },
{ 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 || '',
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,
});
@@ -256,9 +304,9 @@ class LamaranMagangService {
worksheet.getRow(1).font = { bold: true };
worksheet.getRow(1).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFD3D3D3' },
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFD3D3D3" },
};
return data(workbook);
@@ -270,15 +318,22 @@ class LamaranMagangService {
async getDashboardStatistics(query) {
try {
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 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;
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 error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
}
return data({
@@ -299,11 +354,17 @@ class LamaranMagangService {
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 });
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 error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
}
return data(positions);
@@ -315,10 +376,16 @@ class LamaranMagangService {
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 });
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 error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
}
return data(countries);
@@ -330,10 +397,15 @@ class LamaranMagangService {
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 });
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 error(
new NotFoundError(
`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`,
),
);
}
return data(universities);
} catch (err) {
@@ -342,6 +414,4 @@ class 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({
where,
orderBy: {
+19 -14
View File
@@ -14,9 +14,9 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only admin can create project"),
new ForbiddenError("Access denied: only admin or mentor can create project"),
);
}
@@ -65,7 +65,6 @@ class ProjectService {
description: projectData.description || "",
start_date: projectData.start_date,
end_date: projectData.end_date,
max_members: 8,
status: "active",
};
@@ -95,7 +94,6 @@ class ProjectService {
description: project.description,
start_date: project.start_date,
end_date: project.end_date,
max_members: project.max_members,
status: project.status,
admin: this.mapAdmin(project.admin),
total_members: project.members?.length || 0,
@@ -135,9 +133,9 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only admin can update project"),
new ForbiddenError("Access denied: only admin or mentor can update project"),
);
}
@@ -188,9 +186,9 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only admin can archive project"),
new ForbiddenError("Access denied: only admin or mentor can archive project"),
);
}
@@ -223,6 +221,16 @@ class ProjectService {
return null;
}
if (actor.role === "mentor") {
const project = await projectRepository.findById(idProject);
if (!project || project.id_admin !== parseInt(actor.id)) {
return new ForbiddenError(
"Access denied: you are not the mentor of this project",
);
}
return null;
}
if (actor.role === "intern") {
const membership = await projectRepository.checkActiveMember(
idProject,
@@ -259,7 +267,6 @@ class ProjectService {
description: project.description,
start_date: project.start_date,
end_date: project.end_date,
max_members: project.max_members,
status: project.status,
admin: this.mapAdmin(project.admin),
members:
@@ -328,7 +335,6 @@ class ProjectService {
description: project.description,
start_date: project.start_date,
end_date: project.end_date,
max_members: project.max_members,
status: project.status,
admin: this.mapAdmin(project.admin),
total_members: project.members?.length || 0,
@@ -393,7 +399,7 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only mentors/admins can view their created projects"),
);
@@ -408,7 +414,6 @@ class ProjectService {
description: project.description,
start_date: project.start_date,
end_date: project.end_date,
max_members: project.max_members,
status: project.status,
admin: this.mapAdmin(project.admin),
total_members: project.members?.length || 0,
@@ -429,7 +434,7 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only admin/mentor can access interns list"),
);
@@ -465,7 +470,7 @@ class ProjectService {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
if (actor.role !== "admin" && actor.role !== "mentor") {
return error(
new ForbiddenError("Access denied: only mentors/admins can assign members"),
);
+353 -9
View File
@@ -2,6 +2,7 @@ const express = require('express');
const { projectController } = require('../modules/project');
const { verifyJWT } = require('../middleware/verifyJWT');
const isAdmin = require('../middleware/isAdmin');
const isMentorOrAdmin = require('../middleware/isMentorOrAdmin');
const router = express.Router();
@@ -33,9 +34,6 @@ const router = express.Router();
* type: string
* format: date
* example: 2026-08-10
* max_members:
* type: integer
* example: 8
* status:
* type: string
* enum: [active, completed, archived]
@@ -103,6 +101,352 @@ const router = express.Router();
* type: string
* enum: [active, completed, archived]
* example: completed
* ProjectAdminSummary:
* type: object
* properties:
* id:
* type: integer
* example: 1
* full_name:
* type: string
* example: Admin One
* email:
* type: string
* example: admin1@internify.com
* profile_picture:
* type: string
* nullable: true
* example: /uploads/admin-profile.png
* professional_bio:
* type: string
* nullable: true
* example: Senior Program Manager at Internify.
* ProjectListItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* project_icon:
* type: string
* example: code
* project_name:
* type: string
* example: Internify Platform Dev
* description:
* type: string
* example: Upgrading the core Internify platform backend and web client.
* start_date:
* type: string
* format: date-time
* example: 2026-07-15T00:00:00.000Z
* end_date:
* type: string
* format: date-time
* example: 2026-10-15T00:00:00.000Z
* status:
* type: string
* example: active
* admin:
* $ref: '#/components/schemas/ProjectAdminSummary'
* total_members:
* type: integer
* example: 5
* total_tasks:
* type: integer
* example: 3
* created_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* ProjectMemberUser:
* type: object
* properties:
* id:
* type: integer
* example: 1
* full_name:
* type: string
* example: Rafi Athallah
* email:
* type: string
* example: rafi@student.com
* professional_bio:
* type: string
* nullable: true
* example: Backend developer intern
* position:
* type: string
* nullable: true
* example: Web Developer
* kelompok_peminatan:
* type: string
* nullable: true
* example: Software Engineering
* ProjectMemberItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* id_user:
* type: integer
* example: 1
* status:
* type: string
* example: active
* user:
* $ref: '#/components/schemas/ProjectMemberUser'
* created_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* ProjectTaskItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* title:
* type: string
* example: Database Setup
* description:
* type: string
* example: Setup database schema and write initial seed script.
* deadline_at:
* type: string
* format: date-time
* example: 2026-08-01T00:00:00.000Z
* submission_type:
* type: string
* enum: [file_upload, url_link]
* example: url_link
* created_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* ProjectDetail:
* allOf:
* - $ref: '#/components/schemas/ProjectListItem'
* - type: object
* properties:
* members:
* type: array
* items:
* $ref: '#/components/schemas/ProjectMemberItem'
* tasks:
* type: array
* items:
* $ref: '#/components/schemas/ProjectTaskItem'
* ProjectListResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/ProjectListItem'
* message:
* type: string
* example: Projects retrieved successfully
* code:
* type: integer
* example: 200
* ProjectDetailResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* $ref: '#/components/schemas/ProjectDetail'
* message:
* type: string
* example: Project detail retrieved successfully
* code:
* type: integer
* example: 200
* InternListItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* name:
* type: string
* example: Rafi Athallah
* email:
* type: string
* example: rafi@student.com
* projectName:
* type: string
* example: Internify Platform Dev
* projectId:
* type: integer
* nullable: true
* example: 1
* role:
* type: string
* example: Web Developer
* isAssignedByMentor:
* type: boolean
* example: true
* avatar:
* type: string
* nullable: true
* example: null
* InternListResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/InternListItem'
* message:
* type: string
* example: Interns retrieved successfully
* code:
* type: integer
* example: 200
* MyTaskItem:
* type: object
* properties:
* id:
* type: integer
* example: 1
* id_project:
* type: integer
* example: 1
* project_name:
* type: string
* example: Internify Platform Dev
* project_icon:
* type: string
* example: code
* title:
* type: string
* example: Database Setup
* description:
* type: string
* example: Setup database schema and write initial seed script.
* deadline_at:
* type: string
* format: date-time
* example: 2026-08-01T00:00:00.000Z
* submission_type:
* type: string
* enum: [file_upload, url_link]
* example: url_link
* submission_status:
* type: string
* enum: [submitted, not_submitted]
* example: submitted
* submission_details:
* type: object
* nullable: true
* properties:
* id:
* type: integer
* example: 1
* file_path:
* type: string
* nullable: true
* example: null
* url_link:
* type: string
* nullable: true
* example: https://github.com/rafiathallah3
* submitted_at:
* type: string
* format: date-time
* example: 2026-07-20T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-07-20T10:00:00.000Z
* MyTaskListResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/MyTaskItem'
* message:
* type: string
* example: Intern tasks retrieved successfully
* code:
* type: integer
* example: 200
* AssignMemberResponse:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* type: object
* properties:
* id:
* type: integer
* example: 1
* id_project:
* type: integer
* example: 1
* id_user:
* type: integer
* example: 2
* status:
* type: string
* example: active
* created_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* updated_at:
* type: string
* format: date-time
* example: 2026-06-10T10:00:00.000Z
* message:
* type: string
* example: Member assigned successfully
* code:
* type: integer
* example: 200
* ErrorResponse:
* type: object
* properties:
* status:
* type: boolean
* example: false
* data:
* nullable: true
* example: null
* message:
* type: string
* example: Validation error
* code:
* type: integer
* example: 417
*/
/**
@@ -136,7 +480,7 @@ const router = express.Router();
* 500:
* description: Internal server error
*/
router.post('/add', verifyJWT, isAdmin, projectController.createProject);
router.post('/add', verifyJWT, isMentorOrAdmin, projectController.createProject);
/**
* @swagger
@@ -238,7 +582,7 @@ router.get('/get/:id', verifyJWT, projectController.getProjectById);
* 500:
* description: Internal server error
*/
router.patch('/update/:id', verifyJWT, isAdmin, projectController.updateProject);
router.patch('/update/:id', verifyJWT, isMentorOrAdmin, projectController.updateProject);
/**
* @swagger
@@ -269,7 +613,7 @@ router.patch('/update/:id', verifyJWT, isAdmin, projectController.updateProject)
* 500:
* description: Internal server error
*/
router.delete('/delete/:id', verifyJWT, isAdmin, projectController.archiveProject);
router.delete('/delete/:id', verifyJWT, isMentorOrAdmin, projectController.archiveProject);
/**
* @swagger
@@ -332,7 +676,7 @@ router.get('/my-tasks', verifyJWT, projectController.getMyTasks);
* 500:
* description: Internal server error
*/
router.get('/mentor-projects', verifyJWT, isAdmin, projectController.getMentorProjects);
router.get('/mentor-projects', verifyJWT, isMentorOrAdmin, projectController.getMentorProjects);
/**
* @swagger
@@ -391,7 +735,7 @@ router.get('/mentor-projects', verifyJWT, isAdmin, projectController.getMentorPr
* 500:
* description: Internal server error
*/
router.get('/interns', verifyJWT, isAdmin, projectController.getInterns);
router.get('/interns', verifyJWT, isMentorOrAdmin, projectController.getInterns);
/**
* @swagger
@@ -434,6 +778,6 @@ router.get('/interns', verifyJWT, isAdmin, projectController.getInterns);
* 500:
* description: Internal server error
*/
router.post('/assign-member', verifyJWT, isAdmin, projectController.assignMember);
router.post('/assign-member', verifyJWT, isMentorOrAdmin, projectController.assignMember);
module.exports = router;
+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();