import express, { Application } from 'express'; import cors from 'cors'; import { config } from 'dotenv'; import { Config } from './types/api'; import { textToImageRouter } from './routes/textToImage'; import { imagesRouter } from './routes/images'; import { uploadRouter } from './routes/upload'; import bootstrapRoutes from './routes/bootstrap'; import adminKeysRoutes from './routes/admin/keys'; import { errorHandler, notFoundHandler } from './middleware/errorHandler'; // Load environment variables config(); // Application configuration export const appConfig: Config = { port: parseInt(process.env['PORT'] || '3000'), geminiApiKey: process.env['GEMINI_API_KEY'] || '', resultsDir: './results', uploadsDir: './uploads/temp', maxFileSize: 5 * 1024 * 1024, // 5MB maxFiles: 3, }; // Create Express application export const createApp = (): Application => { const app = express(); // Middleware - CORS configuration (allow all origins) app.use( cors({ origin: true, // Allow all origins credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'], exposedHeaders: ['X-Request-ID'], }), ); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Request ID middleware for logging app.use((req, res, next) => { req.requestId = Math.random().toString(36).substr(2, 9); res.setHeader('X-Request-ID', req.requestId); next(); }); // Health check endpoint app.get('/health', (_req, res) => { const health = { status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), environment: process.env['NODE_ENV'] || 'development', version: process.env['npm_package_version'] || '1.0.0', }; console.log(`[${health.timestamp}] Health check - ${health.status}`); res.json(health); }); // API info endpoint app.get('/api/info', async (req: any, res) => { const info: any = { name: 'Banatie - Nano Banana Image Generation API', version: '1.0.0', description: 'REST API service for AI-powered image generation using Gemini Flash Image model', endpoints: { 'GET /health': 'Health check', 'GET /api/info': 'API information', 'POST /api/text-to-image': 'Generate images from text prompt only (JSON)', }, limits: { maxFileSize: `${appConfig.maxFileSize / (1024 * 1024)}MB`, maxFiles: appConfig.maxFiles, supportedFormats: ['PNG', 'JPEG', 'JPG', 'WebP'], }, }; // If API key is provided, validate and return key info const providedKey = req.headers['x-api-key'] as string; if (providedKey) { try { const { ApiKeyService } = await import('./services/ApiKeyService'); const apiKeyService = new ApiKeyService(); const apiKey = await apiKeyService.validateKey(providedKey); if (apiKey) { // Use slugs from validated API key (already fetched via LEFT JOIN) info.authenticated = true; info.keyInfo = { type: apiKey.keyType, organizationId: apiKey.organizationId, organizationSlug: apiKey.organizationSlug, projectId: apiKey.projectId, projectSlug: apiKey.projectSlug, expiresAt: apiKey.expiresAt, }; } } catch (error) { // Ignore errors, just don't add key info } } console.log(`[${new Date().toISOString()}] API info requested`); res.json(info); }); // Public routes (no authentication) // Bootstrap route (no auth, but works only once) app.use('/api/bootstrap', bootstrapRoutes); // Admin routes (require master key) app.use('/api/admin/keys', adminKeysRoutes); // Protected API routes (require valid API key) app.use('/api', textToImageRouter); app.use('/api', imagesRouter); app.use('/api', uploadRouter); // Error handling middleware (must be last) app.use(notFoundHandler); app.use(errorHandler); return app; }; // Extend Express Request type to include requestId declare global { namespace Express { interface Request { requestId: string; } } }