banatie-service/src/middleware/validation.ts

246 lines
6.9 KiB
TypeScript

import { Response, NextFunction } from "express";
// Validation rules
const VALIDATION_RULES = {
prompt: {
minLength: 3,
maxLength: 2000,
required: true,
},
filename: {
minLength: 1,
maxLength: 100,
required: true,
pattern: /^[a-zA-Z0-9_-]+$/, // Only alphanumeric, underscore, hyphen
},
};
/**
* Sanitize filename to prevent directory traversal and invalid characters
*/
export const sanitizeFilename = (filename: string): string => {
return filename
.replace(/[^a-zA-Z0-9_-]/g, "_") // Replace invalid chars with underscore
.replace(/_{2,}/g, "_") // Replace multiple underscores with single
.replace(/^_+|_+$/g, "") // Remove leading/trailing underscores
.substring(0, 100); // Limit length
};
/**
* Validate the generate image request
*/
export const validateGenerateRequest = (
req: any,
res: Response,
next: NextFunction,
): void | Response => {
const timestamp = new Date().toISOString();
const { prompt, filename, autoEnhance, enhancementOptions } = req.body;
const errors: string[] = [];
console.log(`[${timestamp}] [${req.requestId}] Validating generate request`);
// Validate prompt
if (!prompt) {
errors.push("Prompt is required");
} else if (typeof prompt !== "string") {
errors.push("Prompt must be a string");
} else if (prompt.trim().length < VALIDATION_RULES.prompt.minLength) {
errors.push(
`Prompt must be at least ${VALIDATION_RULES.prompt.minLength} characters`,
);
} else if (prompt.length > VALIDATION_RULES.prompt.maxLength) {
errors.push(
`Prompt must be less than ${VALIDATION_RULES.prompt.maxLength} characters`,
);
}
// Validate filename
if (!filename) {
errors.push("Filename is required");
} else if (typeof filename !== "string") {
errors.push("Filename must be a string");
} else if (filename.trim().length < VALIDATION_RULES.filename.minLength) {
errors.push("Filename cannot be empty");
} else if (filename.length > VALIDATION_RULES.filename.maxLength) {
errors.push(
`Filename must be less than ${VALIDATION_RULES.filename.maxLength} characters`,
);
} else if (!VALIDATION_RULES.filename.pattern.test(filename)) {
errors.push(
"Filename can only contain letters, numbers, underscores, and hyphens",
);
}
// Validate autoEnhance (optional boolean)
if (autoEnhance !== undefined && typeof autoEnhance !== "boolean") {
errors.push("autoEnhance must be a boolean");
}
// Validate enhancementOptions (optional object)
if (enhancementOptions !== undefined) {
if (
typeof enhancementOptions !== "object" ||
Array.isArray(enhancementOptions)
) {
errors.push("enhancementOptions must be an object");
} else {
const {
imageStyle,
aspectRatio,
mood,
lighting,
cameraAngle,
negativePrompts,
} = enhancementOptions;
if (
imageStyle !== undefined &&
![
"photorealistic",
"illustration",
"minimalist",
"sticker",
"product",
"comic",
].includes(imageStyle)
) {
errors.push("Invalid imageStyle in enhancementOptions");
}
if (
aspectRatio !== undefined &&
!["square", "portrait", "landscape", "wide", "ultrawide"].includes(
aspectRatio,
)
) {
errors.push("Invalid aspectRatio in enhancementOptions");
}
if (
mood !== undefined &&
(typeof mood !== "string" || mood.length > 100)
) {
errors.push("mood must be a string with max 100 characters");
}
if (
lighting !== undefined &&
(typeof lighting !== "string" || lighting.length > 100)
) {
errors.push("lighting must be a string with max 100 characters");
}
if (
cameraAngle !== undefined &&
(typeof cameraAngle !== "string" || cameraAngle.length > 100)
) {
errors.push("cameraAngle must be a string with max 100 characters");
}
if (negativePrompts !== undefined) {
if (!Array.isArray(negativePrompts) || negativePrompts.length > 10) {
errors.push("negativePrompts must be an array with max 10 items");
} else {
for (const item of negativePrompts) {
if (typeof item !== "string" || item.length > 100) {
errors.push(
"Each negative prompt must be a string with max 100 characters",
);
break;
}
}
}
}
}
}
// Check for XSS attempts in prompt
const xssPatterns = [
/<script/i,
/javascript:/i,
/on\w+\s*=/i,
/<iframe/i,
/<object/i,
/<embed/i,
];
if (prompt && xssPatterns.some((pattern) => pattern.test(prompt))) {
errors.push("Invalid characters detected in prompt");
}
// Log validation results
if (errors.length > 0) {
console.log(
`[${timestamp}] [${req.requestId}] Validation failed: ${errors.join(", ")}`,
);
return res.status(400).json({
success: false,
error: "Validation failed",
message: errors.join(", "),
});
}
// Sanitize filename
if (filename) {
req.body.filename = sanitizeFilename(filename.trim());
if (req.body.filename !== filename.trim()) {
console.log(
`[${timestamp}] [${req.requestId}] Filename sanitized: "${filename}" -> "${req.body.filename}"`,
);
}
}
// Trim and clean prompt
if (prompt) {
req.body.prompt = prompt.trim();
}
console.log(`[${timestamp}] [${req.requestId}] Validation passed`);
next();
};
/**
* Log request details for debugging
*/
export const logRequestDetails = (
req: any,
_res: Response,
next: NextFunction,
): void => {
const timestamp = new Date().toISOString();
const { prompt, filename, autoEnhance, enhancementOptions } = req.body;
const files = (req.files as Express.Multer.File[]) || [];
console.log(`[${timestamp}] [${req.requestId}] === REQUEST DETAILS ===`);
console.log(`[${timestamp}] [${req.requestId}] Method: ${req.method}`);
console.log(`[${timestamp}] [${req.requestId}] Path: ${req.path}`);
console.log(
`[${timestamp}] [${req.requestId}] Prompt: "${prompt?.substring(0, 100)}${prompt?.length > 100 ? "..." : ""}"`,
);
console.log(`[${timestamp}] [${req.requestId}] Filename: "${filename}"`);
console.log(
`[${timestamp}] [${req.requestId}] Auto-enhance: ${autoEnhance || false}`,
);
if (enhancementOptions) {
console.log(
`[${timestamp}] [${req.requestId}] Enhancement options:`,
enhancementOptions,
);
}
console.log(
`[${timestamp}] [${req.requestId}] Reference files: ${files.length}`,
);
if (files.length > 0) {
files.forEach((file, index) => {
console.log(
`[${timestamp}] [${req.requestId}] File ${index + 1}: ${file.originalname} (${file.mimetype}, ${Math.round(file.size / 1024)}KB)`,
);
});
}
console.log(`[${timestamp}] [${req.requestId}] ===========================`);
next();
};