Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1

This commit is contained in:
paritosh18
2026-01-07 16:00:28 +05:30
5 changed files with 143 additions and 281 deletions

View File

@@ -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<string, string> = {
'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<APIGatewayProxyResult> => {
/* 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);
/* 2CONTENT 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');
/* 2PARSE 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<string, any> = {};
const files: Array<{
buffer: Buffer;
mimeType: string;
fileName: string;
fieldName: string;
}> = [];
const {
activity,
media = [],
isDraft = false,
} = body;
await new Promise<void>((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<number, typeof files>();
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,
}),
};
},
}
);

View File

@@ -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');
}
};

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -2412,9 +2412,6 @@ export class HostService {
const durationHours = Number(payload.durationHours ?? 0);
const durationMins = Number(payload.durationMins ?? 0);
const activityDurationMins =
durationDays * 24 * 60 + durationHours * 60 + durationMins;
/* =====================================================
* BASIC GUARDS
* ===================================================== */
@@ -2457,8 +2454,8 @@ export class HostService {
}
if (
activityDurationMins > 0 &&
payload.cancellationAllowedBeforeMins >= activityDurationMins
durationMins > 0 &&
payload.cancellationAllowedBeforeMins >= durationMins
) {
throw new ApiError(
400,
@@ -2637,7 +2634,7 @@ export class HostService {
checkOutAddress: payload.checkOutAddress ?? undefined,
// energyLevelXid: payload.energyLevelXid ?? undefined,
activityDurationMins: activityDurationMins ?? undefined,
activityDurationMins: durationMins ?? undefined,
currencyXid: payload.currencyXid ?? undefined,
sustainabilityScore: payload.sustainabilityScore ?? undefined,