From a21e647d25b35a6d1496f4ee342a862b2dac3e43 Mon Sep 17 00:00:00 2001 From: Disura Randunu Date: Mon, 16 Sep 2024 17:39:15 +0530 Subject: [PATCH 1/3] Admin API for Mentor Details Update --- src/controllers/admin/mentor.controller.ts | 40 +++++++ src/middlewares/requestValidator.ts | 6 +- src/routes/admin/mentor/mentor.route.ts | 10 +- .../admin/admin.mentor-routes.schema.ts | 9 ++ src/services/admin/mentor.service.ts | 104 +++++++++++++++++- 5 files changed, 166 insertions(+), 3 deletions(-) diff --git a/src/controllers/admin/mentor.controller.ts b/src/controllers/admin/mentor.controller.ts index 18c5f0c..1732f2b 100644 --- a/src/controllers/admin/mentor.controller.ts +++ b/src/controllers/admin/mentor.controller.ts @@ -6,6 +6,7 @@ import { findAllMentorEmails, getAllMentors, getMentor, + updateMentorDetails, updateMentorStatus } from '../../services/admin/mentor.service' import { @@ -13,6 +14,45 @@ import { updateAvailability } from '../../services/mentor.service' import type { ApiResponse, PaginatedApiResponse } from '../../types' +import { IMG_HOST } from '../../configs/envConfig' + +export const updateMentorHandler = async ( + req: Request, + res: Response +): Promise> => { + const user = req.user as Profile + + if (user.type !== ProfileTypes.ADMIN) { + return res.status(403).json({ message: 'Only Admins are allowed' }) + } + + try { + const { mentorId } = req.params + const data = req.body.data ? JSON.parse(req.body.data) : req.body + const mentorUpdateData: Partial = { ...data } + const profileUpdateData: Partial = { ...data.profile } + + if (req.file) { + profileUpdateData.image_url = `${IMG_HOST}/${req.file.filename}` + } + + const { mentor, statusCode, message } = await updateMentorDetails( + mentorId, + mentorUpdateData, + profileUpdateData + ) + return res.status(statusCode).json({ mentor, message }) + } catch (err) { + if (err instanceof Error) { + console.error('Error updating mentor details:', err) + return res.status(500).json({ + error: 'Internal server error', + message: err.message + }) + } + throw err + } +} export const mentorStatusHandler = async ( req: Request, diff --git a/src/middlewares/requestValidator.ts b/src/middlewares/requestValidator.ts index 6545928..7dd160d 100644 --- a/src/middlewares/requestValidator.ts +++ b/src/middlewares/requestValidator.ts @@ -4,7 +4,11 @@ import { ZodError, type ZodSchema } from 'zod' export const requestBodyValidator = (schema: T) => { return (req: Request, res: Response, next: NextFunction) => { try { - schema.parse(req.body) + if (req.body.data) { + schema.parse(JSON.parse(req.body.data)) + } else { + schema.parse(req.body) + } next() } catch (err) { console.log(err) diff --git a/src/routes/admin/mentor/mentor.route.ts b/src/routes/admin/mentor/mentor.route.ts index a312299..c30ea74 100644 --- a/src/routes/admin/mentor/mentor.route.ts +++ b/src/routes/admin/mentor/mentor.route.ts @@ -5,7 +5,8 @@ import { mentorDetailsHandler, mentorStatusHandler, searchMentors, - updateMentorAvailability + updateMentorAvailability, + updateMentorHandler } from '../../../controllers/admin/mentor.controller' import { requireAuth } from '../../../controllers/auth.controller' import { @@ -16,13 +17,20 @@ import { getAllMentorEmailsSchema, getAllMentorsByStatusSchema, mentorStatusSchema, + mentorUpdateSchema, searchMentorsSchema, updateMentorAvailabilitySchema } from '../../../schemas/admin/admin.mentor-routes.schema' import { paginationSchema } from '../../../schemas/common/pagination-request.schema' +import { upload } from '../../../utils' const mentorRouter = express.Router() +mentorRouter.put( + '/:mentorId', + [requireAuth, upload, requestBodyValidator(mentorUpdateSchema)], + updateMentorHandler +) mentorRouter.put( '/:mentorId/state', [requireAuth, requestBodyValidator(mentorStatusSchema)], diff --git a/src/schemas/admin/admin.mentor-routes.schema.ts b/src/schemas/admin/admin.mentor-routes.schema.ts index 64006cf..6ffe959 100644 --- a/src/schemas/admin/admin.mentor-routes.schema.ts +++ b/src/schemas/admin/admin.mentor-routes.schema.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { MentorApplicationStatus } from '../../enums' +import { updateProfileSchema } from '../profile-routes.schema' export const mentorStatusSchema = z.object({ state: z.nativeEnum(MentorApplicationStatus) @@ -20,3 +21,11 @@ export const updateMentorAvailabilitySchema = z.object({ export const searchMentorsSchema = z.object({ q: z.string().or(z.undefined()) }) + +export const mentorUpdateSchema = z.object({ + availability: z.boolean().optional(), + application: z.record(z.string(), z.any()).optional(), + state: z.nativeEnum(MentorApplicationStatus).optional(), + category: z.string().uuid().optional(), + profile: updateProfileSchema.optional() +}) diff --git a/src/services/admin/mentor.service.ts b/src/services/admin/mentor.service.ts index aa062fb..0a5989d 100644 --- a/src/services/admin/mentor.service.ts +++ b/src/services/admin/mentor.service.ts @@ -1,7 +1,9 @@ import { dataSource } from '../../configs/dbConfig' +import Category from '../../entities/category.entity' import Mentor from '../../entities/mentor.entity' +import Profile from '../../entities/profile.entity' import type { MentorApplicationStatus } from '../../enums' -import { type PaginatedApiResponse } from '../../types' +import { type CreateProfile, type PaginatedApiResponse } from '../../types' import { getEmailContent } from '../../utils' import { sendEmail } from './email.service' @@ -54,6 +56,106 @@ export const updateMentorStatus = async ( } } +export const updateMentorDetails = async ( + mentorId: string, + mentorData: Partial, + profileData?: Partial +): Promise<{ + statusCode: number + mentor?: Mentor | null + message: string +}> => { + try { + const mentorRepository = dataSource.getRepository(Mentor) + const profileRepository = dataSource.getRepository(Profile) + const categoryRepository = dataSource.getRepository(Category) + + const mentor = await mentorRepository.findOne({ + where: { uuid: mentorId }, + relations: ['profile'] + }) + + if (!mentor) { + return { + statusCode: 404, + message: 'Mentor not found' + } + } + + if (mentorData.availability !== undefined) { + mentor.availability = mentorData.availability + } + + if (mentorData.state) { + mentor.state = mentorData.state + } + + if (mentorData.category) { + if (typeof mentorData.category === 'string') { + const category = await categoryRepository.findOne({ + where: { uuid: mentorData.category } + }) + + if (!category) { + return { + statusCode: 404, + message: 'Category not found' + } + } + mentor.category = category + } + } + + // will override values of keys if exisitng keys provided. add new key-value pairs if not exists + if (mentorData.application) { + mentor.application = { + ...mentor.application, + ...mentorData.application + } + } + + await mentorRepository.save(mentor) + + if (profileData && mentor.profile) { + const updatedProfileData: Partial = {} + + if (profileData.primary_email) { + updatedProfileData.primary_email = profileData.primary_email + } + if (profileData.first_name) { + updatedProfileData.first_name = profileData.first_name + } + if (profileData.last_name) { + updatedProfileData.last_name = profileData.last_name + } + if (profileData.image_url) { + updatedProfileData.image_url = profileData.image_url + } + + if (Object.keys(updatedProfileData).length > 0) { + await profileRepository.update( + { uuid: mentor.profile.uuid }, + updatedProfileData as CreateProfile + ) + } + } + + const updatedMentor = await mentorRepository.findOne({ + where: { uuid: mentorId }, + relations: ['profile', 'category'] + }) + + return { + statusCode: 200, + mentor: updatedMentor, + message: 'Updated Mentor details successfully' + } + } catch (err) { + console.error('Error updating the mentor details', err) + throw new Error('Error updating the mentor details') + } +} + export const getAllMentors = async ({ status, pageNumber, From 0b8299a5ce5e95bb9892743e33fa2d4e04157eb9 Mon Sep 17 00:00:00 2001 From: Disura Randunu Date: Mon, 16 Sep 2024 17:54:01 +0530 Subject: [PATCH 2/3] Removed state updating feature --- src/schemas/admin/admin.mentor-routes.schema.ts | 1 - src/services/admin/mentor.service.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/schemas/admin/admin.mentor-routes.schema.ts b/src/schemas/admin/admin.mentor-routes.schema.ts index 6ffe959..46de531 100644 --- a/src/schemas/admin/admin.mentor-routes.schema.ts +++ b/src/schemas/admin/admin.mentor-routes.schema.ts @@ -25,7 +25,6 @@ export const searchMentorsSchema = z.object({ export const mentorUpdateSchema = z.object({ availability: z.boolean().optional(), application: z.record(z.string(), z.any()).optional(), - state: z.nativeEnum(MentorApplicationStatus).optional(), category: z.string().uuid().optional(), profile: updateProfileSchema.optional() }) diff --git a/src/services/admin/mentor.service.ts b/src/services/admin/mentor.service.ts index 0a5989d..bd2fc85 100644 --- a/src/services/admin/mentor.service.ts +++ b/src/services/admin/mentor.service.ts @@ -86,10 +86,6 @@ export const updateMentorDetails = async ( mentor.availability = mentorData.availability } - if (mentorData.state) { - mentor.state = mentorData.state - } - if (mentorData.category) { if (typeof mentorData.category === 'string') { const category = await categoryRepository.findOne({ From 60bcc8d45a008df761b4bcff2afb166eafd8d76a Mon Sep 17 00:00:00 2001 From: Disura Randunu Date: Mon, 16 Sep 2024 19:19:48 +0530 Subject: [PATCH 3/3] Moved validations insdie controller. Created util to format errors --- src/controllers/admin/mentor.controller.ts | 24 +++++++++++++++++++++- src/routes/admin/mentor/mentor.route.ts | 8 +------- src/utils.ts | 9 ++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/controllers/admin/mentor.controller.ts b/src/controllers/admin/mentor.controller.ts index 1732f2b..b0a2b26 100644 --- a/src/controllers/admin/mentor.controller.ts +++ b/src/controllers/admin/mentor.controller.ts @@ -15,6 +15,8 @@ import { } from '../../services/mentor.service' import type { ApiResponse, PaginatedApiResponse } from '../../types' import { IMG_HOST } from '../../configs/envConfig' +import { formatValidationErrors, upload } from '../../utils' +import { mentorUpdateSchema } from '../../schemas/admin/admin.mentor-routes.schema' export const updateMentorHandler = async ( req: Request, @@ -27,8 +29,26 @@ export const updateMentorHandler = async ( } try { - const { mentorId } = req.params + await new Promise((resolve, reject) => { + upload(req, res, (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + const data = req.body.data ? JSON.parse(req.body.data) : req.body + + const result = mentorUpdateSchema.safeParse(data) + if (!result.success) { + return res.status(400).json({ + error: 'Invalid data', + details: formatValidationErrors(result.error) + }) + } + const mentorUpdateData: Partial = { ...data } const profileUpdateData: Partial = { ...data.profile } @@ -36,6 +56,8 @@ export const updateMentorHandler = async ( profileUpdateData.image_url = `${IMG_HOST}/${req.file.filename}` } + const { mentorId } = req.params + const { mentor, statusCode, message } = await updateMentorDetails( mentorId, mentorUpdateData, diff --git a/src/routes/admin/mentor/mentor.route.ts b/src/routes/admin/mentor/mentor.route.ts index c30ea74..98902ef 100644 --- a/src/routes/admin/mentor/mentor.route.ts +++ b/src/routes/admin/mentor/mentor.route.ts @@ -17,20 +17,14 @@ import { getAllMentorEmailsSchema, getAllMentorsByStatusSchema, mentorStatusSchema, - mentorUpdateSchema, searchMentorsSchema, updateMentorAvailabilitySchema } from '../../../schemas/admin/admin.mentor-routes.schema' import { paginationSchema } from '../../../schemas/common/pagination-request.schema' -import { upload } from '../../../utils' const mentorRouter = express.Router() -mentorRouter.put( - '/:mentorId', - [requireAuth, upload, requestBodyValidator(mentorUpdateSchema)], - updateMentorHandler -) +mentorRouter.put('/:mentorId', requireAuth, updateMentorHandler) mentorRouter.put( '/:mentorId/state', [requireAuth, requestBodyValidator(mentorStatusSchema)], diff --git a/src/utils.ts b/src/utils.ts index 839e423..51d1a0f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,6 +10,7 @@ import { generateCertificate } from './services/admin/generateCertificate' import { randomUUID } from 'crypto' import { certificatesDir } from './app' import type Mentee from './entities/mentee.entity' +import { type ZodError } from 'zod' export const signAndSetCookie = (res: Response, uuid: string): void => { const token = jwt.sign({ userId: uuid }, JWT_SECRET ?? '') @@ -290,3 +291,11 @@ export const getPasswordChangedEmailContent = ( export const capitalizeFirstLetter = (word: string): string => { return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() } + +export const formatValidationErrors = ( + err: ZodError +): Array<{ message: string }> => { + return err.errors.map((issue) => ({ + message: `${issue.path.join('.')} is ${issue.message}` + })) +}