From e8a7521bb074a9a614a76af37eee871c13753f80 Mon Sep 17 00:00:00 2001 From: Muhammad Zhafran Ilham Date: Thu, 25 Jun 2026 22:58:04 +0700 Subject: [PATCH] feat: menambahkan manajemen akun mentor oleh admin --- .../admin/controllers/admin.controller.js | 148 +++++++- .../helpers/cleanupUploadedFiles.helper.js | 9 + .../helpers/mentorProfileUpload.helper.js | 81 ++++ src/modules/admin/models/admin.model.js | 74 ++++ .../admin/repositories/admin.repository.js | 51 +++ src/modules/admin/services/admin.service.js | 175 ++++++++- src/routes/admin.routes.js | 346 +++++++++++++++++- 7 files changed, 881 insertions(+), 3 deletions(-) create mode 100644 src/modules/admin/helpers/cleanupUploadedFiles.helper.js create mode 100644 src/modules/admin/helpers/mentorProfileUpload.helper.js diff --git a/src/modules/admin/controllers/admin.controller.js b/src/modules/admin/controllers/admin.controller.js index fc776e4..7f31cc2 100755 --- a/src/modules/admin/controllers/admin.controller.js +++ b/src/modules/admin/controllers/admin.controller.js @@ -3,7 +3,9 @@ const { response, data, error } = require('../../../helpers/utils/wrapper'); const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code'); const { ForbiddenError, BadRequestError, NotFoundError, InternalServerError } = require('../../../helpers/error'); const { isValidPayload } = require('../../../helpers/utils/validator'); -const { createAdminModel, updateAdminModel } = require('../models/admin.model'); +const { createAdminModel, updateAdminModel, createMentorModel, updateMentorModel } = require('../models/admin.model'); +const fs = require('fs'); +const { cleanupUploadedFile } = require('../helpers/cleanupUploadedFiles.helper') class AdminController { async addAdmin(req, res) { @@ -109,6 +111,150 @@ class AdminController { return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); } } + + async createMentor(req, res) { + try { + const validatePayload = isValidPayload(req.body, createMentorModel); + + if (validatePayload.err) { + cleanupUploadedFile(req.file); + + return response( + res, + 'fail', + { err: validatePayload.err, data: null }, + 'Data mentor tidak valid', + ERROR.EXPECTATION_FAILED + ); + } + + const result = await adminService.createMentor(validatePayload.data, req.file); + + if (result.err) { + cleanupUploadedFile(req.file); + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Mentor berhasil dibuat', SUCCESS.CREATED); + } catch (err) { + cleanupUploadedFile(req.file); + + return response( + res, + 'fail', + error(new InternalServerError(err.message)), + 'Terjadi kesalahan yang tidak terduga', + ERROR.INTERNAL_ERROR + ); + } + } + + async getMentors(req, res) { + try { + const result = await adminService.getMentors(); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Mentor berhasil diambil', SUCCESS.OK); + } catch (err) { + return response( + res, + 'fail', + error(new InternalServerError(err.message)), + 'Terjadi kesalahan yang tidak terduga', + ERROR.INTERNAL_ERROR + ); + } + } + + async getMentorById(req, res) { + try { + const { id } = req.params; + + const result = await adminService.getMentorById(id); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Detail mentor berhasil diambil', SUCCESS.OK); + } catch (err) { + return response( + res, + 'fail', + error(new InternalServerError(err.message)), + 'Terjadi kesalahan yang tidak terduga', + ERROR.INTERNAL_ERROR + ); + } + } + + async updateMentor(req, res) { + try { + const { id } = req.params; + + const validatePayload = isValidPayload(req.body, updateMentorModel); + + if (validatePayload.err) { + cleanupUploadedFile(req.file); + + return response( + res, + 'fail', + { err: validatePayload.err, data: null }, + 'Data mentor tidak valid', + ERROR.EXPECTATION_FAILED + ); + } + + const result = await adminService.updateMentor( + id, + validatePayload.data, + req.file + ); + + if (result.err) { + cleanupUploadedFile(req.file); + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Mentor berhasil diperbarui', SUCCESS.OK); + } catch (err) { + cleanupUploadedFile(req.file); + + return response( + res, + 'fail', + error(new InternalServerError(err.message)), + 'Terjadi kesalahan yang tidak terduga', + ERROR.INTERNAL_ERROR + ); + } + } + + async deleteMentor(req, res) { + try { + const { id } = req.params; + + const result = await adminService.deleteMentor(id); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Mentor berhasil dihapus', SUCCESS.OK); + } catch (err) { + return response( + res, + 'fail', + error(new InternalServerError(err.message)), + 'Terjadi kesalahan yang tidak terduga', + ERROR.INTERNAL_ERROR + ); + } + } } module.exports = new AdminController(); diff --git a/src/modules/admin/helpers/cleanupUploadedFiles.helper.js b/src/modules/admin/helpers/cleanupUploadedFiles.helper.js new file mode 100644 index 0000000..37fc442 --- /dev/null +++ b/src/modules/admin/helpers/cleanupUploadedFiles.helper.js @@ -0,0 +1,9 @@ +const cleanupUploadedFile = (file) => { + if (file?.path && fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } +}; + +module.exports = { + cleanupUploadedFile, +}; diff --git a/src/modules/admin/helpers/mentorProfileUpload.helper.js b/src/modules/admin/helpers/mentorProfileUpload.helper.js new file mode 100644 index 0000000..a58414e --- /dev/null +++ b/src/modules/admin/helpers/mentorProfileUpload.helper.js @@ -0,0 +1,81 @@ +const fs = require('fs'); +const path = require('path'); +const multer = require('multer'); +const { response, error } = require('../../../helpers/utils/wrapper'); +const { BadRequestError } = require('../../../helpers/error'); +const { ERROR } = require('../../../helpers/http-status/status_code'); + +const mentorProfileUploadDir = path.join( + process.cwd(), + 'src', + 'uploads', + 'mentor-profiles' +); + +if (!fs.existsSync(mentorProfileUploadDir)) { + fs.mkdirSync(mentorProfileUploadDir, { recursive: true }); +} + +const allowedMimeTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, mentorProfileUploadDir); + }, + filename: (req, file, cb) => { + const safeOriginalName = file.originalname + .replace(/\s+/g, '-') + .replace(/[^a-zA-Z0-9.\-_]/g, ''); + + cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}-${safeOriginalName}`); + }, +}); + +const fileFilter = (req, file, cb) => { + if (!allowedMimeTypes.includes(file.mimetype)) { + return cb( + new BadRequestError('Profile picture must be a JPEG, JPG, PNG, or WEBP image'), + false + ); + } + + cb(null, true); +}; + +const mentorProfileUpload = multer({ + storage, + fileFilter, + limits: { + fileSize: 5 * 1024 * 1024, + }, +}); + +const mentorProfileUploadErrorHandler = (err, req, res, next) => { + if (!err) { + return next(); + } + + if (err instanceof multer.MulterError) { + return response( + res, + 'fail', + error(new BadRequestError(err.message)), + err.message, + ERROR.BAD_REQUEST + ); + } + + return response( + res, + 'fail', + error(err), + err.message || 'Invalid profile picture upload', + ERROR.BAD_REQUEST + ); +}; + +module.exports = { + mentorProfileUpload, + mentorProfileUploadErrorHandler, + mentorProfileUploadDir, +}; diff --git a/src/modules/admin/models/admin.model.js b/src/modules/admin/models/admin.model.js index a9841c5..ccc5cb9 100755 --- a/src/modules/admin/models/admin.model.js +++ b/src/modules/admin/models/admin.model.js @@ -69,7 +69,81 @@ const updateAdminModel = joi.object().keys({ }), }); +const createMentorModel = joi.object().keys({ + nama_depan: joi.string().max(255).required().messages({ + 'string.base': 'Nama depan harus berupa teks.', + 'string.empty': 'Nama depan tidak boleh kosong.', + 'string.max': 'Nama depan maksimal {#limit} karakter.', + 'any.required': 'Nama depan wajib diisi.', + }), + + nama_belakang: joi.string().max(255).required().messages({ + 'string.base': 'Nama belakang harus berupa teks.', + 'string.empty': 'Nama belakang tidak boleh kosong.', + 'string.max': 'Nama belakang maksimal {#limit} karakter.', + 'any.required': 'Nama belakang wajib diisi.', + }), + + email: joi.string().email({ tlds: { allow: false } }).max(255).required().messages({ + 'string.base': 'Email harus berupa teks.', + 'string.empty': 'Email tidak boleh kosong.', + 'string.email': 'Format email tidak valid.', + 'string.max': 'Email maksimal {#limit} karakter.', + 'any.required': 'Email wajib diisi.', + }), + + password: joi.string().min(8).required().messages({ + 'string.base': 'Password harus berupa teks.', + 'string.empty': 'Password tidak boleh kosong.', + 'string.min': 'Password minimal {#limit} karakter.', + 'any.required': 'Password wajib diisi.', + }), + + signature: joi.string().optional().allow('', null).messages({ + 'string.base': 'Signature harus berupa teks.', + }), + + professional_bio: joi.string().optional().allow('', null).messages({ + 'string.base': 'Professional bio harus berupa teks.', + }), +}); + +const updateMentorModel = joi.object().keys({ + nama_depan: joi.string().max(255).optional().messages({ + 'string.base': 'Nama depan harus berupa teks.', + 'string.empty': 'Nama depan tidak boleh kosong.', + 'string.max': 'Nama depan maksimal {#limit} karakter.', + }), + + nama_belakang: joi.string().max(255).optional().messages({ + 'string.base': 'Nama belakang harus berupa teks.', + 'string.empty': 'Nama belakang tidak boleh kosong.', + 'string.max': 'Nama belakang maksimal {#limit} karakter.', + }), + + email: joi.string().email({ tlds: { allow: false } }).max(255).optional().messages({ + 'string.base': 'Email harus berupa teks.', + 'string.email': 'Format email tidak valid.', + 'string.max': 'Email maksimal {#limit} karakter.', + }), + + password: joi.string().min(8).optional().messages({ + 'string.base': 'Password harus berupa teks.', + 'string.min': 'Password minimal {#limit} karakter.', + }), + + signature: joi.string().optional().allow('', null).messages({ + 'string.base': 'Signature harus berupa teks.', + }), + + professional_bio: joi.string().optional().allow('', null).messages({ + 'string.base': 'Professional bio harus berupa teks.', + }), +}); + module.exports = { createAdminModel, updateAdminModel, + createMentorModel, + updateMentorModel, }; diff --git a/src/modules/admin/repositories/admin.repository.js b/src/modules/admin/repositories/admin.repository.js index 4ddcd79..aab2b12 100755 --- a/src/modules/admin/repositories/admin.repository.js +++ b/src/modules/admin/repositories/admin.repository.js @@ -35,6 +35,57 @@ class AdminRepository { where: { id: parseInt(id) } }); } + + async createMentor(mentorData) { + return prisma.admin.create({ + data: { + ...mentorData, + role: 'mentor', + }, + }); + } + + async findMentors() { + return prisma.admin.findMany({ + where: { + role: 'mentor', + } + }); + } + + async findMentorById(id) { + return prisma.admin.findFirst({ + where: { + id: parseInt(id), + role: 'mentor', + }, + }); + } + + async updateMentorById(id, mentorData) { + return prisma.admin.update({ + where: { + id: parseInt(id), + }, + data: mentorData, + }); + } + + async deleteMentorById(id) { + return prisma.admin.delete({ + where: { + id: parseInt(id), + }, + }); + } + + async countProjectsByAdminId(idAdmin) { + return prisma.project.count({ + where: { + id_admin: parseInt(idAdmin), + }, + }); + } } module.exports = new AdminRepository(); diff --git a/src/modules/admin/services/admin.service.js b/src/modules/admin/services/admin.service.js index ea271b3..4b08fd9 100755 --- a/src/modules/admin/services/admin.service.js +++ b/src/modules/admin/services/admin.service.js @@ -1,7 +1,9 @@ +const fs = require('fs'); +const path = require('path'); const bcrypt = require('bcrypt'); const adminRepository = require('../repositories/admin.repository'); const { data, error } = require('../../../helpers/utils/wrapper'); -const { BadRequestError, NotFoundError } = require('../../../helpers/error'); +const { BadRequestError, NotFoundError, ConflictError } = require('../../../helpers/error'); class AdminService { async createAdmin(adminData) { @@ -109,6 +111,177 @@ class AdminService { return error(err); } } + + async createMentor(mentorData, file = null) { + try { + const existingAdmin = await adminRepository.findByEmail(mentorData.email); + + if (existingAdmin) { + return error(new ConflictError('Email sudah terdaftar')); + } + + const hashedPassword = await bcrypt.hash(mentorData.password, 10); + const profilePicturePath = this.buildMentorProfilePicturePath(file); + + const newMentorData = { + nama_depan: mentorData.nama_depan, + nama_belakang: mentorData.nama_belakang, + email: mentorData.email, + password: hashedPassword, + role: 'mentor', + signature: mentorData.signature || `Mentor-${Date.now()}`, + profile_picture: profilePicturePath, + professional_bio: mentorData.professional_bio || null, + }; + + const mentor = await adminRepository.createMentor(newMentorData); + + return data(this.mapMentor(mentor)); + } catch (err) { + return error(err); + } + } + + async getMentors() { + try { + const mentors = await adminRepository.findMentors(); + + return data(mentors.map((mentor) => this.mapMentor(mentor))); + } catch (err) { + return error(err); + } + } + + async getMentorById(id) { + try { + const mentor = await adminRepository.findMentorById(id); + + if (!mentor) { + return error(new NotFoundError('Mentor tidak ditemukan')); + } + + return data(this.mapMentor(mentor)); + } catch (err) { + return error(err); + } + } + + async updateMentor(id, mentorData, file = null) { + try { + const mentor = await adminRepository.findMentorById(id); + + if (!mentor) { + return error(new NotFoundError('Mentor tidak ditemukan')); + } + + if ((!mentorData || Object.keys(mentorData).length === 0) && !file) { + return error(new BadRequestError('Minimal satu field atau foto profil harus dikirim untuk update mentor')); + } + + const updateData = { ...mentorData }; + + if (updateData.email && updateData.email !== mentor.email) { + const existingAdmin = await adminRepository.findByEmail(updateData.email); + + if (existingAdmin && existingAdmin.id !== parseInt(id)) { + return error(new ConflictError('Email sudah terdaftar')); + } + } + + if (updateData.password) { + updateData.password = await bcrypt.hash(updateData.password, 10); + } + + if (file) { + updateData.profile_picture = this.buildMentorProfilePicturePath(file); + } + + delete updateData.role; + + const updatedMentor = await adminRepository.updateMentorById(id, updateData); + + if (file) { + this.deleteLocalMentorProfilePicture(mentor.profile_picture); + } + + return data(this.mapMentor(updatedMentor)); + } catch (err) { + return error(err); + } + } + + async deleteMentor(id) { + try { + const mentor = await adminRepository.findMentorById(id); + + if (!mentor) { + return error(new NotFoundError('Mentor tidak ditemukan')); + } + + const projectCount = await adminRepository.countProjectsByAdminId(id); + + if (projectCount > 0) { + return error( + new ConflictError('Mentor tidak dapat dihapus karena masih memiliki project') + ); + } + + await adminRepository.deleteMentorById(id); + + this.deleteLocalMentorProfilePicture(mentor.profile_picture); + + return data(null); + } catch (err) { + return error(err); + } + } + + mapMentor(mentor) { + if (!mentor) return null; + + return { + id: mentor.id, + nama_depan: mentor.nama_depan, + nama_belakang: mentor.nama_belakang, + full_name: `${mentor.nama_depan}${mentor.nama_belakang ? ` ${mentor.nama_belakang}` : ''}`, + email: mentor.email, + role: mentor.role, + signature: mentor.signature || null, + profile_picture: mentor.profile_picture || null, + professional_bio: mentor.professional_bio || null, + created_at: mentor.created_at, + updated_at: mentor.updated_at, + }; + } + + buildMentorProfilePicturePath(file) { + if (!file) return null; + + return `/uploads/mentor-profiles/${file.filename}`; + } + + deleteLocalMentorProfilePicture(filePath) { + if (!filePath || typeof filePath !== 'string') { + return; + } + + if (!filePath.startsWith('/uploads/mentor-profiles/')) { + return; + } + + const filename = path.basename(filePath); + const absolutePath = path.join( + process.cwd(), + 'src', + 'uploads', + 'mentor-profiles', + filename + ); + + if (fs.existsSync(absolutePath)) { + fs.unlinkSync(absolutePath); + } + } } module.exports = new AdminService(); diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js index 9f5b324..3ae8d32 100755 --- a/src/routes/admin.routes.js +++ b/src/routes/admin.routes.js @@ -2,6 +2,7 @@ const express = require('express'); const { adminController } = require('../modules/admin'); const { verifyJWT } = require('../middleware/verifyJWT'); const isAdmin = require('../middleware/isAdmin'); +const { mentorProfileUpload, mentorProfileUploadErrorHandler } = require('../modules/admin/helpers/mentorProfileUpload.helper'); const router = express.Router(); @@ -27,6 +28,95 @@ const router = express.Router(); * role: * type: string * example: "admin" + * Mentor: + * type: object + * properties: + * id: + * type: integer + * example: 4 + * nama_depan: + * type: string + * example: Mentor + * nama_belakang: + * type: string + * example: Backend + * full_name: + * type: string + * example: Mentor Backend + * email: + * type: string + * example: mentor.backend@internify.com + * role: + * type: string + * example: mentor + * signature: + * type: string + * nullable: true + * example: Mentor-1718000000000 + * profile_picture: + * type: string + * nullable: true + * example: /uploads/mentor-profiles/1718000000000-profile.png + * professional_bio: + * type: string + * nullable: true + * example: Backend mentor for Internify LMS. + * created_at: + * type: string + * format: date-time + * example: 2026-06-25T10:00:00.000Z + * updated_at: + * type: string + * format: date-time + * example: 2026-06-25T10:00:00.000Z + * + * MentorResponse: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * $ref: '#/components/schemas/Mentor' + * message: + * type: string + * example: Mentor berhasil dibuat + * code: + * type: integer + * example: 201 + * + * MentorListResponse: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * $ref: '#/components/schemas/Mentor' + * message: + * type: string + * example: Mentor berhasil diambil + * code: + * type: integer + * example: 200 + * + * DeleteMentorResponse: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * nullable: true + * example: null + * message: + * type: string + * example: Mentor berhasil dihapus + * code: + * type: integer + * example: 200 */ /** @@ -88,4 +178,258 @@ router.post("/add", verifyJWT, isAdmin, adminController.addAdmin); */ router.get("/get", verifyJWT, adminController.getAdmins); -module.exports = router; \ No newline at end of file +/** + * @swagger + * /admin-api/mentors: + * post: + * summary: Create mentor account + * description: Create a new mentor account. Only admin can access this endpoint. Profile picture is optional. + * tags: [Admin - Mentor] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - nama_depan + * - nama_belakang + * - email + * - password + * properties: + * nama_depan: + * type: string + * example: Mentor + * nama_belakang: + * type: string + * example: Backend + * email: + * type: string + * format: email + * example: mentor.backend@internify.com + * password: + * type: string + * format: password + * example: password123 + * signature: + * type: string + * nullable: true + * example: Mentor-Backend-Signature + * professional_bio: + * type: string + * nullable: true + * example: Backend mentor for Internify LMS. + * profile_picture: + * type: string + * format: binary + * description: Optional mentor profile picture. Allowed types are JPEG, JPG, PNG, and WEBP. Max size 5MB. + * responses: + * 201: + * description: Mentor created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/MentorResponse' + * 400: + * description: Bad request or invalid uploaded file + * 401: + * description: Unauthorized + * 403: + * description: Forbidden. Only admin can create mentor accounts. + * 409: + * description: Email already registered + * 417: + * description: Validation error + * 500: + * description: Internal server error + */ +router.post( + '/mentors', + verifyJWT, + isAdmin, + mentorProfileUpload.single('profile_picture'), + mentorProfileUploadErrorHandler, + adminController.createMentor +); + +/** + * @swagger + * /admin-api/mentors: + * get: + * summary: Get mentor accounts + * description: Retrieve all mentor accounts. Only admin can access this endpoint. + * tags: [Admin - Mentor] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Mentors retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/MentorListResponse' + * 401: + * description: Unauthorized + * 403: + * description: Forbidden. Only admin can view mentor accounts. + * 500: + * description: Internal server error + */ +router.get('/mentors', verifyJWT, isAdmin, adminController.getMentors); + +/** + * @swagger + * /admin-api/mentors/{id}: + * get: + * summary: Get mentor detail + * description: Retrieve detail of a mentor account by ID. Only admin can access this endpoint. + * tags: [Admin - Mentor] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Mentor ID + * example: 4 + * responses: + * 200: + * description: Mentor detail retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/MentorResponse' + * 401: + * description: Unauthorized + * 403: + * description: Forbidden. Only admin can view mentor detail. + * 404: + * description: Mentor not found + * 500: + * description: Internal server error + */ +router.get('/mentors/:id', verifyJWT, isAdmin, adminController.getMentorById); + +/** + * @swagger + * /admin-api/mentors/{id}: + * patch: + * summary: Update mentor account + * description: Update mentor account data by ID. Only admin can access this endpoint. If password is provided, it will be hashed before saving. Profile picture is optional and replaces the old profile picture. + * tags: [Admin - Mentor] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Mentor ID + * example: 4 + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama_depan: + * type: string + * example: Mentor + * nama_belakang: + * type: string + * example: Backend Updated + * email: + * type: string + * format: email + * example: mentor.backend.updated@internify.com + * password: + * type: string + * format: password + * example: newpassword123 + * signature: + * type: string + * nullable: true + * example: Updated-Mentor-Signature + * professional_bio: + * type: string + * nullable: true + * example: Updated backend mentor bio. + * profile_picture: + * type: string + * format: binary + * description: Optional new mentor profile picture. Allowed types are JPEG, JPG, PNG, and WEBP. Max size 5MB. + * responses: + * 200: + * description: Mentor updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/MentorResponse' + * 400: + * description: Bad request or invalid uploaded file + * 401: + * description: Unauthorized + * 403: + * description: Forbidden. Only admin can update mentor accounts. + * 404: + * description: Mentor not found + * 409: + * description: Email already registered + * 417: + * description: Validation error + * 500: + * description: Internal server error + */ +router.patch( + '/mentors/:id', + verifyJWT, + isAdmin, + mentorProfileUpload.single('profile_picture'), + mentorProfileUploadErrorHandler, + adminController.updateMentor +); + +/** + * @swagger + * /admin-api/mentors/{id}: + * delete: + * summary: Delete mentor account + * description: Delete a mentor account by ID. Only admin can access this endpoint. Mentor cannot be deleted if they still own one or more projects. + * tags: [Admin - Mentor] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Mentor ID + * example: 4 + * responses: + * 200: + * description: Mentor deleted successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DeleteMentorResponse' + * 401: + * description: Unauthorized + * 403: + * description: Forbidden. Only admin can delete mentor accounts. + * 404: + * description: Mentor not found + * 409: + * description: Mentor cannot be deleted because they still own one or more projects + * 500: + * description: Internal server error + */ +router.delete('/mentors/:id', verifyJWT, isAdmin, adminController.deleteMentor); + +module.exports = router;