diff --git a/logs/server.log b/logs/server.log new file mode 100644 index 0000000..b5aae56 --- /dev/null +++ b/logs/server.log @@ -0,0 +1,18 @@ +/projects/my-projects/Magic-Building/src/server/app.ts:1 +import express, { Application } from 'express'; +^ + + +ReferenceError: Cannot access 'appConfig' before initialization + at Object.appConfig (/projects/my-projects/Magic-Building/src/server/app.ts:1:1) + at Object.get [as appConfig] (/projects/my-projects/Magic-Building/src/server/app.ts:2:661) + at multer (/projects/my-projects/Magic-Building/src/server/middleware/upload.ts:30:15) + at Object. (/projects/my-projects/Magic-Building/src/server/middleware/upload.ts:40:35) + at Module._compile (node:internal/modules/cjs/loader:1546:14) + at Object.transformer (/projects/my-projects/Magic-Building/node_modules/.pnpm/tsx@4.20.5/node_modules/tsx/dist/register-D46fvsV_.cjs:3:1104) + at Module.load (node:internal/modules/cjs/loader:1318:32) + at Function._load (node:internal/modules/cjs/loader:1128:12) + at TracingChannel.traceSync (node:diagnostics_channel:315:14) + at wrapModuleLoad (node:internal/modules/cjs/loader:218:24) + +Node.js v22.11.0 diff --git a/logs/test-api-20250915_010051.log b/logs/test-api-20250915_010051.log new file mode 100644 index 0000000..4f34276 --- /dev/null +++ b/logs/test-api-20250915_010051.log @@ -0,0 +1,29 @@ +⚠️ jq not found - JSON response parsing will be limited +ℹ️ 🚀 Starting Magic Building API Test Suite +ℹ️ Server URL: http://localhost:3000 +ℹ️ Test log: ./logs/test-api-20250915_010051.log +ℹ️ Testing health check endpoint... +❌ Health check failed (HTTP 000) +ℹ️ Testing API info endpoint... +❌ API info endpoint failed (HTTP 000) +ℹ️ Testing text-to-image generation... +❌ Text-to-image generation failed (HTTP 000) +2025-09-15 01:00:51 - Error response: +ℹ️ Testing validation error handling... +❌ Missing prompt validation failed (HTTP 000) +❌ Missing filename validation failed (HTTP 000) +⚠️ Filename sanitization test had unexpected result (HTTP 000) +ℹ️ Testing 404 error handling... +❌ 404 error handling failed (HTTP 000) +ℹ️ Creating test reference image... +⚠️ ImageMagick not available, skipping reference image test +ℹ️ Testing concurrent requests (performance test)... +2025-09-15 01:00:51 - Concurrent request 1 failed (HTTP 000) +2025-09-15 01:00:51 - Concurrent request 3 failed (HTTP 000) +2025-09-15 01:00:51 - Concurrent request 2 failed (HTTP 000) +✅ Concurrent requests test completed +ℹ️ 📊 Test Results Summary +✅ Passed: 3/6 tests +⚠️ ⚠️ Some tests failed. Check the logs for details. +ℹ️ Generated images are in: src/results/ +ℹ️ Test logs saved to: ./logs/test-api-20250915_010051.log diff --git a/src/results/test_crystal.png b/src/results/test_crystal.png new file mode 100644 index 0000000..8f4909d Binary files /dev/null and b/src/results/test_crystal.png differ diff --git a/src/server/app.ts b/src/server/app.ts index 1172312..a9cee4d 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -2,6 +2,8 @@ import express, { Application } from 'express'; import cors from 'cors'; import { config } from 'dotenv'; import { Config } from './types/api'; +import { generateRouter } from './routes/generate'; +import { errorHandler, notFoundHandler } from './middleware/errorHandler'; // Load environment variables config(); @@ -72,6 +74,13 @@ export const createApp = (): Application => { res.json(info); }); + // Mount API routes + app.use('/api', generateRouter); + + // Error handling middleware (must be last) + app.use(notFoundHandler); + app.use(errorHandler); + return app; }; diff --git a/src/server/middleware/errorHandler.ts b/src/server/middleware/errorHandler.ts new file mode 100644 index 0000000..fbb1c0a --- /dev/null +++ b/src/server/middleware/errorHandler.ts @@ -0,0 +1,103 @@ +import { Request, Response, NextFunction } from 'express'; +import { GenerateImageResponse } from '../types/api'; + +/** + * Global error handler for the Express application + */ +export const errorHandler = ( + error: Error, + req: Request, + res: Response, + next: NextFunction +) => { + const timestamp = new Date().toISOString(); + const requestId = req.requestId || 'unknown'; + + // Log the error + console.error(`[${timestamp}] [${requestId}] ERROR:`, { + message: error.message, + stack: error.stack, + path: req.path, + method: req.method, + body: req.body, + query: req.query + }); + + // Don't send error response if headers already sent + if (res.headersSent) { + return next(error); + } + + // Determine error type and status code + let statusCode = 500; + let errorMessage = 'Internal server error'; + let errorType = 'INTERNAL_ERROR'; + + if (error.name === 'ValidationError') { + statusCode = 400; + errorMessage = error.message; + errorType = 'VALIDATION_ERROR'; + } else if (error.message.includes('API key') || error.message.includes('authentication')) { + statusCode = 401; + errorMessage = 'Authentication failed'; + errorType = 'AUTH_ERROR'; + } else if (error.message.includes('not found') || error.message.includes('404')) { + statusCode = 404; + errorMessage = 'Resource not found'; + errorType = 'NOT_FOUND'; + } else if (error.message.includes('timeout') || error.message.includes('503')) { + statusCode = 503; + errorMessage = 'Service temporarily unavailable'; + errorType = 'SERVICE_UNAVAILABLE'; + } else if (error.message.includes('overloaded') || error.message.includes('rate limit')) { + statusCode = 429; + errorMessage = 'Service overloaded, please try again later'; + errorType = 'RATE_LIMITED'; + } + + // Create error response + const errorResponse: GenerateImageResponse = { + success: false, + message: 'Request failed', + error: errorMessage + }; + + // Add additional debug info in development + if (process.env.NODE_ENV === 'development') { + (errorResponse as any).debug = { + originalError: error.message, + errorType, + requestId, + timestamp + }; + } + + console.log(`[${timestamp}] [${requestId}] Sending error response: ${statusCode} - ${errorMessage}`); + + res.status(statusCode).json(errorResponse); +}; + +/** + * 404 handler for unmatched routes + */ +export const notFoundHandler = (req: Request, res: Response) => { + const timestamp = new Date().toISOString(); + const requestId = req.requestId || 'unknown'; + + console.log(`[${timestamp}] [${requestId}] 404 - Route not found: ${req.method} ${req.path}`); + + const notFoundResponse: GenerateImageResponse = { + success: false, + message: 'Route not found', + error: `The requested endpoint ${req.method} ${req.path} does not exist` + }; + + res.status(404).json(notFoundResponse); +}; + +/** + * Async error wrapper to catch errors in async route handlers + */ +export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; \ No newline at end of file diff --git a/src/server/middleware/upload.ts b/src/server/middleware/upload.ts new file mode 100644 index 0000000..d6555b5 --- /dev/null +++ b/src/server/middleware/upload.ts @@ -0,0 +1,89 @@ +import multer, { Multer } from 'multer'; +import { Request, RequestHandler } from 'express'; + +// Configure multer for memory storage (we'll process files in memory) +const storage = multer.memoryStorage(); + +// File filter for image types only +const fileFilter = (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => { + const allowedTypes = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/webp' + ]; + + if (allowedTypes.includes(file.mimetype)) { + console.log(`[${new Date().toISOString()}] Accepted file: ${file.originalname} (${file.mimetype})`); + cb(null, true); + } else { + console.log(`[${new Date().toISOString()}] Rejected file: ${file.originalname} (${file.mimetype})`); + cb(new Error(`Unsupported file type: ${file.mimetype}. Allowed: PNG, JPEG, WebP`)); + } +}; + +// Configuration constants +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const MAX_FILES = 3; + +// Configure multer with limits and file filtering +export const upload = multer({ + storage: storage, + limits: { + fileSize: MAX_FILE_SIZE, // 5MB per file + files: MAX_FILES, // Maximum 3 files + }, + fileFilter: fileFilter +}); + +// Middleware for handling reference images +export const uploadReferenceImages: RequestHandler = upload.array('referenceFiles', MAX_FILES); + +// Error handler for multer errors +export const handleUploadErrors = (error: any, req: Request, res: any, next: any) => { + if (error instanceof multer.MulterError) { + const timestamp = new Date().toISOString(); + console.error(`[${timestamp}] Multer error:`, error.message); + + switch (error.code) { + case 'LIMIT_FILE_SIZE': + return res.status(400).json({ + success: false, + error: `File too large. Maximum size: ${MAX_FILE_SIZE / (1024 * 1024)}MB`, + message: 'File upload failed' + }); + + case 'LIMIT_FILE_COUNT': + return res.status(400).json({ + success: false, + error: `Too many files. Maximum: ${MAX_FILES} files`, + message: 'File upload failed' + }); + + case 'LIMIT_UNEXPECTED_FILE': + return res.status(400).json({ + success: false, + error: 'Unexpected file field. Use "referenceFiles" for image uploads', + message: 'File upload failed' + }); + + default: + return res.status(400).json({ + success: false, + error: error.message, + message: 'File upload failed' + }); + } + } + + if (error.message.includes('Unsupported file type')) { + return res.status(400).json({ + success: false, + error: error.message, + message: 'File validation failed' + }); + } + + // Pass other errors to the next error handler + next(error); +}; \ No newline at end of file diff --git a/src/server/middleware/validation.ts b/src/server/middleware/validation.ts new file mode 100644 index 0000000..c0ba486 --- /dev/null +++ b/src/server/middleware/validation.ts @@ -0,0 +1,136 @@ +import { Request, Response, NextFunction } from 'express'; +import { GenerateImageRequestWithFiles } from '../types/api'; + +// 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 +) => { + const timestamp = new Date().toISOString(); + const { prompt, filename } = 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'); + } + + // Check for XSS attempts in prompt + const xssPatterns = [ + /