Merge pull request #17 from internship-humic/feat/managemen-mentor
feat: menambahkan manajemen akun mentor oleh admin
This commit is contained in:
@@ -3,7 +3,9 @@ const { response, data, error } = require('../../../helpers/utils/wrapper');
|
|||||||
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||||
const { ForbiddenError, BadRequestError, NotFoundError, InternalServerError } = require('../../../helpers/error');
|
const { ForbiddenError, BadRequestError, NotFoundError, InternalServerError } = require('../../../helpers/error');
|
||||||
const { isValidPayload } = require('../../../helpers/utils/validator');
|
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 {
|
class AdminController {
|
||||||
async addAdmin(req, res) {
|
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);
|
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();
|
module.exports = new AdminController();
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const cleanupUploadedFile = (file) => {
|
||||||
|
if (file?.path && fs.existsSync(file.path)) {
|
||||||
|
fs.unlinkSync(file.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
cleanupUploadedFile,
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
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: 2 * 1024 * 1024, //2MB
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mentorProfileUploadErrorHandler = (err, req, res, next) => {
|
||||||
|
if (!err) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof multer.MulterError) {
|
||||||
|
const message = err.code === 'LIMIT_FILE_SIZE'
|
||||||
|
? 'Profile picture maksimal 2MB'
|
||||||
|
: err.message;
|
||||||
|
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new BadRequestError(message)),
|
||||||
|
message,
|
||||||
|
ERROR.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(err),
|
||||||
|
err.message || 'Invalid profile picture upload',
|
||||||
|
ERROR.BAD_REQUEST
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mentorProfileUpload,
|
||||||
|
mentorProfileUploadErrorHandler,
|
||||||
|
mentorProfileUploadDir,
|
||||||
|
};
|
||||||
@@ -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 = {
|
module.exports = {
|
||||||
createAdminModel,
|
createAdminModel,
|
||||||
updateAdminModel,
|
updateAdminModel,
|
||||||
|
createMentorModel,
|
||||||
|
updateMentorModel,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,57 @@ class AdminRepository {
|
|||||||
where: { id: parseInt(id) }
|
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();
|
module.exports = new AdminRepository();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const adminRepository = require('../repositories/admin.repository');
|
const adminRepository = require('../repositories/admin.repository');
|
||||||
const { data, error } = require('../../../helpers/utils/wrapper');
|
const { data, error } = require('../../../helpers/utils/wrapper');
|
||||||
const { BadRequestError, NotFoundError } = require('../../../helpers/error');
|
const { BadRequestError, NotFoundError, ConflictError } = require('../../../helpers/error');
|
||||||
|
|
||||||
class AdminService {
|
class AdminService {
|
||||||
async createAdmin(adminData) {
|
async createAdmin(adminData) {
|
||||||
@@ -109,6 +111,177 @@ class AdminService {
|
|||||||
return error(err);
|
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();
|
module.exports = new AdminService();
|
||||||
|
|||||||
+345
-1
@@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const { adminController } = require('../modules/admin');
|
const { adminController } = require('../modules/admin');
|
||||||
const { verifyJWT } = require('../middleware/verifyJWT');
|
const { verifyJWT } = require('../middleware/verifyJWT');
|
||||||
const isAdmin = require('../middleware/isAdmin');
|
const isAdmin = require('../middleware/isAdmin');
|
||||||
|
const { mentorProfileUpload, mentorProfileUploadErrorHandler } = require('../modules/admin/helpers/mentorProfileUpload.helper');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -27,6 +28,95 @@ const router = express.Router();
|
|||||||
* role:
|
* role:
|
||||||
* type: string
|
* type: string
|
||||||
* example: "admin"
|
* 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);
|
router.get("/get", verifyJWT, adminController.getAdmins);
|
||||||
|
|
||||||
module.exports = router;
|
/**
|
||||||
|
* @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 2MB.
|
||||||
|
* 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 2MB.
|
||||||
|
* 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user