diff --git a/.mcp.json b/.mcp.json index 085f810..6e1a357 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,29 +1,34 @@ { "mcpServers": { "context7": { - "type": "stdio", - "command": "npx", - "args": [ - "-y", - "@upstash/context7-mcp", - "--api-key", - "ctx7sk-48cb1995-935a-4cc5-b9b0-535d600ea5e6" - ], - "env": {} - }, - "brave-search": { + "type": "stdio", "command": "npx", "args": [ "-y", - "@modelcontextprotocol/server-brave-search" + "@upstash/context7-mcp", + "--api-key", + "ctx7sk-48cb1995-935a-4cc5-b9b0-535d600ea5e6" ], + "env": {} + }, + "brave-search": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-brave-search"], "env": { "BRAVE_API_KEY": "BSAcRGGikEzY4B2j3NZ8Qy5NYh9l4HZ" } }, "postgres": { "command": "docker", - "args": ["run", "-i", "--rm", "-e", "DATABASE_URI", "crystaldba/postgres-mcp", "--access-mode=unrestricted"], + "args": [ + "run", + "-i", + "--rm", + "-e", + "DATABASE_URI", + "crystaldba/postgres-mcp", + "--access-mode=unrestricted" + ], "env": { "DATABASE_URI": "postgresql://banatie_user:banatie_secure_password@localhost:5434/banatie_db" } diff --git a/.prettierignore b/.prettierignore index 9ea06f7..b14406d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -21,6 +21,7 @@ pnpm-debug.log* # Data and storage data/ +data/postgres/** *.sql *.db *.sqlite diff --git a/CLAUDE.md b/CLAUDE.md index 785c88f..3408c14 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,6 +115,7 @@ Shared Drizzle ORM package used by API service and future apps: - **Configuration**: `drizzle.config.ts` - Drizzle Kit configuration Key table: `api_keys` + - Stores hashed API keys (SHA-256) - Two types: `master` (admin, never expires) and `project` (90-day expiration) - Soft delete via `is_active` flag @@ -127,6 +128,7 @@ Key table: `api_keys` ### Root `.env` (Docker Compose Infrastructure) Used by Docker Compose services (MinIO, Postgres, API container). Key differences from local: + - `DATABASE_URL=postgresql://banatie_user:banatie_secure_password@postgres:5432/banatie_db` (Docker network hostname) - `MINIO_ENDPOINT=minio:9000` (Docker network hostname) - `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD` - MinIO admin credentials @@ -135,6 +137,7 @@ Used by Docker Compose services (MinIO, Postgres, API container). Key difference ### API Service `.env` (Local Development Only) Located at `apps/api-service/.env` - used ONLY when running `pnpm dev:api` locally: + - `DATABASE_URL=postgresql://banatie_user:banatie_secure_password@localhost:5434/banatie_db` (port-forwarded) - `MINIO_ENDPOINT=localhost:9000` (port-forwarded) - **NOTE**: This file is excluded from Docker builds (see Dockerfile.mono) @@ -197,16 +200,19 @@ Located at `apps/api-service/.env` - used ONLY when running `pnpm dev:api` local ## API Endpoints (API Service) ### Public Endpoints (No Authentication) + - `GET /health` - Health check with uptime and status - `GET /api/info` - API information and limits - `POST /api/bootstrap/initial-key` - Create first master key (one-time only) ### Admin Endpoints (Master Key Required) + - `POST /api/admin/keys` - Create new API keys (master or project) - `GET /api/admin/keys` - List all API keys - `DELETE /api/admin/keys/:keyId` - Revoke an API key ### Protected Endpoints (API Key Required) + - `POST /api/generate` - Generate images from text + optional reference images - `POST /api/text-to-image` - Generate images from text only (JSON) - `POST /api/enhance` - Enhance and optimize text prompts @@ -220,9 +226,11 @@ Located at `apps/api-service/.env` - used ONLY when running `pnpm dev:api` local 1. **Start Services**: `docker compose up -d` 2. **Create Master Key**: + ```bash curl -X POST http://localhost:3000/api/bootstrap/initial-key ``` + Save the returned key securely! 3. **Create Project Key** (for testing): diff --git a/README.md b/README.md index 6d0d292..d01999b 100644 --- a/README.md +++ b/README.md @@ -19,24 +19,28 @@ banatie-service/ ## Applications ### 🚀 API Service (`apps/api-service`) + - **Port**: 3000 - **Tech**: Express.js, TypeScript, Gemini AI - **Purpose**: Core REST API for AI image generation - **Features**: Image generation, file upload, rate limiting, logging ### 🌐 Landing Page (`apps/landing`) + - **Port**: 3001 - **Tech**: Next.js 14, Tailwind CSS - **Purpose**: Public landing page with demo - **Features**: Service overview, image generation demo ### 🏢 Studio (`apps/studio`) + - **Port**: 3002 - **Tech**: Next.js 14, Supabase, Stripe - **Purpose**: SaaS platform for paid users - **Features**: Authentication, billing, subscriptions, usage tracking ### 🔧 Admin (`apps/admin`) + - **Port**: 3003 - **Tech**: Next.js 14, Dashboard components - **Purpose**: Service administration and monitoring @@ -45,6 +49,7 @@ banatie-service/ ## Quick Start ### Prerequisites + - Node.js >= 18.0.0 - pnpm >= 8.0.0 - Docker & Docker Compose @@ -124,19 +129,20 @@ See individual app README files for specific environment variables. ## Services & Ports -| Service | Port | Description | -|---------|------|-------------| -| API Service | 3000 | Core REST API | -| Landing Page | 3001 | Public website | +| Service | Port | Description | +| --------------- | ---- | ---------------- | +| API Service | 3000 | Core REST API | +| Landing Page | 3001 | Public website | | Studio Platform | 3002 | SaaS application | -| Admin Dashboard | 3003 | Administration | -| PostgreSQL | 5434 | Database | -| MinIO API | 9000 | Object storage | -| MinIO Console | 9001 | Storage admin | +| Admin Dashboard | 3003 | Administration | +| PostgreSQL | 5434 | Database | +| MinIO API | 9000 | Object storage | +| MinIO Console | 9001 | Storage admin | ## API Usage ### Generate Image + ```bash # Basic text-to-image curl -X POST http://localhost:3000/api/generate \ @@ -160,4 +166,4 @@ See `apps/api-service/README.md` for detailed API documentation. ## License -MIT License - see individual app directories for more details. \ No newline at end of file +MIT License - see individual app directories for more details. diff --git a/apps/admin/.eslintrc.json b/apps/admin/.eslintrc.json index 85cf00f..f8a16da 100644 --- a/apps/admin/.eslintrc.json +++ b/apps/admin/.eslintrc.json @@ -1,8 +1,5 @@ { - "extends": [ - "next/core-web-vitals", - "prettier" - ], + "extends": ["next/core-web-vitals", "prettier"], "plugins": ["prettier"], "rules": { "prettier/prettier": "error" diff --git a/apps/admin/README.md b/apps/admin/README.md index 7c68652..45587f2 100644 --- a/apps/admin/README.md +++ b/apps/admin/README.md @@ -63,4 +63,4 @@ ADMIN_PASSWORD=secure_password - [ ] Create backup/restore functionality - [ ] Add alert system - [ ] Implement audit logging -- [ ] Add API key management \ No newline at end of file +- [ ] Add API key management diff --git a/apps/admin/next.config.js b/apps/admin/next.config.js index 6751012..eaa39fd 100644 --- a/apps/admin/next.config.js +++ b/apps/admin/next.config.js @@ -11,6 +11,6 @@ const nextConfig = { POSTGRES_URL: process.env.POSTGRES_URL, MINIO_ENDPOINT: process.env.MINIO_ENDPOINT, }, -} +}; -module.exports = nextConfig \ No newline at end of file +module.exports = nextConfig; diff --git a/apps/admin/package.json b/apps/admin/package.json index 8d14861..1bca53d 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -34,4 +34,4 @@ "node": ">=18.0.0", "pnpm": ">=8.0.0" } -} \ No newline at end of file +} diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx index 98a0d28..745ebda 100644 --- a/apps/admin/src/app/layout.tsx +++ b/apps/admin/src/app/layout.tsx @@ -1,22 +1,16 @@ -import type { Metadata } from 'next' +import type { Metadata } from 'next'; export const metadata: Metadata = { title: 'Banatie Admin - Service Administration', description: 'Administration dashboard for managing the Banatie AI image generation service', -} +}; -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( -
- {children} -
+
{children}
- ) -} \ No newline at end of file + ); +} diff --git a/apps/admin/src/app/page.tsx b/apps/admin/src/app/page.tsx index 380db8d..8786773 100644 --- a/apps/admin/src/app/page.tsx +++ b/apps/admin/src/app/page.tsx @@ -6,9 +6,7 @@ export default function AdminDashboard() {
-

- Banatie Admin -

+

Banatie Admin

System Status: Online @@ -21,7 +19,6 @@ export default function AdminDashboard() {
- {/* Stats Cards */}
@@ -33,12 +30,8 @@ export default function AdminDashboard() {
-
- API Requests -
-
- 1,234 -
+
API Requests
+
1,234
@@ -58,9 +51,7 @@ export default function AdminDashboard() {
Images Generated
-
- 567 -
+
567
@@ -77,12 +68,8 @@ export default function AdminDashboard() {
-
- Active Users -
-
- 89 -
+
Active Users
+
89
@@ -99,12 +86,8 @@ export default function AdminDashboard() {
-
- Errors (24h) -
-
- 3 -
+
Errors (24h)
+
3
@@ -116,9 +99,7 @@ export default function AdminDashboard() {
-

- Service Status -

+

Service Status

API Service @@ -159,8 +140,8 @@ export default function AdminDashboard() {

- This admin dashboard is being developed to provide comprehensive - monitoring and management capabilities for the Banatie service. + This admin dashboard is being developed to provide comprehensive monitoring + and management capabilities for the Banatie service.

@@ -170,5 +151,5 @@ export default function AdminDashboard() {
- ) -} \ No newline at end of file + ); +} diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json index a26cf70..3a3c3c4 100644 --- a/apps/admin/tsconfig.json +++ b/apps/admin/tsconfig.json @@ -28,4 +28,4 @@ }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] -} \ No newline at end of file +} diff --git a/apps/api-service/.prettier.config.js b/apps/api-service/.prettier.config.js index 3692e10..899fe7a 100644 --- a/apps/api-service/.prettier.config.js +++ b/apps/api-service/.prettier.config.js @@ -12,4 +12,4 @@ module.exports = { quoteProps: 'as-needed', jsxSingleQuote: true, proseWrap: 'preserve', -}; \ No newline at end of file +}; diff --git a/apps/api-service/eslint.config.js b/apps/api-service/eslint.config.js index ff48600..6694cda 100644 --- a/apps/api-service/eslint.config.js +++ b/apps/api-service/eslint.config.js @@ -13,9 +13,7 @@ module.exports = [ '@typescript-eslint': require('@typescript-eslint/eslint-plugin'), prettier: require('eslint-plugin-prettier'), }, - extends: [ - require('eslint-config-prettier'), - ], + extends: [require('eslint-config-prettier')], rules: { 'prettier/prettier': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], @@ -30,12 +28,6 @@ module.exports = [ }, }, { - ignores: [ - 'node_modules/**', - 'dist/**', - '*.js', - '*.mjs', - 'eslint.config.js' - ], + ignores: ['node_modules/**', 'dist/**', '*.js', '*.mjs', 'eslint.config.js'], }, -]; \ No newline at end of file +]; diff --git a/apps/api-service/package.json b/apps/api-service/package.json index f6a1e3e..7c95e8a 100644 --- a/apps/api-service/package.json +++ b/apps/api-service/package.json @@ -71,4 +71,4 @@ "tsx": "^4.20.5", "typescript": "^5.9.2" } -} \ No newline at end of file +} diff --git a/apps/api-service/src/app.ts b/apps/api-service/src/app.ts index bd685ed..7adeef5 100644 --- a/apps/api-service/src/app.ts +++ b/apps/api-service/src/app.ts @@ -1,24 +1,24 @@ -import express, { Application } from "express"; -import cors from "cors"; -import { config } from "dotenv"; -import { Config } from "./types/api"; -import { generateRouter } from "./routes/generate"; -import { enhanceRouter } from "./routes/enhance"; -import { textToImageRouter } from "./routes/textToImage"; -import { imagesRouter } from "./routes/images"; -import bootstrapRoutes from "./routes/bootstrap"; -import adminKeysRoutes from "./routes/admin/keys"; -import { errorHandler, notFoundHandler } from "./middleware/errorHandler"; +import express, { Application } from 'express'; +import cors from 'cors'; +import { config } from 'dotenv'; +import { Config } from './types/api'; +import { generateRouter } from './routes/generate'; +import { enhanceRouter } from './routes/enhance'; +import { textToImageRouter } from './routes/textToImage'; +import { imagesRouter } from './routes/images'; +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", + port: parseInt(process.env['PORT'] || '3000'), + geminiApiKey: process.env['GEMINI_API_KEY'] || '', + resultsDir: './results', + uploadsDir: './uploads/temp', maxFileSize: 5 * 1024 * 1024, // 5MB maxFiles: 3, }; @@ -32,30 +32,30 @@ export const createApp = (): Application => { 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"], + 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" })); + 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); + res.setHeader('X-Request-ID', req.requestId); next(); }); // Health check endpoint - app.get("/health", (_req, res) => { + app.get('/health', (_req, res) => { const health = { - status: "healthy", + status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), - environment: process.env["NODE_ENV"] || "development", - version: process.env["npm_package_version"] || "1.0.0", + environment: process.env['NODE_ENV'] || 'development', + version: process.env['npm_package_version'] || '1.0.0', }; console.log(`[${health.timestamp}] Health check - ${health.status}`); @@ -63,34 +63,31 @@ export const createApp = (): Application => { }); // API info endpoint - app.get("/api/info", async (req: any, res) => { + app.get('/api/info', async (req: any, res) => { const info: any = { - name: "Banatie - Nano Banana Image Generation API", - version: "1.0.0", + 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", + '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/generate": - "Generate images from text prompt with optional reference images", - "POST /api/text-to-image": - "Generate images from text prompt only (JSON)", - "POST /api/enhance": - "Enhance and optimize prompts for better image generation", + 'GET /health': 'Health check', + 'GET /api/info': 'API information', + 'POST /api/generate': 'Generate images from text prompt with optional reference images', + 'POST /api/text-to-image': 'Generate images from text prompt only (JSON)', + 'POST /api/enhance': 'Enhance and optimize prompts for better image generation', }, limits: { maxFileSize: `${appConfig.maxFileSize / (1024 * 1024)}MB`, maxFiles: appConfig.maxFiles, - supportedFormats: ["PNG", "JPEG", "JPG", "WebP"], + supportedFormats: ['PNG', 'JPEG', 'JPG', 'WebP'], }, }; // If API key is provided, validate and return key info - const providedKey = req.headers["x-api-key"] as string; + const providedKey = req.headers['x-api-key'] as string; if (providedKey) { try { - const { ApiKeyService } = await import("./services/ApiKeyService"); + const { ApiKeyService } = await import('./services/ApiKeyService'); const apiKeyService = new ApiKeyService(); const apiKey = await apiKeyService.validateKey(providedKey); @@ -117,16 +114,16 @@ export const createApp = (): Application => { // Public routes (no authentication) // Bootstrap route (no auth, but works only once) - app.use("/api/bootstrap", bootstrapRoutes); + app.use('/api/bootstrap', bootstrapRoutes); // Admin routes (require master key) - app.use("/api/admin/keys", adminKeysRoutes); + app.use('/api/admin/keys', adminKeysRoutes); // Protected API routes (require valid API key) - app.use("/api", generateRouter); - app.use("/api", enhanceRouter); - app.use("/api", textToImageRouter); - app.use("/api", imagesRouter); + app.use('/api', generateRouter); + app.use('/api', enhanceRouter); + app.use('/api', textToImageRouter); + app.use('/api', imagesRouter); // Error handling middleware (must be last) app.use(notFoundHandler); diff --git a/apps/api-service/src/db.ts b/apps/api-service/src/db.ts index e084852..0800e4c 100644 --- a/apps/api-service/src/db.ts +++ b/apps/api-service/src/db.ts @@ -1,8 +1,11 @@ import { createDbClient } from '@banatie/database'; -const DATABASE_URL = process.env['DATABASE_URL'] || +const DATABASE_URL = + process.env['DATABASE_URL'] || 'postgresql://banatie_user:banatie_secure_password@localhost:5434/banatie_db'; export const db = createDbClient(DATABASE_URL); -console.log(`[${new Date().toISOString()}] Database client initialized - ${new URL(DATABASE_URL).host}`); \ No newline at end of file +console.log( + `[${new Date().toISOString()}] Database client initialized - ${new URL(DATABASE_URL).host}`, +); diff --git a/apps/api-service/src/middleware/auth/rateLimiter.ts b/apps/api-service/src/middleware/auth/rateLimiter.ts index c4ecebf..911573f 100644 --- a/apps/api-service/src/middleware/auth/rateLimiter.ts +++ b/apps/api-service/src/middleware/auth/rateLimiter.ts @@ -37,7 +37,7 @@ class RateLimiter { return { allowed: true, remaining: this.limit - record.count, - resetAt: record.resetAt + resetAt: record.resetAt, }; } @@ -57,11 +57,7 @@ const rateLimiter = new RateLimiter(100, 60 * 60 * 1000); // 100 requests per ho * Rate limiting middleware * Must be used AFTER validateApiKey middleware */ -export function rateLimitByApiKey( - req: Request, - res: Response, - next: NextFunction -): void { +export function rateLimitByApiKey(req: Request, res: Response, next: NextFunction): void { if (!req.apiKey) { next(); return; @@ -77,9 +73,12 @@ export function rateLimitByApiKey( if (!result.allowed) { const retryAfter = Math.ceil((result.resetAt - Date.now()) / 1000); - console.warn(`[${new Date().toISOString()}] Rate limit exceeded: ${req.apiKey.id} (${req.apiKey.keyType}) - reset: ${new Date(result.resetAt).toISOString()}`); + console.warn( + `[${new Date().toISOString()}] Rate limit exceeded: ${req.apiKey.id} (${req.apiKey.keyType}) - reset: ${new Date(result.resetAt).toISOString()}`, + ); - res.status(429) + res + .status(429) .setHeader('Retry-After', retryAfter.toString()) .json({ error: 'Rate limit exceeded', @@ -90,4 +89,4 @@ export function rateLimitByApiKey( } next(); -} \ No newline at end of file +} diff --git a/apps/api-service/src/middleware/auth/requireMasterKey.ts b/apps/api-service/src/middleware/auth/requireMasterKey.ts index f6d7402..8d58350 100644 --- a/apps/api-service/src/middleware/auth/requireMasterKey.ts +++ b/apps/api-service/src/middleware/auth/requireMasterKey.ts @@ -4,11 +4,7 @@ import { Request, Response, NextFunction } from 'express'; * Middleware to ensure the API key is a master key * Must be used AFTER validateApiKey middleware */ -export function requireMasterKey( - req: Request, - res: Response, - next: NextFunction -): void { +export function requireMasterKey(req: Request, res: Response, next: NextFunction): void { if (!req.apiKey) { res.status(401).json({ error: 'Authentication required', @@ -18,7 +14,9 @@ export function requireMasterKey( } if (req.apiKey.keyType !== 'master') { - console.warn(`[${new Date().toISOString()}] Non-master key attempted admin action: ${req.apiKey.id} (${req.apiKey.keyType}) - ${req.path}`); + console.warn( + `[${new Date().toISOString()}] Non-master key attempted admin action: ${req.apiKey.id} (${req.apiKey.keyType}) - ${req.path}`, + ); res.status(403).json({ error: 'Master key required', @@ -28,4 +26,4 @@ export function requireMasterKey( } next(); -} \ No newline at end of file +} diff --git a/apps/api-service/src/middleware/auth/requireProjectKey.ts b/apps/api-service/src/middleware/auth/requireProjectKey.ts index e65818b..7dd2498 100644 --- a/apps/api-service/src/middleware/auth/requireProjectKey.ts +++ b/apps/api-service/src/middleware/auth/requireProjectKey.ts @@ -4,11 +4,7 @@ import { Request, Response, NextFunction } from 'express'; * Middleware to ensure only project keys can access generation endpoints * Master keys are for admin purposes only */ -export function requireProjectKey( - req: Request, - res: Response, - next: NextFunction -): void { +export function requireProjectKey(req: Request, res: Response, next: NextFunction): void { // This middleware assumes validateApiKey has already run and attached req.apiKey if (!req.apiKey) { res.status(401).json({ @@ -22,7 +18,8 @@ export function requireProjectKey( if (req.apiKey.keyType === 'master') { res.status(403).json({ error: 'Forbidden', - message: 'Master keys cannot be used for image generation. Please use a project-specific API key.', + message: + 'Master keys cannot be used for image generation. Please use a project-specific API key.', }); return; } @@ -36,7 +33,9 @@ export function requireProjectKey( return; } - console.log(`[${new Date().toISOString()}] Project key validated for generation: ${req.apiKey.id}`); + console.log( + `[${new Date().toISOString()}] Project key validated for generation: ${req.apiKey.id}`, + ); next(); } diff --git a/apps/api-service/src/middleware/auth/validateApiKey.ts b/apps/api-service/src/middleware/auth/validateApiKey.ts index 6652d8c..a69db91 100644 --- a/apps/api-service/src/middleware/auth/validateApiKey.ts +++ b/apps/api-service/src/middleware/auth/validateApiKey.ts @@ -18,7 +18,7 @@ const apiKeyService = new ApiKeyService(); export async function validateApiKey( req: Request, res: Response, - next: NextFunction + next: NextFunction, ): Promise { const providedKey = req.headers['x-api-key'] as string; @@ -44,7 +44,9 @@ export async function validateApiKey( // Attach to request for use in routes req.apiKey = apiKey; - console.log(`[${new Date().toISOString()}] API key validated: ${apiKey.id} (${apiKey.keyType})`); + console.log( + `[${new Date().toISOString()}] API key validated: ${apiKey.id} (${apiKey.keyType})`, + ); next(); } catch (error) { @@ -54,4 +56,4 @@ export async function validateApiKey( message: 'An error occurred during authentication', }); } -} \ No newline at end of file +} diff --git a/apps/api-service/src/middleware/errorHandler.ts b/apps/api-service/src/middleware/errorHandler.ts index 81fd625..70e2114 100644 --- a/apps/api-service/src/middleware/errorHandler.ts +++ b/apps/api-service/src/middleware/errorHandler.ts @@ -1,17 +1,12 @@ -import { Request, Response, NextFunction } from "express"; -import { GenerateImageResponse } from "../types/api"; +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, -) => { +export const errorHandler = (error: Error, req: Request, res: Response, next: NextFunction) => { const timestamp = new Date().toISOString(); - const requestId = req.requestId || "unknown"; + const requestId = req.requestId || 'unknown'; // Log the error console.error(`[${timestamp}] [${requestId}] ERROR:`, { @@ -30,52 +25,40 @@ export const errorHandler = ( // Determine error type and status code let statusCode = 500; - let errorMessage = "Internal server error"; - let errorType = "INTERNAL_ERROR"; + let errorMessage = 'Internal server error'; + let errorType = 'INTERNAL_ERROR'; - if (error.name === "ValidationError") { + if (error.name === 'ValidationError') { statusCode = 400; errorMessage = error.message; - errorType = "VALIDATION_ERROR"; - } else if ( - error.message.includes("API key") || - error.message.includes("authentication") - ) { + 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") - ) { + 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") - ) { + 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") - ) { + 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"; + errorMessage = 'Service overloaded, please try again later'; + errorType = 'RATE_LIMITED'; } // Create error response const errorResponse: GenerateImageResponse = { success: false, - message: "Request failed", + message: 'Request failed', error: errorMessage, }; // Add additional debug info in development - if (process.env["NODE_ENV"] === "development") { + if (process.env['NODE_ENV'] === 'development') { (errorResponse as any).debug = { originalError: error.message, errorType, @@ -96,15 +79,13 @@ export const errorHandler = ( */ export const notFoundHandler = (req: Request, res: Response) => { const timestamp = new Date().toISOString(); - const requestId = req.requestId || "unknown"; + const requestId = req.requestId || 'unknown'; - console.log( - `[${timestamp}] [${requestId}] 404 - Route not found: ${req.method} ${req.path}`, - ); + console.log(`[${timestamp}] [${requestId}] 404 - Route not found: ${req.method} ${req.path}`); const notFoundResponse: GenerateImageResponse = { success: false, - message: "Route not found", + message: 'Route not found', error: `The requested endpoint ${req.method} ${req.path} does not exist`, }; @@ -114,7 +95,6 @@ export const notFoundHandler = (req: Request, res: Response) => { /** * 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); - }; +export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; diff --git a/apps/api-service/src/middleware/jsonValidation.ts b/apps/api-service/src/middleware/jsonValidation.ts index 1693f91..651d5d7 100644 --- a/apps/api-service/src/middleware/jsonValidation.ts +++ b/apps/api-service/src/middleware/jsonValidation.ts @@ -1,5 +1,5 @@ -import { Response, NextFunction } from "express"; -import { sanitizeFilename } from "./validation"; +import { Response, NextFunction } from 'express'; +import { sanitizeFilename } from './validation'; // Validation rules (same as existing validation but for JSON) const VALIDATION_RULES = { @@ -18,16 +18,16 @@ const VALIDATION_RULES = { // 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) + '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; /** @@ -42,16 +42,14 @@ export const validateTextToImageRequest = ( const { prompt, filename, aspectRatio, autoEnhance, enhancementOptions } = req.body; const errors: string[] = []; - console.log( - `[${timestamp}] [${req.requestId}] Validating text-to-image JSON request`, - ); + console.log(`[${timestamp}] [${req.requestId}] Validating text-to-image JSON request`); // Validate that request body exists - if (!req.body || typeof req.body !== "object") { + 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", + error: 'Request body must be valid JSON', + message: 'Invalid request format', }); } @@ -63,67 +61,54 @@ export const validateTextToImageRequest = ( // Default template to "photorealistic" in enhancementOptions if (req.body.enhancementOptions && !req.body.enhancementOptions.template) { - req.body.enhancementOptions.template = "photorealistic"; + 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" }; + 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"); + 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`, - ); + 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`, - ); + 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"); + 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"); + 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`, - ); + 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", - ); + 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"); + 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(", ")}` - ); + 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"); + 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"); + if (typeof enhancementOptions !== 'object' || Array.isArray(enhancementOptions)) { + errors.push('enhancementOptions must be an object'); } else { const { template } = enhancementOptions; @@ -131,17 +116,17 @@ export const validateTextToImageRequest = ( if ( template !== undefined && ![ - "photorealistic", - "illustration", - "minimalist", - "sticker", - "product", - "comic", - "general", + '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", + 'Invalid template in enhancementOptions. Must be one of: photorealistic, illustration, minimalist, sticker, product, comic, general', ); } } @@ -149,19 +134,16 @@ export const validateTextToImageRequest = ( // 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"); + 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"); + 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"); + if (typeof tag !== 'string') { + errors.push('Each tag in meta.tags must be a string'); break; } } @@ -170,28 +152,19 @@ export const validateTextToImageRequest = ( } // Check for XSS attempts in prompt - const xssPatterns = [ - /