feat: add file upload endpoint
This commit is contained in:
parent
6944e6b750
commit
237443194f
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -75,11 +75,13 @@ banatie-service/
|
||||||
- **Route Handling**:
|
- **Route Handling**:
|
||||||
- `src/routes/bootstrap.ts` - Bootstrap initial master key (one-time)
|
- `src/routes/bootstrap.ts` - Bootstrap initial master key (one-time)
|
||||||
- `src/routes/admin/keys.ts` - API key management (master key required)
|
- `src/routes/admin/keys.ts` - API key management (master key required)
|
||||||
- `src/routes/generate.ts` - Image generation endpoint (API key required)
|
- `src/routes/textToImage.ts` - Image generation endpoint (API key required)
|
||||||
|
- `src/routes/upload.ts` - File upload endpoint (project key required)
|
||||||
|
- `src/routes/images.ts` - Image serving endpoint (public)
|
||||||
|
|
||||||
### Middleware Stack (API Service)
|
### Middleware Stack (API Service)
|
||||||
|
|
||||||
- `src/middleware/upload.ts` - Multer configuration for file uploads (max 3 files, 5MB each)
|
- `src/middleware/upload.ts` - Multer configuration for file uploads (max 3 reference images, 5MB each; single file upload for `/api/upload`)
|
||||||
- `src/middleware/validation.ts` - Express-validator for request validation
|
- `src/middleware/validation.ts` - Express-validator for request validation
|
||||||
- `src/middleware/errorHandler.ts` - Centralized error handling and 404 responses
|
- `src/middleware/errorHandler.ts` - Centralized error handling and 404 responses
|
||||||
- `src/middleware/auth/validateApiKey.ts` - API key authentication
|
- `src/middleware/auth/validateApiKey.ts` - API key authentication
|
||||||
|
|
@ -214,6 +216,7 @@ Located at `apps/api-service/.env` - used ONLY when running `pnpm dev:api` local
|
||||||
### Protected Endpoints (API Key Required)
|
### Protected Endpoints (API Key Required)
|
||||||
|
|
||||||
- `POST /api/text-to-image` - Generate images from text only (JSON)
|
- `POST /api/text-to-image` - Generate images from text only (JSON)
|
||||||
|
- `POST /api/upload` - Upload single image file to project storage
|
||||||
- `GET /api/images` - List generated images
|
- `GET /api/images` - List generated images
|
||||||
|
|
||||||
**Authentication**: All protected endpoints require `X-API-Key` header
|
**Authentication**: All protected endpoints require `X-API-Key` header
|
||||||
|
|
@ -250,6 +253,11 @@ curl -X POST http://localhost:3000/api/text-to-image \
|
||||||
"prompt": "a sunset",
|
"prompt": "a sunset",
|
||||||
"filename": "test_image"
|
"filename": "test_image"
|
||||||
}'
|
}'
|
||||||
|
|
||||||
|
# File upload with project key
|
||||||
|
curl -X POST http://localhost:3000/api/upload \
|
||||||
|
-H "X-API-Key: YOUR_PROJECT_KEY" \
|
||||||
|
-F "file=@image.png"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Management
|
### Key Management
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { config } from 'dotenv';
|
||||||
import { Config } from './types/api';
|
import { Config } from './types/api';
|
||||||
import { textToImageRouter } from './routes/textToImage';
|
import { textToImageRouter } from './routes/textToImage';
|
||||||
import { imagesRouter } from './routes/images';
|
import { imagesRouter } from './routes/images';
|
||||||
|
import { uploadRouter } from './routes/upload';
|
||||||
import bootstrapRoutes from './routes/bootstrap';
|
import bootstrapRoutes from './routes/bootstrap';
|
||||||
import adminKeysRoutes from './routes/admin/keys';
|
import adminKeysRoutes from './routes/admin/keys';
|
||||||
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
|
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
|
||||||
|
|
@ -118,6 +119,7 @@ export const createApp = (): Application => {
|
||||||
// Protected API routes (require valid API key)
|
// Protected API routes (require valid API key)
|
||||||
app.use('/api', textToImageRouter);
|
app.use('/api', textToImageRouter);
|
||||||
app.use('/api', imagesRouter);
|
app.use('/api', imagesRouter);
|
||||||
|
app.use('/api', uploadRouter);
|
||||||
|
|
||||||
// Error handling middleware (must be last)
|
// Error handling middleware (must be last)
|
||||||
app.use(notFoundHandler);
|
app.use(notFoundHandler);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ export const upload = multer({
|
||||||
// Middleware for handling reference images
|
// Middleware for handling reference images
|
||||||
export const uploadReferenceImages: RequestHandler = upload.array('referenceImages', MAX_FILES);
|
export const uploadReferenceImages: RequestHandler = upload.array('referenceImages', MAX_FILES);
|
||||||
|
|
||||||
|
// Middleware for handling single image upload
|
||||||
|
export const uploadSingleImage: RequestHandler = upload.single('file');
|
||||||
|
|
||||||
// Error handler for multer errors
|
// Error handler for multer errors
|
||||||
export const handleUploadErrors = (error: any, _req: Request, res: any, next: any) => {
|
export const handleUploadErrors = (error: any, _req: Request, res: any, next: any) => {
|
||||||
if (error instanceof multer.MulterError) {
|
if (error instanceof multer.MulterError) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { Response, Router } from 'express';
|
||||||
|
import type { Router as RouterType } from 'express';
|
||||||
|
import { StorageFactory } from '../services/StorageFactory';
|
||||||
|
import { asyncHandler } from '../middleware/errorHandler';
|
||||||
|
import { validateApiKey } from '../middleware/auth/validateApiKey';
|
||||||
|
import { requireProjectKey } from '../middleware/auth/requireProjectKey';
|
||||||
|
import { rateLimitByApiKey } from '../middleware/auth/rateLimiter';
|
||||||
|
import { uploadSingleImage, handleUploadErrors } from '../middleware/upload';
|
||||||
|
import { UploadFileResponse } from '../types/api';
|
||||||
|
|
||||||
|
export const uploadRouter: RouterType = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/upload - Upload a single image file
|
||||||
|
*/
|
||||||
|
uploadRouter.post(
|
||||||
|
'/upload',
|
||||||
|
// Authentication middleware
|
||||||
|
validateApiKey,
|
||||||
|
requireProjectKey,
|
||||||
|
rateLimitByApiKey,
|
||||||
|
|
||||||
|
// File upload middleware
|
||||||
|
uploadSingleImage,
|
||||||
|
handleUploadErrors,
|
||||||
|
|
||||||
|
// Main handler
|
||||||
|
asyncHandler(async (req: any, res: Response) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const requestId = req.requestId;
|
||||||
|
|
||||||
|
// Check if file was provided
|
||||||
|
if (!req.file) {
|
||||||
|
const errorResponse: UploadFileResponse = {
|
||||||
|
success: false,
|
||||||
|
message: 'File upload failed',
|
||||||
|
error: 'No file provided',
|
||||||
|
};
|
||||||
|
return res.status(400).json(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract org/project slugs from validated API key
|
||||||
|
const orgId = req.apiKey?.organizationSlug || 'default';
|
||||||
|
const projectId = req.apiKey?.projectSlug!; // Guaranteed by requireProjectKey middleware
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[${timestamp}] [${requestId}] Starting file upload for org:${orgId}, project:${projectId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const file = req.file;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize storage service
|
||||||
|
const storageService = await StorageFactory.getInstance();
|
||||||
|
|
||||||
|
// Upload file to MinIO in 'uploads' category
|
||||||
|
console.log(
|
||||||
|
`[${timestamp}] [${requestId}] Uploading file: ${file.originalname} (${file.size} bytes)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadResult = await storageService.uploadFile(
|
||||||
|
orgId,
|
||||||
|
projectId,
|
||||||
|
'uploads',
|
||||||
|
file.originalname,
|
||||||
|
file.buffer,
|
||||||
|
file.mimetype,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uploadResult.success) {
|
||||||
|
const errorResponse: UploadFileResponse = {
|
||||||
|
success: false,
|
||||||
|
message: 'File upload failed',
|
||||||
|
error: uploadResult.error || 'Storage service error',
|
||||||
|
};
|
||||||
|
return res.status(500).json(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare success response
|
||||||
|
const successResponse: UploadFileResponse = {
|
||||||
|
success: true,
|
||||||
|
message: 'File uploaded successfully',
|
||||||
|
data: {
|
||||||
|
filename: uploadResult.filename,
|
||||||
|
originalName: file.originalname,
|
||||||
|
path: uploadResult.path,
|
||||||
|
url: uploadResult.url,
|
||||||
|
size: uploadResult.size,
|
||||||
|
contentType: uploadResult.contentType,
|
||||||
|
uploadedAt: timestamp,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[${timestamp}] [${requestId}] File uploaded successfully: ${uploadResult.url}`);
|
||||||
|
|
||||||
|
return res.status(200).json(successResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${timestamp}] [${requestId}] Unhandled error in upload endpoint:`, error);
|
||||||
|
|
||||||
|
const errorResponse: UploadFileResponse = {
|
||||||
|
success: false,
|
||||||
|
message: 'File upload failed',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(500).json(errorResponse);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
@ -166,6 +166,29 @@ export interface EnhancedGenerateImageRequest extends GenerateImageRequest {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload file types
|
||||||
|
export interface UploadFileRequest {
|
||||||
|
metadata?: {
|
||||||
|
description?: string;
|
||||||
|
tags?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadFileResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data?: {
|
||||||
|
filename: string;
|
||||||
|
originalName: string;
|
||||||
|
path: string;
|
||||||
|
url: string;
|
||||||
|
size: number;
|
||||||
|
contentType: string;
|
||||||
|
uploadedAt: string;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Environment configuration
|
// Environment configuration
|
||||||
export interface Config {
|
export interface Config {
|
||||||
port: number;
|
port: number;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ All authenticated endpoints (those requiring API keys) are rate limited:
|
||||||
- **Applies to:**
|
- **Applies to:**
|
||||||
- `POST /api/generate`
|
- `POST /api/generate`
|
||||||
- `POST /api/text-to-image`
|
- `POST /api/text-to-image`
|
||||||
|
- `POST /api/upload`
|
||||||
- `POST /api/enhance`
|
- `POST /api/enhance`
|
||||||
- **Not rate limited:**
|
- **Not rate limited:**
|
||||||
- Public endpoints (`GET /health`, `GET /api/info`)
|
- Public endpoints (`GET /health`, `GET /api/info`)
|
||||||
|
|
@ -88,6 +89,7 @@ Rate limit information included in response headers:
|
||||||
| `/api/admin/keys/:keyId` | DELETE | Master Key | No | Revoke API key |
|
| `/api/admin/keys/:keyId` | DELETE | Master Key | No | Revoke API key |
|
||||||
| `/api/generate` | POST | API Key | 100/hour | Generate images with files |
|
| `/api/generate` | POST | API Key | 100/hour | Generate images with files |
|
||||||
| `/api/text-to-image` | POST | API Key | 100/hour | Generate images (JSON only) |
|
| `/api/text-to-image` | POST | API Key | 100/hour | Generate images (JSON only) |
|
||||||
|
| `/api/upload` | POST | API Key | 100/hour | Upload single image file |
|
||||||
| `/api/enhance` | POST | API Key | 100/hour | Enhance text prompts |
|
| `/api/enhance` | POST | API Key | 100/hour | Enhance text prompts |
|
||||||
| `/api/images/*` | GET | None | No | Serve generated images |
|
| `/api/images/*` | GET | None | No | Serve generated images |
|
||||||
|
|
||||||
|
|
@ -438,6 +440,89 @@ curl -X POST http://localhost:3000/api/text-to-image \
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Upload File
|
||||||
|
|
||||||
|
#### `POST /api/upload`
|
||||||
|
|
||||||
|
Upload a single image file to project storage.
|
||||||
|
|
||||||
|
**Authentication:** Project API key required (master keys not allowed)
|
||||||
|
**Rate Limit:** 100 requests per hour per API key
|
||||||
|
|
||||||
|
**Content-Type:** `multipart/form-data`
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `file` | file | Yes | Single image file (PNG, JPEG, JPG, WebP) |
|
||||||
|
| `metadata` | JSON | No | Optional metadata (description, tags) |
|
||||||
|
|
||||||
|
**File Specifications:**
|
||||||
|
- **Max file size:** 5MB
|
||||||
|
- **Supported formats:** PNG, JPEG, JPG, WebP
|
||||||
|
- **Max files per request:** 1
|
||||||
|
|
||||||
|
**Example Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/upload \
|
||||||
|
-H "X-API-Key: bnt_your_project_key_here" \
|
||||||
|
-F "file=@image.png" \
|
||||||
|
-F 'metadata={"description":"Product photo","tags":["demo","test"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "File uploaded successfully",
|
||||||
|
"data": {
|
||||||
|
"filename": "image-1728561234567-a1b2c3.png",
|
||||||
|
"originalName": "image.png",
|
||||||
|
"path": "org-slug/project-slug/uploads/image-1728561234567-a1b2c3.png",
|
||||||
|
"url": "http://localhost:3000/api/images/org-slug/project-slug/uploads/image-1728561234567-a1b2c3.png",
|
||||||
|
"size": 123456,
|
||||||
|
"contentType": "image/png",
|
||||||
|
"uploadedAt": "2025-10-10T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response (400 - No file):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "File upload failed",
|
||||||
|
"error": "No file provided"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response (400 - Invalid file type):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "File validation failed",
|
||||||
|
"error": "Unsupported file type: image/gif. Allowed: PNG, JPEG, WebP"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response (400 - File too large):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "File upload failed",
|
||||||
|
"error": "File too large. Maximum size: 5MB"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Storage Details:**
|
||||||
|
- Files are stored in MinIO under: `{orgSlug}/{projectSlug}/uploads/`
|
||||||
|
- Filenames are automatically made unique with timestamp and random suffix
|
||||||
|
- Original filename is preserved in response
|
||||||
|
- Uploaded files can be accessed via the returned URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Enhance Prompt
|
### Enhance Prompt
|
||||||
|
|
||||||
#### `POST /api/enhance`
|
#### `POST /api/enhance`
|
||||||
|
|
@ -520,7 +605,7 @@ Enhance and optimize text prompts for better image generation results.
|
||||||
### Authentication Errors (401)
|
### Authentication Errors (401)
|
||||||
- `"Missing API key"` - No X-API-Key header provided
|
- `"Missing API key"` - No X-API-Key header provided
|
||||||
- `"Invalid API key"` - The provided API key is invalid, expired, or revoked
|
- `"Invalid API key"` - The provided API key is invalid, expired, or revoked
|
||||||
- **Affected endpoints:** `/api/generate`, `/api/text-to-image`, `/api/enhance`, `/api/admin/*`
|
- **Affected endpoints:** `/api/generate`, `/api/text-to-image`, `/api/upload`, `/api/enhance`, `/api/admin/*`
|
||||||
|
|
||||||
### Authorization Errors (403)
|
### Authorization Errors (403)
|
||||||
- `"Master key required"` - This endpoint requires a master API key (not project key)
|
- `"Master key required"` - This endpoint requires a master API key (not project key)
|
||||||
|
|
@ -534,7 +619,7 @@ Enhance and optimize text prompts for better image generation results.
|
||||||
|
|
||||||
### Rate Limiting Errors (429)
|
### Rate Limiting Errors (429)
|
||||||
- `"Rate limit exceeded"` - Too many requests, retry after specified time
|
- `"Rate limit exceeded"` - Too many requests, retry after specified time
|
||||||
- **Applies to:** `/api/generate`, `/api/text-to-image`, `/api/enhance`
|
- **Applies to:** `/api/generate`, `/api/text-to-image`, `/api/upload`, `/api/enhance`
|
||||||
- **Rate limit:** 100 requests per hour per API key
|
- **Rate limit:** 100 requests per hour per API key
|
||||||
- **Response includes:** `Retry-After` header with seconds until reset
|
- **Response includes:** `Retry-After` header with seconds until reset
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue