feat: nambahin API assign member, list interns, update profile, serta list projects/tasks

This commit is contained in:
Rafi Athallah
2026-06-08 17:28:37 +07:00
parent f0df564499
commit edc287968d
14 changed files with 1295 additions and 36 deletions
+1
View File
@@ -2961,6 +2961,7 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.16.2",
"@prisma/engines": "6.16.2"
+3
View File
@@ -37,5 +37,8 @@
},
"devDependencies": {
"prisma": "^6.16.2"
},
"prisma": {
"seed": "node prisma/seed.js"
}
}
+222
View File
@@ -0,0 +1,222 @@
const bcrypt = require('bcrypt');
const { PrismaClient } = require('../src/generated/prisma');
const prisma = new PrismaClient();
// Diperlukan ketika isi databasenya masih kosong dan ingin menambahkan dummy data sebagai testing development [Rafi 08/06/2026 16:34]
async function main() {
await prisma.taskSubmission.deleteMany({});
await prisma.projectMember.deleteMany({});
await prisma.user.deleteMany({});
await prisma.task.deleteMany({});
await prisma.lamaranMagang.deleteMany({});
await prisma.project.deleteMany({});
await prisma.lowonganMagang.deleteMany({});
await prisma.mahasiswa.deleteMany({});
await prisma.riwayatPelamar.deleteMany({});
await prisma.feedback.deleteMany({});
await prisma.faq.deleteMany({});
await prisma.partnership.deleteMany({});
await prisma.hasilResearch.deleteMany({});
await prisma.batch.deleteMany({});
await prisma.admin.deleteMany({});
const hashedPassword = await bcrypt.hash('password123', 10);
const admin1 = await prisma.admin.create({
data: {
nama_depan: 'Admin',
nama_belakang: 'One',
email: 'admin1@internify.com',
password: hashedPassword,
role: 'admin',
signature: `Admin-One-${Date.now()}`,
profile_picture: 'https://placehold.co/150',
professional_bio: 'Senior Program Manager at Internify.'
}
});
const admin2 = await prisma.admin.create({
data: {
nama_depan: 'Admin',
nama_belakang: 'Two',
email: 'admin2@internify.com',
password: hashedPassword,
role: 'admin',
signature: `Admin-Two-${Date.now()}`,
profile_picture: 'https://placehold.co/150',
professional_bio: 'Lead Engineering Mentor at Internify.'
}
});
const admin3 = await prisma.admin.create({
data: {
nama_depan: 'Admin',
nama_belakang: 'Three',
email: 'admin3@internify.com',
password: hashedPassword,
role: 'admin',
signature: `Admin-Three-${Date.now()}`,
profile_picture: 'https://placehold.co/150',
professional_bio: 'Design Mentor at Internify.'
}
});
const batch1 = await prisma.batch.create({
data: { id: 1, batch_number: 1, is_active: false }
});
const batch2 = await prisma.batch.create({
data: { id: 2, batch_number: 2, is_active: false }
});
const batch3 = await prisma.batch.create({
data: { id: 3, batch_number: 3, is_active: true }
});
await prisma.hasilResearch.createMany({
data: [
{
nama_project: 'AI Intern Matcher',
deskripsi: 'Match students using machine learning algorithms',
image_path: 'https://placehold.co/600x400',
link_project: 'https://github.com/internify/ai-matcher'
},
{
nama_project: 'Feedback Sentiment Analyzer',
deskripsi: 'Analyze student feedback sentiment trends',
image_path: 'https://placehold.co/600x400',
link_project: 'https://github.com/internify/sentiment'
},
{
nama_project: 'LMS Portal Engine',
deskripsi: 'Modular learning management system backend',
image_path: 'https://placehold.co/600x400',
link_project: 'https://github.com/internify/lms'
}
]
});
await prisma.partnership.createMany({
data: [
{ nama_partner: 'Google Indonesia', image_path: 'https://placehold.co/150' },
{ nama_partner: 'Microsoft Indonesia', image_path: 'https://placehold.co/150' },
{ nama_partner: 'Meta APAC', image_path: 'https://placehold.co/150' }
]
});
// 5. Faq
await prisma.faq.createMany({
data: [
{ pertanyaan: 'Bagaimana cara mendaftar magang?', jawaban: 'Buka menu Lowongan Magang, pilih salah satu lowongan aktif, dan klik tombol daftar.' },
{ pertanyaan: 'Apakah program magang ini berbayar?', jawaban: 'Tergantung pada tipe lowongan (paid/unpaid). Detail tertera di tiap deskripsi lowongan.' },
{ pertanyaan: 'Berapa lama durasi program magang?', jawaban: 'Biasanya durasi magang berlangsung antara 3 hingga 6 bulan penuh.' }
]
});
// 6. Feedback
await prisma.feedback.createMany({
data: [
{ nama: 'Budi Utomo', universitas: 'Institut Teknologi Bandung', pesan: 'Pengalaman magang yang luar biasa! Mentornya ramah dan proyeknya menantang.', batch: 1, posisi: 'Web Developer', tahun: 2025, image_path: 'https://placehold.co/150' },
{ nama: 'Siti Aminah', universitas: 'Universitas Indonesia', pesan: 'Mendapatkan banyak wawasan praktis tentang UI/UX prototyping di industri nyata.', batch: 2, posisi: 'UI/UX Designer', tahun: 2025, image_path: 'https://placehold.co/150' },
{ nama: 'Andi Wijaya', universitas: 'Universitas Gadjah Mada', pesan: 'Sangat direkomendasikan bagi mahasiswa yang ingin belajar data engineering dengan benar.', batch: 2, posisi: 'Data Analyst', tahun: 2026, image_path: 'https://placehold.co/150' }
]
});
// 7. RiwayatPelamar
await prisma.riwayatPelamar.createMany({
data: [
{ batch: 1, negara: 'Indonesia', posisi: 'Web Developer', tahun: 2025, jumlah: 100, pelamar_diterima: 10, pelamar_ditolak: 70, pelamar_diproses: 20, presentase_diterima: 10.0, presentase_ditolak: 70.0, presentase_diproses: 20.0 },
{ batch: 2, negara: 'Indonesia', posisi: 'UI/UX Designer', tahun: 2025, jumlah: 150, pelamar_diterima: 15, pelamar_ditolak: 120, pelamar_diproses: 15, presentase_diterima: 10.0, presentase_ditolak: 80.0, presentase_diproses: 10.0 },
{ batch: 2, negara: 'Malaysia', posisi: 'Data Analyst', tahun: 2026, jumlah: 50, pelamar_diterima: 5, pelamar_ditolak: 40, pelamar_diproses: 5, presentase_diterima: 10.0, presentase_ditolak: 80.0, presentase_diproses: 10.0 }
]
});
// 8. Mahasiswa
const mhs1 = await prisma.mahasiswa.create({
data: { nama_depan: 'Rafi', nama_belakang: 'Athallah', email: 'rafi@student.com', kontak: '08123456789', jurusan: 'Teknik Informatika', universitas: 'ITS', negara: 'Indonesia', cv_path: 'https://example.com/cv-rafi.pdf', portofolio_path: 'https://example.com/portfolio-rafi.pdf', motivasi: 'Ingin mendalami Node.js', relevant_skills: 'Node.js, React, MySQL' }
});
const mhs2 = await prisma.mahasiswa.create({
data: { nama_depan: 'Alice', nama_belakang: 'Smith', email: 'alice@student.com', kontak: '08234567890', jurusan: 'Sistem Informasi', universitas: 'UI', negara: 'Indonesia', cv_path: 'https://example.com/cv-alice.pdf', portofolio_path: 'https://example.com/portfolio-alice.pdf', motivasi: 'Tertarik dengan UI/UX design', relevant_skills: 'Figma, CSS, React' }
});
const mhs3 = await prisma.mahasiswa.create({
data: { nama_depan: 'Bob', nama_belakang: 'Johnson', email: 'bob@student.com', kontak: '08345678901', jurusan: 'Ilmu Komputer', universitas: 'UGM', negara: 'Indonesia', cv_path: 'https://example.com/cv-bob.pdf', portofolio_path: 'https://example.com/portfolio-bob.pdf', motivasi: 'Ingin berkarir di data science', relevant_skills: 'Python, SQL, R' }
});
// 9. LowonganMagang
const lowongan1 = await prisma.lowonganMagang.create({
data: { id: 'LOW-001', posisi: 'Web Developer', kelompok_peminatan: 'Software Engineering', image_path: 'https://placehold.co/600x400', jobdesk: 'Build REST APIs and user interfaces', lokasi: 'Remote', kualifikasi: 'Knowledge of Node.js & React', benefit: 'Certificate, stipend', durasi_awal: new Date('2026-07-01'), durasi_akhir: new Date('2026-12-31'), id_batch: batch3.id, status_lowongan: 'dibuka', paid: 'paid' }
});
const lowongan2 = await prisma.lowonganMagang.create({
data: { id: 'LOW-002', posisi: 'UI/UX Designer', kelompok_peminatan: 'Design', image_path: 'https://placehold.co/600x400', jobdesk: 'Create wireframes, mockups, and high-fidelity designs', lokasi: 'Hybrid', kualifikasi: 'Proficiency in Figma', benefit: 'Certificate, mentoring', durasi_awal: new Date('2026-07-01'), durasi_akhir: new Date('2026-12-31'), id_batch: batch3.id, status_lowongan: 'dibuka', paid: 'unpaid' }
});
const lowongan3 = await prisma.lowonganMagang.create({
data: { id: 'LOW-003', posisi: 'Data Analyst', kelompok_peminatan: 'Data Science', image_path: 'https://placehold.co/600x400', jobdesk: 'Analyze dataset and create interactive dashboard', lokasi: 'Onsite', kualifikasi: 'Skills in Python, SQL, Tableau', benefit: 'Certificate, meals', durasi_awal: new Date('2026-07-01'), durasi_akhir: new Date('2026-12-31'), id_batch: batch3.id, status_lowongan: 'dibuka', paid: 'paid' }
});
// 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' }
});
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' }
});
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' }
});
// 11. LamaranMagang
const lamaran1 = await prisma.lamaranMagang.create({
data: { id_mahasiswa: mhs1.id, id_lowongan_magang: lowongan1.id, status: 'diterima', batch: batch3.id, update_at: new Date() }
});
const lamaran2 = await prisma.lamaranMagang.create({
data: { id_mahasiswa: mhs2.id, id_lowongan_magang: lowongan2.id, status: 'diterima', batch: batch3.id, update_at: new Date() }
});
const lamaran3 = await prisma.lamaranMagang.create({
data: { id_mahasiswa: mhs3.id, id_lowongan_magang: lowongan3.id, status: 'diterima', batch: batch3.id, update_at: new Date() }
});
// 12. Task
const task1 = await prisma.task.create({
data: { id_project: proj1.id, title: 'Database Setup', description: 'Setup database schema and write initial seed script.', deadline_at: new Date('2026-08-01'), submission_type: 'url_link' }
});
const task2 = await prisma.task.create({
data: { id_project: proj1.id, title: 'API Specification', description: 'Draft comprehensive swagger API document.', deadline_at: new Date('2026-08-15'), submission_type: 'file_upload' }
});
const task3 = await prisma.task.create({
data: { id_project: proj2.id, title: 'Implement JWT Validation', description: 'Setup middleware and keys for JWT validation.', deadline_at: new Date('2026-08-10'), submission_type: 'url_link' }
});
// 13. User
const user1 = await prisma.user.create({
data: { id_mahasiswa: mhs1.id, id_lamaran_magang: lamaran1.id, full_name: 'Rafi Athallah', email: 'rafi@student.com', password: hashedPassword, role: 'intern', is_active: true }
});
const user2 = await prisma.user.create({
data: { id_mahasiswa: mhs2.id, id_lamaran_magang: lamaran2.id, full_name: 'Alice Smith', email: 'alice@student.com', password: hashedPassword, role: 'intern', is_active: true }
});
const user3 = await prisma.user.create({
data: { id_mahasiswa: mhs3.id, id_lamaran_magang: lamaran3.id, full_name: 'Bob Johnson', email: 'bob@student.com', password: hashedPassword, role: 'intern', is_active: true }
});
// 14. ProjectMember
await prisma.projectMember.createMany({
data: [
{ id_project: proj1.id, id_user: user1.id, status: 'active' },
{ id_project: proj1.id, id_user: user2.id, status: 'active' },
{ id_project: proj2.id, id_user: user3.id, status: 'active' }
]
});
// 15. TaskSubmission
await prisma.taskSubmission.createMany({
data: [
{ id_task: task1.id, id_user: user1.id, url_link: 'https://github.com/rafiathallah3' },
{ id_task: task2.id, id_user: user2.id, file_path: 'https://example.com/submission-alice.zip' },
{ id_task: task3.id, id_user: user3.id, url_link: 'https://github.com/bob/auth-gateway' }
]
});
}
main()
.catch((e) => {
console.error('Error seeding data:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
+1 -1
View File
@@ -7,5 +7,5 @@ module.exports = function isAdmin(req, res, next) {
return next();
}
return response(res, 'fail', new UnauthorizedError('Akses ditolak: Hanya admin yang dapat mengakses ini'), 'Akses ditolak: Hanya admin yang dapat mengakses ini', ERROR.UNAUTHORIZED);
return response(res, 'fail', { err: new UnauthorizedError('Akses ditolak: Hanya admin yang dapat mengakses ini') }, 'Akses ditolak: Hanya admin yang dapat mengakses ini', ERROR.UNAUTHORIZED);
}
@@ -3,7 +3,9 @@ const { response, data, error } = require('../../../helpers/utils/wrapper');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { BadRequestError, UnauthorizedError, InternalServerError } = require('../../../helpers/error');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { loginModel } = require('../models/auth.model');
const { loginModel, updateProfileModel } = require('../models/auth.model');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
class AuthController {
async login(req, res) {
@@ -48,7 +50,7 @@ class AuthController {
return response(res, 'fail', error(new UnauthorizedError('Tidak diotorisasi: ID pengguna tidak ditemukan')));
}
const result = await authService.getCurrentUser(id);
const result = await authService.getCurrentUser(id, req.role);
if (result.err) {
return response(res, 'fail', result);
@@ -67,6 +69,48 @@ class AuthController {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateProfile(req, res) {
try {
const validatePayload = isValidPayload(req.body, updateProfileModel);
if (validatePayload.err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename).catch(() => { });
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const id = req.id;
const role = req.role;
if (!id || !role) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename).catch(() => { });
}
return response(res, 'fail', error(new UnauthorizedError('Tidak diotorisasi: ID pengguna tidak ditemukan')));
}
const result = await authService.updateProfile(id, role, validatePayload.data, req.file);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Profil berhasil diperbarui', SUCCESS.OK);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename).catch(() => { });
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new AuthController();
+19
View File
@@ -66,9 +66,28 @@ const resetPasswordModel = joi.object().keys({
}),
});
const updateProfileModel = joi.object().keys({
full_name: joi.string().max(255).optional().messages({
'string.base': 'Nama lengkap harus berupa teks.',
'string.max': 'Nama lengkap tidak boleh lebih dari 255 karakter.'
}),
email: joi.string().email({ tlds: { allow: false } }).optional().messages({
'string.base': 'Email harus berupa teks.',
'string.email': 'Format email tidak valid.'
}),
password: joi.string().min(8).optional().messages({
'string.base': 'Password harus berupa teks.',
'string.min': 'Password minimal {#limit} karakter.'
}),
professional_bio: joi.string().allow('', null).optional().messages({
'string.base': 'Bio harus berupa teks.'
})
});
module.exports = {
loginModel,
registerModel,
forgotPasswordModel,
resetPasswordModel,
updateProfileModel,
};
+246 -31
View File
@@ -2,58 +2,133 @@ const bcrypt = require('bcrypt');
const adminService = require('../../admin/services/admin.service');
const { createToken } = require('../../../middleware/verifyJWT');
const { data, error } = require('../../../helpers/utils/wrapper');
const { UnauthorizedError, NotFoundError } = require('../../../helpers/error');
const { UnauthorizedError, NotFoundError, ForbiddenError, ConflictError, BadRequestError } = require('../../../helpers/error');
const prisma = require('../../../helpers/db/db_connection');
class AuthService {
async login(email, password) {
try {
const result = await adminService.getAdminByEmail(email);
if (result.err || !result.data) {
let userRecord = null;
let isUserAdmin = false;
const adminResult = await adminService.getAdminByEmail(email);
if (adminResult.data) {
userRecord = adminResult.data;
isUserAdmin = true;
} else {
const internRecord = await prisma.user.findUnique({
where: {
email: email,
is_active: true
}
});
if (internRecord) {
userRecord = internRecord;
isUserAdmin = false;
}
}
if (!userRecord) {
return error(new UnauthorizedError('Email atau password tidak valid'));
}
const admin = result.data;
const isPasswordValid = await bcrypt.compare(password, admin.password);
const isPasswordValid = await bcrypt.compare(password, userRecord.password);
if (!isPasswordValid) {
return error(new UnauthorizedError('Email atau password tidak valid'));
}
const token = createToken({
id: admin.id.toString(),
email: admin.email,
role: admin.role,
signature: admin.signature
});
const role = isUserAdmin ? (userRecord.role || 'admin') : 'intern';
return data({
token,
admin: {
id: admin.id.toString(),
nama_depan: admin.nama_depan,
nama_belakang: admin.nama_belakang,
email: admin.email,
role: admin.role,
signature: admin.signature,
},
});
const tokenPayload = {
id: userRecord.id.toString(),
email: userRecord.email,
role: role,
signature: isUserAdmin ? userRecord.signature : null
};
const token = createToken(tokenPayload);
if (isUserAdmin) {
return data({
token,
user: {
id: userRecord.id.toString(),
full_name: `${userRecord.nama_depan}${userRecord.nama_belakang ? ' ' + userRecord.nama_belakang : ''}`,
email: userRecord.email,
role: role,
signature: userRecord.signature
},
admin: {
id: userRecord.id.toString(),
nama_depan: userRecord.nama_depan,
nama_belakang: userRecord.nama_belakang,
email: userRecord.email,
role: role,
signature: userRecord.signature,
}
});
} else {
return data({
token,
user: {
id: userRecord.id.toString(),
full_name: userRecord.full_name,
email: userRecord.email,
role: role,
}
});
}
} catch (err) {
return error(err);
}
}
async getCurrentUser(userId) {
async getCurrentUser(userId, role) {
try {
const result = await adminService.getAdminById(userId);
if (role === 'admin') {
const result = await adminService.getAdminById(userId);
if (result.err) {
return result;
if (result.err) {
return result;
}
const { password, ...adminWithoutPassword } = result.data;
return data({
...adminWithoutPassword,
role: 'admin'
});
} else if (role === 'intern') {
const user = await prisma.user.findUnique({
where: {
id: parseInt(userId),
is_active: true
},
include: {
lamaran_magang: {
include: {
lowongan_magang: {
select: {
posisi: true,
kelompok_peminatan: true
}
}
}
}
}
});
if (!user) {
return error(new NotFoundError('Pengguna tidak ditemukan'));
}
const { password, ...userWithoutPassword } = user;
return data(userWithoutPassword);
} else {
return error(new NotFoundError('Role tidak valid'));
}
const { password, ...adminWithoutPassword } = result.data;
return data(adminWithoutPassword);
} catch (err) {
return error(new NotFoundError('Pengguna tidak ditemukan'));
}
@@ -67,6 +142,146 @@ class AuthService {
return error(err);
}
}
async updateProfile(userId, role, payload, imageFile) {
try {
const { full_name, email, password, professional_bio } = payload;
if (email) {
const existingAdmin = await prisma.admin.findUnique({
where: { email }
});
if (existingAdmin && (role !== 'admin' || existingAdmin.id !== parseInt(userId))) {
if (imageFile?.filename) {
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
await deleteFileIfExists(imageFile.filename).catch(() => { });
}
return error(new ConflictError('Email sudah terdaftar'));
}
const existingUser = await prisma.user.findUnique({
where: { email }
});
if (existingUser && (role !== 'intern' || existingUser.id !== parseInt(userId))) {
if (imageFile?.filename) {
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
await deleteFileIfExists(imageFile.filename).catch(() => { });
}
return error(new ConflictError('Email sudah terdaftar'));
}
}
let hashedPassword = undefined;
if (password) {
hashedPassword = await bcrypt.hash(password, 10);
}
if (role === 'admin') {
const adminId = parseInt(userId);
const existingAdmin = await prisma.admin.findUnique({
where: { id: adminId }
});
if (!existingAdmin) {
if (imageFile?.filename) {
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
await deleteFileIfExists(imageFile.filename).catch(() => { });
}
return error(new NotFoundError('Admin tidak ditemukan'));
}
const updateData = {};
if (full_name) {
const nameParts = full_name.trim().split(/\s+/);
updateData.nama_depan = nameParts[0];
updateData.nama_belakang = nameParts.slice(1).join(' ') || '';
}
if (email) updateData.email = email;
if (hashedPassword) updateData.password = hashedPassword;
if (professional_bio !== undefined) updateData.professional_bio = professional_bio;
if (imageFile) {
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
const imagePath = imageUploadHelper.uploadImage(imageFile);
updateData.profile_picture = imagePath;
if (existingAdmin.profile_picture) {
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const path = require('path');
const oldFileName = path.basename(existingAdmin.profile_picture);
await deleteFileIfExists(oldFileName).catch(() => { });
}
}
const updatedAdmin = await prisma.admin.update({
where: { id: adminId },
data: updateData
});
const { password: _, ...adminWithoutPassword } = updatedAdmin;
return data({
...adminWithoutPassword,
role: 'admin'
});
} else if (role === 'intern') {
if (imageFile) {
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
await deleteFileIfExists(imageFile.filename).catch(() => { });
return error(new ForbiddenError('Hanya admin yang dapat mengupload foto profil'));
}
const internId = parseInt(userId);
const existingUser = await prisma.user.findUnique({
where: { id: internId }
});
if (!existingUser) {
return error(new NotFoundError('User tidak ditemukan'));
}
const updateData = {};
if (full_name) updateData.full_name = full_name;
if (email) updateData.email = email;
if (hashedPassword) updateData.password = hashedPassword;
if (professional_bio !== undefined) updateData.professional_bio = professional_bio;
const updatedUser = await prisma.user.update({
where: { id: internId },
data: updateData,
include: {
lamaran_magang: {
include: {
lowongan_magang: {
select: {
posisi: true,
kelompok_peminatan: true
}
}
}
}
}
});
const { password: _, ...userWithoutPassword } = updatedUser;
return data(userWithoutPassword);
} else {
if (imageFile?.filename) {
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
await deleteFileIfExists(imageFile.filename).catch(() => { });
}
return error(new NotFoundError('Role tidak valid'));
}
} catch (err) {
if (imageFile?.filename) {
try {
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup file:', cleanupErr);
}
}
return error(err);
}
}
}
module.exports = new AuthService();
@@ -43,7 +43,7 @@ const sendConfirmationEmail = async (mahasiswa, lowongan) => {
}
};
const sendStatusEmail = async (lamaran, status) => {
const sendStatusEmail = async (lamaran, status, generatedPassword = null) => {
try {
const { mahasiswa, lowongan_magang } = lamaran;
const { nama_depan, nama_belakang, email } = mahasiswa;
@@ -61,6 +61,14 @@ const sendStatusEmail = async (lamaran, status) => {
<p>Halo ${fullName},</p>
<p>Selamat! Lamaran Anda untuk posisi <strong>${posisi}</strong> di <strong>Humic Engineering</strong> telah diterima.</p>
<p>Tim kami sangat terkesan dengan profil dan kualifikasi Anda. Kami akan segera menghubungi Anda terkait tahapan selanjutnya.</p>
${generatedPassword ? `
<div style="background-color: #f9f9f9; padding: 15px; border-radius: 5px; margin: 15px 0; border: 1px solid #e0e0e0; font-family: monospace;">
<p style="margin-top: 0; font-weight: bold; color: #333; font-family: sans-serif;">Akun Portal Intern Anda telah dibuat:</p>
<p style="margin: 5px 0;"><strong>Email:</strong> ${email}</p>
<p style="margin: 5px 0;"><strong>Password:</strong> ${generatedPassword}</p>
<p style="margin-bottom: 0; font-size: 13px; color: #555; font-family: sans-serif;">Silakan masuk menggunakan akun ini untuk melihat project dan tugas magang Anda.</p>
</div>
` : ''}
<p>Terima kasih telah melamar dan kami menantikan kerja sama yang luar biasa bersama Anda.</p>
`
: `
@@ -2,6 +2,9 @@ const lamaranMagangRepository = require('../repositories/lamaranMagang.repositor
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');
@@ -151,7 +154,40 @@ class LamaranMagangService {
const result = await lamaranMagangRepository.updateStatus(id_lamaran_magang, status);
await sendStatusEmail(checkResult.data, status);
let generatedPassword = null;
if (status === 'diterima') {
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ id_mahasiswa: checkResult.data.id_mahasiswa },
{ id_lamaran_magang: parseInt(id_lamaran_magang) },
{ email: checkResult.data.mahasiswa.email }
]
}
});
// Akan dibuat akunnya ketika sudah ACC ama mentor dengan password yang random [Rafi 08/06/2026 16:05]
if (!existingUser) {
generatedPassword = crypto.randomBytes(4).toString('hex');
const hashedPassword = await bcrypt.hash(generatedPassword, 10);
const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? ' ' + checkResult.data.mahasiswa.nama_belakang : ''}`;
await prisma.user.create({
data: {
id_mahasiswa: checkResult.data.id_mahasiswa,
id_lamaran_magang: parseInt(id_lamaran_magang),
full_name: fullName,
email: checkResult.data.mahasiswa.email,
password: hashedPassword,
role: 'intern',
is_active: true
}
});
}
}
await sendStatusEmail(checkResult.data, status, generatedPassword);
return data(result);
} catch (err) {
@@ -162,6 +162,111 @@ class ProjectController {
);
}
}
async getMyProjects(req, res) {
try {
const actor = getActor(req);
const result = await projectService.getMyProjects(actor);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Intern projects retrieved successfully', SUCCESS.OK);
} catch (err) {
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Unexpected error occurred',
ERROR.INTERNAL_ERROR
);
}
}
async getMyTasks(req, res) {
try {
const actor = getActor(req);
const result = await projectService.getMyTasks(actor);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Intern tasks retrieved successfully', SUCCESS.OK);
} catch (err) {
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Unexpected error occurred',
ERROR.INTERNAL_ERROR
);
}
}
async getMentorProjects(req, res) {
try {
const actor = getActor(req);
const result = await projectService.getMentorProjects(actor);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Mentor projects retrieved successfully', SUCCESS.OK);
} catch (err) {
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Unexpected error occurred',
ERROR.INTERNAL_ERROR
);
}
}
async getInterns(req, res) {
try {
const actor = getActor(req);
const result = await projectService.getInterns(actor);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Interns retrieved successfully', SUCCESS.OK);
} catch (err) {
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Unexpected error occurred',
ERROR.INTERNAL_ERROR
);
}
}
async assignMember(req, res) {
try {
const actor = getActor(req);
const result = await projectService.assignMember(req.body, actor);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Member assigned successfully', SUCCESS.OK);
} catch (err) {
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Unexpected error occurred',
ERROR.INTERNAL_ERROR
);
}
}
}
module.exports = new ProjectController();
@@ -203,6 +203,193 @@ class ProjectRepository {
},
};
}
async findProjectsByIntern(internId) {
return prisma.project.findMany({
where: {
members: {
some: {
id_user: parseInt(internId),
status: "active",
},
},
},
orderBy: {
created_at: "desc",
},
include: {
admin: {
select: {
id: true,
nama_depan: true,
nama_belakang: true,
email: true,
profile_picture: true,
professional_bio: true,
},
},
members: {
where: {
status: "active",
},
select: {
id: true,
},
},
tasks: {
select: {
id: true,
},
},
},
});
}
async findTasksByIntern(internId) {
return prisma.task.findMany({
where: {
project: {
members: {
some: {
id_user: parseInt(internId),
status: "active",
},
},
},
},
orderBy: {
deadline_at: "asc",
},
include: {
project: {
select: {
id: true,
project_name: true,
project_icon: true,
},
},
submissions: {
where: {
id_user: parseInt(internId),
},
select: {
id: true,
file_path: true,
url_link: true,
submitted_at: true,
updated_at: true,
},
},
},
});
}
async findProjectsByMentor(adminId) {
return prisma.project.findMany({
where: {
id_admin: parseInt(adminId),
},
orderBy: {
created_at: "desc",
},
include: {
admin: {
select: {
id: true,
nama_depan: true,
nama_belakang: true,
email: true,
profile_picture: true,
professional_bio: true,
},
},
members: {
where: {
status: "active",
},
select: {
id: true,
},
},
tasks: {
select: {
id: true,
},
},
},
});
}
async findActiveInternsWithProjects() {
return prisma.user.findMany({
where: {
role: "intern",
is_active: true,
},
include: {
project_members: {
where: {
status: "active",
},
include: {
project: {
select: {
id: true,
project_name: true,
},
},
},
},
lamaran_magang: {
include: {
lowongan_magang: {
select: {
posisi: true,
},
},
},
},
},
orderBy: {
id: "asc",
},
});
}
async assignMember(id_project, id_user) {
return prisma.projectMember.create({
data: {
id_project: parseInt(id_project),
id_user: parseInt(id_user),
status: "active",
},
});
}
async findUserByIdAndActive(id) {
return prisma.user.findFirst({
where: {
id: parseInt(id),
is_active: true,
},
});
}
async findMembership(idProject, idUser) {
return prisma.projectMember.findFirst({
where: {
id_project: parseInt(idProject),
id_user: parseInt(idUser),
},
});
}
async updateMembershipStatus(id, status) {
return prisma.projectMember.update({
where: { id: parseInt(id) },
data: { status },
});
}
}
module.exports = new ProjectRepository();
@@ -4,6 +4,7 @@ const {
BadRequestError,
NotFoundError,
ForbiddenError,
ConflictError,
} = require("../../../helpers/error");
class ProjectService {
@@ -305,6 +306,209 @@ class ProjectService {
updated_at: member.updated_at,
};
}
async getMyProjects(actor = {}) {
try {
if (!actor?.id) {
return error(new BadRequestError("User ID is required"));
}
if (actor.role !== "intern") {
return error(
new ForbiddenError("Access denied: only interns can view their projects"),
);
}
const projects = await projectRepository.findProjectsByIntern(actor.id);
const mappedProjects = projects.map((project) => ({
id: project.id,
project_icon: project.project_icon,
project_name: project.project_name,
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,
total_tasks: project.tasks?.length || 0,
created_at: project.created_at,
updated_at: project.updated_at,
}));
return data(mappedProjects);
} catch (err) {
return error(err);
}
}
async getMyTasks(actor = {}) {
try {
if (!actor?.id) {
return error(new BadRequestError("User ID is required"));
}
if (actor.role !== "intern") {
return error(
new ForbiddenError("Access denied: only interns can view their tasks"),
);
}
const tasks = await projectRepository.findTasksByIntern(actor.id);
const mappedTasks = tasks.map((task) => {
const submission = task.submissions?.[0] || null;
return {
id: task.id,
id_project: task.id_project,
project_name: task.project?.project_name || null,
project_icon: task.project?.project_icon || null,
title: task.title,
description: task.description,
deadline_at: task.deadline_at,
submission_type: task.submission_type,
created_at: task.created_at,
updated_at: task.updated_at,
submission_status: submission ? "submitted" : "not_submitted",
submission_details: submission ? {
id: submission.id,
file_path: submission.file_path,
url_link: submission.url_link,
submitted_at: submission.submitted_at,
updated_at: submission.updated_at,
} : null,
};
});
return data(mappedTasks);
} catch (err) {
return error(err);
}
}
async getMentorProjects(actor = {}) {
try {
if (!actor?.id) {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
return error(
new ForbiddenError("Access denied: only mentors/admins can view their created projects"),
);
}
const projects = await projectRepository.findProjectsByMentor(actor.id);
const mappedProjects = projects.map((project) => ({
id: project.id,
project_icon: project.project_icon,
project_name: project.project_name,
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,
total_tasks: project.tasks?.length || 0,
created_at: project.created_at,
updated_at: project.updated_at,
}));
return data(mappedProjects);
} catch (err) {
return error(err);
}
}
async getInterns(actor = {}) {
try {
if (!actor?.id) {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
return error(
new ForbiddenError("Access denied: only admin/mentor can access interns list"),
);
}
const interns = await projectRepository.findActiveInternsWithProjects();
const mappedInterns = interns.map((intern) => {
const activeMembership = intern.project_members?.[0] || null;
const projectName = activeMembership?.project?.project_name || "Unassigned";
const role = intern.lamaran_magang?.lowongan_magang?.posisi || "-";
return {
id: intern.id,
name: intern.full_name,
email: intern.email,
projectName: projectName,
role: role,
isAssignedByMentor: projectName !== "Unassigned",
avatar: null
};
});
return data(mappedInterns);
} catch (err) {
return error(err);
}
}
async assignMember(payload, actor = {}) {
try {
if (!actor?.id) {
return error(new BadRequestError("Admin ID is required"));
}
if (actor.role !== "admin") {
return error(
new ForbiddenError("Access denied: only mentors/admins can assign members"),
);
}
const { id_project, id_user } = payload;
if (!id_project || !id_user) {
return error(new BadRequestError("Project ID and User ID are required"));
}
// Check if project exists
const project = await projectRepository.findById(id_project);
if (!project) {
return error(new NotFoundError("Project not found"));
}
// Check if user exists and is active
const user = await projectRepository.findUserByIdAndActive(id_user);
if (!user) {
return error(new NotFoundError("User not found or inactive"));
}
// Check if already a member of this project
const existingMembership = await projectRepository.findMembership(id_project, id_user);
let membership;
if (existingMembership) {
if (existingMembership.status === 'active') {
return error(new ConflictError("User is already an active member of this project"));
}
// Reactivate membership
membership = await projectRepository.updateMembershipStatus(existingMembership.id, 'active');
} else {
// Create new membership
membership = await projectRepository.assignMember(id_project, id_user);
}
return data(membership);
} catch (err) {
return error(err);
}
}
}
module.exports = new ProjectService();
+50
View File
@@ -1,6 +1,8 @@
const express = require('express');
const { authController } = require('../modules/auth');
const { verifyJWT } = require('../middleware/verifyJWT');
const multer = require('../middleware/multer');
const { multerErrorHandler } = require('../middleware/multer');
const router = express.Router();
@@ -92,4 +94,52 @@ router.post("/login", authController.login);
*/
router.get("/me", verifyJWT, authController.me);
/**
* @swagger
* /auth-api/update-profile:
* patch:
* summary: Update authenticated user profile
* description: Update profile details (full name, email, password, professional bio). Admins can also upload a profile picture.
* tags: [Auth]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* full_name:
* type: string
* example: "Jonathan Kristina"
* email:
* type: string
* example: "jonathan.kristina@example.com"
* password:
* type: string
* example: "newSecurePassword123"
* professional_bio:
* type: string
* example: "Experienced UI/UX Designer and Engineer"
* profile_picture:
* type: string
* format: binary
* description: Admin only. Optional profile picture upload.
* responses:
* 200:
* description: Profile successfully updated
* 400:
* description: Bad Request (Validation error)
* 401:
* description: Unauthorized
* 403:
* description: Forbidden (Interns uploading images)
* 409:
* description: Conflict (Email already in use)
* 500:
* description: Internal server error
*/
router.patch('/update-profile', verifyJWT, multer.single('profile_picture'), multerErrorHandler, authController.updateProfile);
module.exports = router;
+165
View File
@@ -271,4 +271,169 @@ router.patch('/update/:id', verifyJWT, isAdmin, projectController.updateProject)
*/
router.delete('/delete/:id', verifyJWT, isAdmin, projectController.archiveProject);
/**
* @swagger
* /project-api/my-projects:
* get:
* summary: Get intern's active projects
* description: Retrieve all active projects where the logged-in intern is a member.
* tags: [Project]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Intern projects retrieved successfully
* 401:
* description: Unauthorized
* 403:
* description: Forbidden (Only for interns)
* 500:
* description: Internal server error
*/
router.get('/my-projects', verifyJWT, projectController.getMyProjects);
/**
* @swagger
* /project-api/my-tasks:
* get:
* summary: Get intern's tasks and submission statuses
* description: Retrieve all tasks from all projects where the logged-in intern is an active member, including their specific submission detail/status.
* tags: [Project]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Intern tasks retrieved successfully
* 401:
* description: Unauthorized
* 403:
* description: Forbidden (Only for interns)
* 500:
* description: Internal server error
*/
router.get('/my-tasks', verifyJWT, projectController.getMyTasks);
/**
* @swagger
* /project-api/mentor-projects:
* get:
* summary: Get projects created by mentor
* description: Retrieve all projects created by the logged-in admin/mentor.
* tags: [Project]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Mentor projects retrieved successfully
* 401:
* description: Unauthorized
* 403:
* description: Forbidden (Only for mentors)
* 500:
* description: Internal server error
*/
router.get('/mentor-projects', verifyJWT, isAdmin, projectController.getMentorProjects);
/**
* @swagger
* /project-api/interns:
* get:
* summary: Get all interns details
* description: Retrieve details for all active interns, including their current assigned project and project role. Accessible only by admin/mentor.
* tags: [Project]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Interns details retrieved successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: "success"
* message:
* type: string
* example: "Interns retrieved successfully"
* data:
* type: array
* items:
* 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"
* role:
* type: string
* example: "Web Developer"
* isAssignedByMentor:
* type: boolean
* example: true
* avatar:
* type: string
* nullable: true
* example: null
* 401:
* description: Unauthorized
* 403:
* description: Forbidden (Only for admin/mentors)
* 500:
* description: Internal server error
*/
router.get('/interns', verifyJWT, isAdmin, projectController.getInterns);
/**
* @swagger
* /project-api/assign-member:
* post:
* summary: Assign an intern to a project
* description: Assigns an intern to a project by project ID and user ID. Only accessible by mentor/admin.
* tags: [Project]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - id_project
* - id_user
* properties:
* id_project:
* type: integer
* example: 1
* id_user:
* type: integer
* example: 2
* responses:
* 200:
* description: Member assigned successfully
* 400:
* description: Bad request
* 401:
* description: Unauthorized
* 403:
* description: Forbidden (Only for admin/mentors)
* 404:
* description: Project or User not found
* 409:
* description: Conflict (User is already an active member of this project)
* 500:
* description: Internal server error
*/
router.post('/assign-member', verifyJWT, isAdmin, projectController.assignMember);
module.exports = router;