Merge pull request #35 from internship-humic/feat/certificate-api
fix: memberikan akses ke mentor untuk /project/{id_project} route
This commit is contained in:
@@ -221,10 +221,21 @@ class CertificateService {
|
|||||||
async getProjectCertificates(id_project, actor) {
|
async getProjectCertificates(id_project, actor) {
|
||||||
try {
|
try {
|
||||||
if (!actor?.id) {
|
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(
|
return error(
|
||||||
new ForbiddenError('Access denied: only admins/mentors can view project certificates'),
|
new ForbiddenError('Access denied: only admins/mentors can view project certificates'),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ router.get('/detail/:id', verifyJWT, certificateController.getCertificateDetail)
|
|||||||
* @swagger
|
* @swagger
|
||||||
* /certificate-api/project/{id_project}:
|
* /certificate-api/project/{id_project}:
|
||||||
* get:
|
* 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.
|
* description: Admin/Mentor can retrieve all certificates issued to interns for a specific project.
|
||||||
* tags: [Certificate]
|
* tags: [Certificate]
|
||||||
* security:
|
* security:
|
||||||
@@ -324,11 +324,11 @@ router.get('/detail/:id', verifyJWT, certificateController.getCertificateDetail)
|
|||||||
* 401:
|
* 401:
|
||||||
* description: Unauthorized
|
* description: Unauthorized
|
||||||
* 403:
|
* 403:
|
||||||
* description: Forbidden (Admin only)
|
* description: Forbidden (Admin/Mentor only)
|
||||||
* 500:
|
* 500:
|
||||||
* description: Internal server error
|
* description: Internal server error
|
||||||
*/
|
*/
|
||||||
router.get('/project/:id_project', verifyJWT, isAdmin, certificateController.getProjectCertificates);
|
router.get('/project/:id_project', verifyJWT, isMentorOrAdmin, certificateController.getProjectCertificates);
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /certificate-api/verify-uuid/{uuid}:
|
* /certificate-api/verify-uuid/{uuid}:
|
||||||
|
|||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user