From 4a6292d1083890126a8d6c769beb9b725d62f86c Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 6 Jan 2026 14:12:21 +0530 Subject: [PATCH] Fixed the create activity content type json error --- .../OnBoarding/CreateNewActivity.ts | 293 +++--------------- .../host/handlers/mediaDeleteFromS3.ts | 89 +++++- .../host/handlers/mediaUploadForVenueToS3.ts | 17 +- src/modules/host/handlers/mediaUploadToS3.ts | 16 +- 4 files changed, 140 insertions(+), 275 deletions(-) diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts index ec23d26..1d840e4 100644 --- a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts @@ -1,7 +1,5 @@ import config from '../../../../../config/config'; import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; -import AWS from 'aws-sdk'; -import Busboy from 'busboy'; import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; @@ -13,295 +11,80 @@ import { import { HostService } from '../../../services/host.service'; const hostService = new HostService(prismaClient); -const s3 = new AWS.S3({ region: config.aws.region }); - -/* ------------------------------- Utilities ------------------------------- */ - -function getExtensionFromMime(mimeType: string) { - const map: Record = { - 'image/jpeg': 'jpg', - 'image/png': 'png', - 'image/webp': 'webp', - 'video/mp4': 'mp4', - 'video/quicktime': 'mov', - 'video/x-msvideo': 'avi', - 'video/x-matroska': 'mkv', - }; - return map[mimeType] || 'bin'; -} - -function normalizeJsonField(fields: any, key: string) { - if (!fields[key]) return undefined; - if (typeof fields[key] === 'object') return fields[key]; - - try { - return JSON.parse(fields[key]); - } catch { - throw new ApiError(400, `Invalid JSON in field: ${key}`); - } -} - -function sanitizeFileName(originalName: string): string { - const extIndex = originalName.lastIndexOf('.'); - const extension = - extIndex !== -1 ? originalName.slice(extIndex).toLowerCase() : ''; - - const baseName = - extIndex !== -1 ? originalName.slice(0, extIndex) : originalName; - - return ( - baseName - .trim() - .replace(/\s+/g, '_') // spaces → underscore - .replace(/[^a-zA-Z0-9_-]/g, '') // remove special chars - .toLowerCase() + - extension - ); -} - - -/* -------------------------------- Handler -------------------------------- */ export const handler = safeHandler( async (event: APIGatewayProxyEvent): Promise => { + /* 1️⃣ AUTH */ const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']; if (!token) { - throw new ApiError( - 401, - 'This is a protected route. Please provide a valid token.', - ); + throw new ApiError(401, 'Missing auth token'); } const userInfo = await verifyHostToken(token); - /* 2️⃣ CONTENT TYPE */ - const contentType = - event.headers['content-type'] || event.headers['Content-Type']; - if (!contentType?.includes('multipart/form-data')) { - throw new ApiError(400, 'Content-Type must be multipart/form-data'); + /* 2️⃣ PARSE JSON BODY */ + if (!event.body) { + throw new ApiError(400, 'Request body is required'); } - /* 3️⃣ BODY BUFFER */ - const bodyBuffer = event.isBase64Encoded - ? Buffer.from(event.body as string, 'base64') - : Buffer.from(event.body as string); + let body: any; + try { + body = JSON.parse(event.body); + } catch { + throw new ApiError(400, 'Invalid JSON body'); + } - const fields: Record = {}; - const files: Array<{ - buffer: Buffer; - mimeType: string; - fileName: string; - fieldName: string; - }> = []; + const { + activity, + media = [], + isDraft = false, + } = body; - await new Promise((resolve, reject) => { - const bb = Busboy({ - headers: { - ...event.headers, - 'content-type': contentType, - }, - }); - - bb.on('field', (name, value) => { - fields[name] = value; - }); - - bb.on('file', (fieldName, file, info) => { - const { filename, mimeType } = info; - const chunks: Buffer[] = []; - let size = 0; - const MAX_SIZE = 5 * 1024 * 1024; - - file.on('data', (chunk) => { - size += chunk.length; - if (size > MAX_SIZE) { - file.destroy(new Error('File exceeds 5MB limit')); - return; - } - chunks.push(chunk); - }); - - file.on('end', () => { - if (chunks.length > 0) { - files.push({ - buffer: Buffer.concat(chunks), - mimeType: mimeType || 'application/octet-stream', - fileName: sanitizeFileName(filename || 'unknown'), - fieldName, - }); - } - }); - }); - - bb.on('finish', () => resolve()); - bb.on('error', (err) => reject(new ApiError(400, err.message))); - - bb.end(bodyBuffer); - }); - - /* 4️⃣ FLAGS */ - const isDraft = fields.isDraft === 'true' || fields.isDraft === true; - - /* 5️⃣ ACTIVITY PAYLOAD */ - const activityPayload: any = normalizeJsonField(fields, 'activity'); - if (!activityPayload) { + if (!activity) { throw new ApiError(400, 'activity payload is required'); } - /* 6️⃣ NORMALIZE IDS */ - if (activityPayload.activityXid) { - activityPayload.activityXid = Number(activityPayload.activityXid); + /* 3️⃣ NORMALIZE ACTIVITY ID */ + if (activity.activityXid) { + activity.activityXid = Number(activity.activityXid); } - const numberKeys = [ - 'currencyXid', - 'energyLevelXid', - 'activityDurationMins', - 'activityTypeXid', - 'frequenciesXid', - 'trainerTotalAmount', - 'pickupDropTotalPrice', - 'navigationModeTotalPrice', - 'sustainabilityScore', - 'safetyScore', - 'checkInLat', - 'checkInLong', - 'checkOutLat', - 'checkOutLong', - ]; - - for (const key of numberKeys) { - if (activityPayload[key] !== undefined && activityPayload[key] !== null && activityPayload[key] !== '') { - activityPayload[key] = Number(activityPayload[key]); - } + /* 4️⃣ ATTACH ACTIVITY MEDIA (S3 URLs) */ + if (!Array.isArray(media)) { + throw new ApiError(400, 'media must be an array'); } - /* 7️⃣ NORMALIZE BOOLEANS */ - const booleanKeys = [ - 'isInstantBooking', - 'foodAvailable', - 'foodIsChargeable', - 'alcoholAvailable', - 'trainerAvailable', - 'trainerIsChargeable', - 'pickUpDropAvailable', - 'pickUpDropIsChargeable', - 'inActivityAvailable', - 'inActivityIsChargeable', - 'equipmentAvailable', - 'equipmentIsChargeable', - 'cancellationAvailable', - 'isCheckOutSame', - ]; + activity.media = media.map((m: any) => ({ + mediaType: m.mediaType ?? 'image', + mediaFileName: m.mediaFileName, + })); - for (const key of booleanKeys) { - if (activityPayload[key] === 'true') activityPayload[key] = true; - if (activityPayload[key] === 'false') activityPayload[key] = false; - } - - /* 8️⃣ UPLOAD ACTIVITY-LEVEL MEDIA (images/videos) */ - const uploadedActivityMedia: Array<{ mediaType?: string; mediaFileName: string }> = []; - - for (const file of files.filter( - (f) => f.fieldName === 'images' || f.fieldName === 'videos' - )) { - const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Artifacts/${file.fileName}`; - - await s3.upload({ - Bucket: config.aws.bucketName, - Key: s3Key, - Body: file.buffer, - ContentType: file.mimeType, - ACL: 'private', - }).promise(); - - uploadedActivityMedia.push({ - mediaType: file.mimeType, - mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`, - }); - } - - - /* 🔥 MERGE ACTIVITY MEDIA */ - const existingMedia = Array.isArray(activityPayload.media) - ? activityPayload.media - : []; - activityPayload.media = [...existingMedia, ...uploadedActivityMedia]; - - /* 9️⃣ PROCESS VENUE MEDIA UPLOADS */ - // Group venue files by index: venueImages[0], venueImages[1], etc. - const venueFilesMap = new Map(); - - for (const file of files) { - const match = file.fieldName.match(/^venueFiles(\d+)$/); - if (!match) continue; - - const venueIndex = Number(match[1]); - - if (!venueFilesMap.has(venueIndex)) { - venueFilesMap.set(venueIndex, []); - } - - venueFilesMap.get(venueIndex)!.push(file); - } - - - // Upload venue files and attach to corresponding venues - if (Array.isArray(activityPayload.venues)) { - for (let i = 0; i < activityPayload.venues.length; i++) { - const venue = activityPayload.venues[i]; - const venueFiles = venueFilesMap.get(i) || []; - - const uploadedVenueMedia = []; - - for (const file of venueFiles) { - const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Venues/Venue_${i}/${file.fileName}`; - - await s3.upload({ - Bucket: config.aws.bucketName, - Key: s3Key, - Body: file.buffer, - ContentType: file.mimeType, - ACL: 'private', - }).promise(); - - uploadedVenueMedia.push({ - mediaType: file.mimeType, - mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`, - }); - } - - venue.media = [...(venue.media || []), ...uploadedVenueMedia]; - } - } - - - /* 🔟 VALIDATION */ + /* 5️⃣ VALIDATION */ let parsedDto: CreateActivityInput; if (!isDraft) { - const parsed = CreateActivityDto.safeParse(activityPayload); + const parsed = CreateActivityDto.safeParse(activity); if (!parsed.success) { throw new ApiError( 400, - parsed.error.issues.map((i) => i.message).join(', '), + parsed.error.issues.map((i) => i.message).join(', ') ); } parsedDto = parsed.data; } else { - parsedDto = activityPayload as CreateActivityInput; + parsedDto = activity as CreateActivityInput; } - /* 1️⃣1️⃣ SAVE ACTIVITY */ - const createdActivity = await hostService.createOrUpdateActivity( + /* 6️⃣ SAVE TO DB */ + const result = await hostService.createOrUpdateActivity( userInfo.id, parsedDto, - isDraft, + isDraft ); - /* 1️⃣2️⃣ RESPONSE */ + /* 7️⃣ RESPONSE */ return { statusCode: 200, headers: { @@ -312,9 +95,9 @@ export const handler = safeHandler( success: true, message: isDraft ? 'Activity saved as draft successfully' - : 'Activity created successfully', - data: createdActivity, + : 'Activity submitted successfully', + data: result, }), }; - }, + } ); \ No newline at end of file diff --git a/src/modules/host/handlers/mediaDeleteFromS3.ts b/src/modules/host/handlers/mediaDeleteFromS3.ts index fe2e305..71534ea 100644 --- a/src/modules/host/handlers/mediaDeleteFromS3.ts +++ b/src/modules/host/handlers/mediaDeleteFromS3.ts @@ -1,30 +1,97 @@ -// mediaDelete.ts import { APIGatewayProxyHandler } from 'aws-lambda'; import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3'; import config from '../../../config/config'; +import ApiError from '../../../common/utils/helper/ApiError'; +import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost'; +import { prismaClient } from '../../../common/database/prisma.lambda.service'; -const s3 = new S3Client({ region: config.aws.region, }); +const s3 = new S3Client({ region: config.aws.region }); + +function extractS3Key(input: string): string { + if (input.startsWith('s3://')) { + return input.replace(`s3://${config.aws.bucketName}/`, ''); + } + + if (input.startsWith('https://')) { + const url = new URL(input); + return url.pathname.replace(/^\/+/, ''); + } + + return input; +} export const handler: APIGatewayProxyHandler = async (event) => { try { - const body = JSON.parse(event.body || '{}'); - const { key } = body; + /* ---------------- AUTH ---------------- */ + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']; + if (!token) throw new ApiError(401, 'Missing token.'); + await verifyHostToken(token); + + /* ---------------- BODY ---------------- */ + const body = JSON.parse(event.body || '{}'); + const { key, mediaSource, mediaId } = body; + + if (mediaSource && mediaId) { + + if (!['ACTIVITY', 'VENUE'].includes(mediaSource)) { + throw new ApiError(400, 'Invalid mediaSource'); + } + + /* ---------------- DB DELETE ---------------- */ + if (mediaSource === 'ACTIVITY') { + const media = await prismaClient.activitiesMedia.findUnique({ + where: { id: Number(mediaId) }, + }); + + if (!media) throw new ApiError(404, 'Activity media not found'); + + await prismaClient.activitiesMedia.delete({ + where: { id: media.id }, + }); + } + + if (mediaSource === 'VENUE') { + const media = await prismaClient.activityVenueArtifacts.findUnique({ + where: { id: Number(mediaId) }, + }); + + if (!media) throw new ApiError(404, 'Venue media not found'); + + await prismaClient.activityVenueArtifacts.delete({ + where: { id: media.id }, + }); + } - if (!key) { - return response(400, 'S3 key is required'); } + const s3Key = extractS3Key(key); + + /* ---------------- PATH SAFETY ---------------- */ + const allowedPrefixes = ['ActivityOnboarding/']; + if (!allowedPrefixes.some((p) => s3Key.startsWith(p))) { + throw new ApiError(403, 'Unauthorized delete path'); + } + + /* ---------------- S3 DELETE ---------------- */ await s3.send( new DeleteObjectCommand({ Bucket: config.aws.bucketName!, - Key: key, - }) + Key: s3Key, + }), ); - return response(200, { success: true }); + return response(200, { + success: true, + message: 'Media deleted from DB and S3 successfully', + }); } catch (err: any) { - console.error(err); - return response(500, 'Failed to delete file'); + console.error('ERROR:', err); + + if (err instanceof ApiError) { + return response(err.statusCode, err.message); + } + + return response(500, 'Internal server error'); } }; diff --git a/src/modules/host/handlers/mediaUploadForVenueToS3.ts b/src/modules/host/handlers/mediaUploadForVenueToS3.ts index 4fba408..87568cb 100644 --- a/src/modules/host/handlers/mediaUploadForVenueToS3.ts +++ b/src/modules/host/handlers/mediaUploadForVenueToS3.ts @@ -18,7 +18,6 @@ export const handler: APIGatewayProxyHandler = async (event) => { if (!token) throw new ApiError(401, 'Missing token.'); await verifyHostToken(token); - const body = JSON.parse(event.body || '{}'); const { files, venueTempId } = body; @@ -35,7 +34,7 @@ export const handler: APIGatewayProxyHandler = async (event) => { throw new ApiError(400, 'activityXid is required in path parameters'); } - const activityDetails = await hostService.getActivityDetailsById(activityXid); + const activityDetails = await hostService.getActivityDetailsById(Number(activityXid)); if (!activityDetails) { throw new ApiError(404, 'Activity not found'); } @@ -79,10 +78,18 @@ export const handler: APIGatewayProxyHandler = async (event) => { files: results, }); - } catch (err) { - console.error(err); - return response(500, 'Failed to generate venue presigned URLs'); + } catch (err: any) { + console.error('ERROR:', err); + + // If it's your ApiError, return its status & message + if (err instanceof ApiError) { + return response(err.statusCode, err.message); + } + + // Fallback for unknown errors + return response(500, 'Internal server error'); } + }; function response(statusCode: number, body: any) { diff --git a/src/modules/host/handlers/mediaUploadToS3.ts b/src/modules/host/handlers/mediaUploadToS3.ts index 7d6396f..e03e324 100644 --- a/src/modules/host/handlers/mediaUploadToS3.ts +++ b/src/modules/host/handlers/mediaUploadToS3.ts @@ -31,7 +31,7 @@ export const handler: APIGatewayProxyHandler = async (event) => { throw new ApiError(400, 'activityXid is required in path parameters'); } - const activityDetails = await hostService.getActivityDetailsById(activityXid); + const activityDetails = await hostService.getActivityDetailsById(Number(activityXid)); if (!activityDetails) { throw new ApiError(404, 'Activity not found'); } @@ -72,10 +72,18 @@ export const handler: APIGatewayProxyHandler = async (event) => { return response(200, { files: results }); - } catch (err) { - console.error(err); - return response(500, 'Failed to generate presigned URLs'); + } catch (err: any) { + console.error('ERROR:', err); + + // If it's your ApiError, return its status & message + if (err instanceof ApiError) { + return response(err.statusCode, err.message); + } + + // Fallback for unknown errors + return response(500, 'Internal server error'); } + }; function response(statusCode: number, body: any) {