From bbfa152fc2116304156f215b26698a56abbc481e Mon Sep 17 00:00:00 2001 From: Rafi Athallah <65345768+rafiathallah3@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:57:57 +0700 Subject: [PATCH] fix: memberikan akses ke mentor untuk /project/{id_project} route --- .../services/certificate.service.js | 15 +- src/routes/certificate.routes.js | 6 +- test/test_project_certificates_auth.js | 291 ++++++++++++++++++ 3 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 test/test_project_certificates_auth.js diff --git a/src/modules/certificate/services/certificate.service.js b/src/modules/certificate/services/certificate.service.js index 860789b..c3a6002 100644 --- a/src/modules/certificate/services/certificate.service.js +++ b/src/modules/certificate/services/certificate.service.js @@ -221,10 +221,21 @@ class CertificateService { async getProjectCertificates(id_project, actor) { try { if (!actor?.id) { - return error(new BadRequestError('Admin ID is required')); + return error(new BadRequestError('User ID is required')); } - if (actor.role !== 'admin') { + const project = await certificateRepository.findProjectById(id_project); + if (!project) { + return error(new NotFoundError('Project not found')); + } + + if (actor.role === 'mentor') { + if (project.id_admin !== parseInt(actor.id)) { + return error( + new ForbiddenError('Access denied: you are not the mentor of this project'), + ); + } + } else if (actor.role !== 'admin') { return error( new ForbiddenError('Access denied: only admins/mentors can view project certificates'), ); diff --git a/src/routes/certificate.routes.js b/src/routes/certificate.routes.js index 066cba2..0cb75e1 100644 --- a/src/routes/certificate.routes.js +++ b/src/routes/certificate.routes.js @@ -287,7 +287,7 @@ router.get('/detail/:id', verifyJWT, certificateController.getCertificateDetail) * @swagger * /certificate-api/project/{id_project}: * get: - * summary: Get certificates issued for a specific project (Admin only) + * summary: Get certificates issued for a specific project (Admin/Mentor only) * description: Admin/Mentor can retrieve all certificates issued to interns for a specific project. * tags: [Certificate] * security: @@ -324,11 +324,11 @@ router.get('/detail/:id', verifyJWT, certificateController.getCertificateDetail) * 401: * description: Unauthorized * 403: - * description: Forbidden (Admin only) + * description: Forbidden (Admin/Mentor only) * 500: * description: Internal server error */ -router.get('/project/:id_project', verifyJWT, isAdmin, certificateController.getProjectCertificates); +router.get('/project/:id_project', verifyJWT, isMentorOrAdmin, certificateController.getProjectCertificates); /** * @swagger * /certificate-api/verify-uuid/{uuid}: diff --git a/test/test_project_certificates_auth.js b/test/test_project_certificates_auth.js new file mode 100644 index 0000000..469f221 --- /dev/null +++ b/test/test_project_certificates_auth.js @@ -0,0 +1,291 @@ +const axios = require('axios'); +const bcrypt = require('bcrypt'); +const { PrismaClient } = require('../src/generated/prisma'); + +const BASE_URL = 'http://localhost:9000'; +const prisma = new PrismaClient(); + +function createMultipartPayload(boundary, fields, files) { + const chunks = []; + + for (const [key, value] of Object.entries(fields)) { + if (value !== undefined && value !== null) { + chunks.push(Buffer.from(`--${boundary}\r\n` + + `Content-Disposition: form-data; name="${key}"\r\n\r\n` + + `${value}\r\n`)); + } + } + + for (const [key, file] of Object.entries(files)) { + if (file) { + chunks.push(Buffer.from(`--${boundary}\r\n` + + `Content-Disposition: form-data; name="${key}"; filename="${file.name}"\r\n` + + `Content-Type: ${file.type}\r\n\r\n`)); + chunks.push(file.content); + chunks.push(Buffer.from('\r\n')); + } + } + + chunks.push(Buffer.from(`--${boundary}--\r\n`)); + return Buffer.concat(chunks); +} + +async function registerIntern(adminToken, email, firstName, lastName) { + // Create lowongan magang + const lwnBoundary = '----WebKitFormBoundaryLowonganUpload'; + const lwnFields = { + posisi: `Web Developer Test - ${Date.now()}`, + kelompok_peminatan: 'Software Engineering', + jobdesk: 'Testing project certificates access.', + lokasi: 'Remote', + kualifikasi: 'NodeJS', + benefit: 'Certificate', + durasi_awal: '2026-07-01', + durasi_akhir: '2026-10-01', + paid: 'unpaid' + }; + const lwnFiles = { + image: { name: 'poster.png', type: 'image/png', content: Buffer.from('fake-image-data') } + }; + const lwnPayload = createMultipartPayload(lwnBoundary, lwnFields, lwnFiles); + + const createLwnRes = await axios.post( + `${BASE_URL}/lowongan-magang-api/add`, + lwnPayload, + { + headers: { + Authorization: `Bearer ${adminToken}`, + 'Content-Type': `multipart/form-data; boundary=${lwnBoundary}` + } + } + ); + const lowonganId = createLwnRes.data.data.id; + + // Submit application + const appBoundary = '----WebKitFormBoundaryApplicationSubmission'; + const appFields = { + nama_depan: firstName, + nama_belakang: lastName, + email: email, + kontak: '08123456789', + jurusan: 'Computer Science', + universitas: 'Intern University', + negara: 'Indonesia', + motivasi: 'I want to learn.', + relevant_skills: 'NodeJS, React' + }; + const appFiles = { + cv: { name: 'cv.pdf', type: 'application/pdf', content: Buffer.from('fake-pdf-cv') }, + portofolio: { name: 'portfolio.pdf', type: 'application/pdf', content: Buffer.from('fake-pdf-portfolio') } + }; + const appPayload = createMultipartPayload(appBoundary, appFields, appFiles); + + const applyRes = await axios.post( + `${BASE_URL}/lamaran-magang-api/add-mobile/${lowonganId}`, + appPayload, + { + headers: { + 'Content-Type': `multipart/form-data; boundary=${appBoundary}` + } + } + ); + const idMahasiswa = applyRes.data.data.id_mahasiswa; + + // Wait a brief moment + await new Promise(resolve => setTimeout(resolve, 500)); + + // Find lamaran ID + const listLamaranRes = await axios.get(`${BASE_URL}/lamaran-magang-api/get?limit=100`, { + headers: { Authorization: `Bearer ${adminToken}` } + }); + const lamaranItem = listLamaranRes.data.data.find(l => l.id_mahasiswa === idMahasiswa); + if (!lamaranItem) { + throw new Error(`Failed to find lamaran for student ID: ${idMahasiswa}`); + } + const lamaranId = lamaranItem.id; + + // Accept application + await axios.patch(`${BASE_URL}/lamaran-magang-api/update/${lamaranId}`, { + status: 'diterima' + }, { + headers: { Authorization: `Bearer ${adminToken}` } + }); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Force override password + const hashedPassword = await bcrypt.hash('password123', 10); + await prisma.user.update({ + where: { email }, + data: { password: hashedPassword } + }); + + // Login + const loginRes = await axios.post(`${BASE_URL}/auth-api/login`, { + email, + password: 'password123' + }); + + const user = await prisma.user.findUnique({ + where: { email } + }); + + return { + userId: user.id, + token: loginRes.data.data.token + }; +} + +async function runTests() { + console.log('=== STARTING PROJECT CERTIFICATES AUTHENTICATION AND AUTHORIZATION TESTS ===\n'); + + let adminToken = ''; + let mentor1Token = ''; + let mentor2Token = ''; + let internToken = ''; + let project1Id = null; + + const password = 'password123'; + const internEmail = `auth_intern_cert_${Date.now()}@internify.com`; + + const getHeader = (token) => ({ + headers: { Authorization: `Bearer ${token}` } + }); + + try { + // 1. Login as Admin + console.log('1. Logging in as Admin (admin1@internify.com)...'); + const adminLoginRes = await axios.post(`${BASE_URL}/auth-api/login`, { + email: 'admin1@internify.com', + password: password + }); + adminToken = adminLoginRes.data.data.token; + console.log(' Admin logged in successfully!\n'); + + // 2. Logging in as Mentors + console.log('2. Logging in as Mentors...'); + const mentor1LoginRes = await axios.post(`${BASE_URL}/auth-api/login`, { + email: 'mentor1@internify.com', + password: password + }); + mentor1Token = mentor1LoginRes.data.data.token; + + const mentor2LoginRes = await axios.post(`${BASE_URL}/auth-api/login`, { + email: 'mentor2@internify.com', + password: password + }); + mentor2Token = mentor2LoginRes.data.data.token; + console.log(' Mentors logged in successfully!\n'); + + // 3. Registering a new Intern + console.log(`3. Registering a new Intern (${internEmail})...`); + const internObj = await registerIntern(adminToken, internEmail, 'Auth', 'Intern'); + internToken = internObj.token; + console.log(' Intern registered and logged in successfully!\n'); + + // 4. Mentor 1 creates project + console.log('4. Creating project managed by Mentor 1...'); + const project1Res = await axios.post(`${BASE_URL}/project-api/add`, { + project_name: `Mentor 1 Auth Project - ${Date.now()}`, + description: 'Project to test certificate list access authorization.', + start_date: '2026-07-10', + end_date: '2026-08-10', + project_icon: 'code', + background_color: '#BA1A1A' + }, getHeader(mentor1Token)); + project1Id = project1Res.data.data.id; + console.log(` Project created successfully! ID: ${project1Id}\n`); + + // 5. Test Admin Access (Should succeed - 200) + console.log('5. Testing Admin Access...'); + const adminAccessRes = await axios.get( + `${BASE_URL}/certificate-api/project/${project1Id}`, + getHeader(adminToken) + ); + if (adminAccessRes.status !== 200) { + throw new Error(`TEST FAILED: Expected 200, got ${adminAccessRes.status}`); + } + console.log(' PASSED: Admin successfully accessed project certificates.\n'); + + // 6. Test Mentor 1 (Owner) Access (Should succeed - 200) + console.log('6. Testing Mentor 1 (Owner) Access...'); + const mentor1AccessRes = await axios.get( + `${BASE_URL}/certificate-api/project/${project1Id}`, + getHeader(mentor1Token) + ); + if (mentor1AccessRes.status !== 200) { + throw new Error(`TEST FAILED: Expected 200, got ${mentor1AccessRes.status}`); + } + console.log(' PASSED: Mentor 1 successfully accessed project certificates.\n'); + + // 7. Test Mentor 2 (Non-Owner) Access (Should fail - 403) + console.log('7. Testing Mentor 2 (Non-Owner) Access...'); + try { + await axios.get( + `${BASE_URL}/certificate-api/project/${project1Id}`, + getHeader(mentor2Token) + ); + throw new Error('TEST FAILED: Mentor 2 (non-owner) accessed the project certificates!'); + } catch (err) { + if (err.response && err.response.status === 403) { + console.log(' PASSED: Mentor 2 (non-owner) blocked with 403 Forbidden.'); + console.log(' Error Message:', err.response.data.message); + } else { + throw err; + } + } + console.log(''); + + // 8. Test Intern Access (Should fail - 401 or 403) + console.log('8. Testing Intern Access...'); + try { + await axios.get( + `${BASE_URL}/certificate-api/project/${project1Id}`, + getHeader(internToken) + ); + throw new Error('TEST FAILED: Intern accessed the project certificates!'); + } catch (err) { + if (err.response && (err.response.status === 401 || err.response.status === 403)) { + console.log(` PASSED: Intern blocked with ${err.response.status} as expected.`); + console.log(' Error Message:', err.response.data.message); + } else { + throw err; + } + } + console.log(''); + + // 9. Test Non-Existent Project Access by Admin (Should fail - 404) + console.log('9. Testing Non-Existent Project Access by Admin...'); + try { + await axios.get( + `${BASE_URL}/certificate-api/project/999999`, + getHeader(adminToken) + ); + throw new Error('TEST FAILED: Admin retrieved certificates for non-existent project!'); + } catch (err) { + if (err.response && err.response.status === 404) { + console.log(' PASSED: Non-existent project returned 404 Not Found.'); + console.log(' Error Message:', err.response.data.message); + } else { + throw err; + } + } + console.log(''); + + console.log('=== ALL PROJECT CERTIFICATE AUTHENTICATION TESTS PASSED SUCCESSFULLY! ==='); + await prisma.$disconnect(); + process.exit(0); + } 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); + } + await prisma.$disconnect(); + process.exit(1); + } +} + +runTests();