246 lines
6.9 KiB
TypeScript
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();
|
|
};
|