feat: menambahkan manajemen akun mentor oleh admin

This commit is contained in:
Muhammad Zhafran Ilham
2026-06-25 22:58:04 +07:00
parent 6a1a3c1a97
commit e8a7521bb0
7 changed files with 881 additions and 3 deletions
@@ -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();
@@ -0,0 +1,9 @@
const cleanupUploadedFile = (file) => {
if (file?.path && fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
};
module.exports = {
cleanupUploadedFile,
};
@@ -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,
};
+74
View File
@@ -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,
};
@@ -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();
+174 -1
View File
@@ -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();