banatie-service/apps/api-service/src/middleware/jsonValidation.ts

213 lines
7.3 KiB
TypeScript

import { Response, NextFunction } from 'express';
import { sanitizeFilename } from './validation';
// Validation rules (same as existing validation but for JSON)
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
},
};
// Valid aspect ratios supported by Gemini SDK
const VALID_ASPECT_RATIOS = [
'1:1', // Square (1024x1024)
'2:3', // Portrait (832x1248)
'3:2', // Landscape (1248x832)
'3:4', // Portrait (864x1184)
'4:3', // Landscape (1184x864)
'4:5', // Portrait (896x1152)
'5:4', // Landscape (1152x896)
'9:16', // Vertical (768x1344)
'16:9', // Widescreen (1344x768)
'21:9', // Ultrawide (1536x672)
] as const;
/**
* Validate the text-to-image JSON request
*/
export const validateTextToImageRequest = (
req: any,
res: Response,
next: NextFunction,
): void | Response => {
const timestamp = new Date().toISOString();
const { prompt, filename, aspectRatio, autoEnhance, enhancementOptions } = req.body;
const errors: string[] = [];
console.log(`[${timestamp}] [${req.requestId}] Validating text-to-image JSON request`);
// Validate that request body exists
if (!req.body || typeof req.body !== 'object') {
return res.status(400).json({
success: false,
error: 'Request body must be valid JSON',
message: 'Invalid request format',
});
}
// Set defaults before validation
// Default autoEnhance to true if not explicitly set
if (req.body.autoEnhance === undefined) {
req.body.autoEnhance = true;
}
// Default template to "photorealistic" in enhancementOptions
if (req.body.enhancementOptions && !req.body.enhancementOptions.template) {
req.body.enhancementOptions.template = 'photorealistic';
} else if (!req.body.enhancementOptions && req.body.autoEnhance !== false) {
// If autoEnhance is true (default) and no enhancementOptions, create it with default template
req.body.enhancementOptions = { template: 'photorealistic' };
}
// 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 aspectRatio (optional, defaults to "1:1")
if (aspectRatio !== undefined) {
if (typeof aspectRatio !== 'string') {
errors.push('aspectRatio must be a string');
} else if (!VALID_ASPECT_RATIOS.includes(aspectRatio as any)) {
errors.push(`Invalid aspectRatio. Must be one of: ${VALID_ASPECT_RATIOS.join(', ')}`);
}
}
// 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 { template } = enhancementOptions;
// Validate template parameter
if (
template !== undefined &&
![
'photorealistic',
'illustration',
'minimalist',
'sticker',
'product',
'comic',
'general',
].includes(template)
) {
errors.push(
'Invalid template in enhancementOptions. Must be one of: photorealistic, illustration, minimalist, sticker, product, comic, general',
);
}
}
}
// Validate meta (optional object)
if (req.body.meta !== undefined) {
if (typeof req.body.meta !== 'object' || Array.isArray(req.body.meta)) {
errors.push('meta must be an object');
} else if (req.body.meta.tags !== undefined) {
if (!Array.isArray(req.body.meta.tags)) {
errors.push('meta.tags must be an array');
} else {
// Validate each tag is a string
for (const tag of req.body.meta.tags) {
if (typeof tag !== 'string') {
errors.push('Each tag in meta.tags must be a string');
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}] JSON validation passed`);
next();
};
/**
* Log text-to-image request details for debugging
*/
export const logTextToImageRequest = (req: any, _res: Response, next: NextFunction): void => {
const timestamp = new Date().toISOString();
const { prompt, filename, autoEnhance, enhancementOptions } = req.body;
console.log(`[${timestamp}] [${req.requestId}] === TEXT-TO-IMAGE REQUEST ===`);
console.log(`[${timestamp}] [${req.requestId}] Method: ${req.method}`);
console.log(`[${timestamp}] [${req.requestId}] Path: ${req.path}`);
console.log(`[${timestamp}] [${req.requestId}] Content-Type: ${req.get('Content-Type')}`);
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: 0 (text-only endpoint)`);
console.log(`[${timestamp}] [${req.requestId}] =====================================`);
next();
};