feat: Nambahin API untuk hapusin member dari project
This commit is contained in:
@@ -267,6 +267,27 @@ class ProjectController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeMember(req, res) {
|
||||||
|
try {
|
||||||
|
const actor = getActor(req);
|
||||||
|
const result = await projectService.removeMember(req.body, actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Member removed successfully', SUCCESS.OK);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new ProjectController();
|
module.exports = new ProjectController();
|
||||||
|
|||||||
@@ -571,6 +571,54 @@ class ProjectService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeMember(payload, actor = {}) {
|
||||||
|
try {
|
||||||
|
if (!actor?.id) {
|
||||||
|
return error(new BadRequestError("Admin ID is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.role !== "admin" && actor.role !== "mentor") {
|
||||||
|
return error(
|
||||||
|
new ForbiddenError("Access denied: only mentors/admins can remove members"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id_project, id_user } = payload;
|
||||||
|
|
||||||
|
if (!id_project || !id_user) {
|
||||||
|
return error(new BadRequestError("Project ID and User ID are required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await projectRepository.findById(id_project);
|
||||||
|
if (!project) {
|
||||||
|
return error(new NotFoundError("Project not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await projectRepository.findUserByIdAndActive(id_user);
|
||||||
|
if (!user) {
|
||||||
|
return error(new NotFoundError("User not found or inactive"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMembership = await projectRepository.findMembership(id_project, id_user);
|
||||||
|
|
||||||
|
if (!existingMembership || existingMembership.status !== 'active') {
|
||||||
|
return error(new BadRequestError("User is not an active member of this project"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const membership = await projectRepository.updateMembershipStatus(existingMembership.id, 'removed');
|
||||||
|
|
||||||
|
await notificationService.createNotificationForIntern(
|
||||||
|
id_user,
|
||||||
|
"Dikeluarkan dari Proyek",
|
||||||
|
`Anda telah dikeluarkan dari proyek '${project.project_name}'.`
|
||||||
|
).catch(err => console.error("Failed to send project removal notification:", err));
|
||||||
|
|
||||||
|
return data(membership);
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async generateUniqueProjectSlug(projectName, excludeProjectId = null) {
|
async generateUniqueProjectSlug(projectName, excludeProjectId = null) {
|
||||||
const baseSlug = slugify(projectName, 'project');
|
const baseSlug = slugify(projectName, 'project');
|
||||||
|
|
||||||
|
|||||||
@@ -852,4 +852,59 @@ router.get('/interns', verifyJWT, isMentorOrAdmin, projectController.getInterns)
|
|||||||
*/
|
*/
|
||||||
router.post('/assign-member', verifyJWT, isMentorOrAdmin, projectController.assignMember);
|
router.post('/assign-member', verifyJWT, isMentorOrAdmin, projectController.assignMember);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /project-api/remove-member:
|
||||||
|
* post:
|
||||||
|
* summary: Remove an intern from a project
|
||||||
|
* description: Removes an intern from 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 removed successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Member removed successfully
|
||||||
|
* code:
|
||||||
|
* type: integer
|
||||||
|
* example: 200
|
||||||
|
* 400:
|
||||||
|
* description: Bad request
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden (Only for admin/mentors)
|
||||||
|
* 404:
|
||||||
|
* description: Project or User not found
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.post('/remove-member', verifyJWT, isMentorOrAdmin, projectController.removeMember);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -101,11 +101,83 @@ async function runTests() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Admin accesses Project details of the mentor\'s project
|
// 8. Admin accesses Project details of the mentor's project
|
||||||
console.log(`8. Admin accessing details of the Mentor's project (ID: ${createdProjectId})...`);
|
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));
|
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(` PASSED: Admin retrieved project details successfully (Name: ${adminDetailRes.data.data.project_name}).\n`);
|
||||||
|
|
||||||
|
// 9. Assign and Remove Member Test
|
||||||
|
console.log('9. Testing Assign and Remove Member feature...');
|
||||||
|
// 9.1. Get active interns
|
||||||
|
console.log(' Fetching active interns...');
|
||||||
|
const internsRes = await axios.get(`${BASE_URL}/project-api/interns`, getHeader(mentorToken));
|
||||||
|
const interns = internsRes.data.data;
|
||||||
|
if (interns.length === 0) {
|
||||||
|
throw new Error('TEST FAILED: No interns found to test assignment!');
|
||||||
|
}
|
||||||
|
const testIntern = interns[0];
|
||||||
|
console.log(` Found intern: ${testIntern.name} (ID: ${testIntern.id})`);
|
||||||
|
|
||||||
|
// 9.2. Assign intern to mentor's project
|
||||||
|
console.log(` Assigning intern ID: ${testIntern.id} to project ID: ${createdProjectId}...`);
|
||||||
|
await axios.post(`${BASE_URL}/project-api/assign-member`, {
|
||||||
|
id_project: createdProjectId,
|
||||||
|
id_user: testIntern.id
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
console.log(' PASSED: Intern assigned successfully.');
|
||||||
|
|
||||||
|
// 9.3. Try to remove the intern as an intern (should fail with 401/403)
|
||||||
|
console.log(' Testing authorization: Intern trying to remove themselves or others...');
|
||||||
|
let internToken = '';
|
||||||
|
try {
|
||||||
|
const internLoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: 'rafi@student.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
internToken = internLoginRes.data.data.token;
|
||||||
|
|
||||||
|
await axios.post(`${BASE_URL}/project-api/remove-member`, {
|
||||||
|
id_project: createdProjectId,
|
||||||
|
id_user: testIntern.id
|
||||||
|
}, getHeader(internToken));
|
||||||
|
throw new Error('TEST FAILED: Intern successfully removed a member!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && (err.response.status === 401 || err.response.status === 403)) {
|
||||||
|
console.log(' PASSED: Intern request denied with 401 Unauthorized / 403 Forbidden as expected.');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9.4. Remove the intern as Mentor (should succeed with 200)
|
||||||
|
console.log(` Removing intern ID: ${testIntern.id} from project ID: ${createdProjectId} as Mentor...`);
|
||||||
|
const removeRes = await axios.post(`${BASE_URL}/project-api/remove-member`, {
|
||||||
|
id_project: createdProjectId,
|
||||||
|
id_user: testIntern.id
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
|
||||||
|
if (removeRes.status === 200 && removeRes.data.status === true) {
|
||||||
|
console.log(' PASSED: Intern removed successfully (200 OK).');
|
||||||
|
} else {
|
||||||
|
throw new Error(`TEST FAILED: Failed to remove intern. Response: ${JSON.stringify(removeRes.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9.5. Try to remove the intern again (should fail with 400 because already removed)
|
||||||
|
console.log(' Trying to remove the already removed intern again...');
|
||||||
|
try {
|
||||||
|
await axios.post(`${BASE_URL}/project-api/remove-member`, {
|
||||||
|
id_project: createdProjectId,
|
||||||
|
id_user: testIntern.id
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
throw new Error('TEST FAILED: Successfully removed already removed intern!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 400) {
|
||||||
|
console.log(' PASSED: Request denied with 400 Bad Request as expected.\n');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('=== ALL INTEGRATION TESTS PASSED SUCCESSFULLY! ===');
|
console.log('=== ALL INTEGRATION TESTS PASSED SUCCESSFULLY! ===');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('=== TEST EXECUTION ENCOUNTERED AN ERROR ===');
|
console.error('=== TEST EXECUTION ENCOUNTERED AN ERROR ===');
|
||||||
|
|||||||
Reference in New Issue
Block a user