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**:
|
||||
- `src/routes/bootstrap.ts` - Bootstrap initial master key (one-time)
|
||||
- `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)
|
||||
|
||||
- `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/errorHandler.ts` - Centralized error handling and 404 responses
|
||||
- `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)
|
||||
|
||||
- `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
|
||||
|
||||
**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",
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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';
|
||||
|
|
@ -118,6 +119,7 @@ export const createApp = (): Application => {
|
|||
// 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);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ export const upload = multer({
|
|||
// Middleware for handling reference images
|
||||
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
|
||||
export const handleUploadErrors = (error: any, _req: Request, res: any, next: any) => {
|
||||
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
|
||||
export interface Config {
|
||||
port: number;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ All authenticated endpoints (those requiring API keys) are rate limited:
|
|||
- **Applies to:**
|
||||
- `POST /api/generate`
|
||||
- `POST /api/text-to-image`
|
||||
- `POST /api/upload`
|
||||
- `POST /api/enhance`
|
||||
- **Not rate limited:**
|
||||
- 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/generate` | POST | API Key | 100/hour | Generate images with files |
|
||||
| `/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/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
|
||||
|
||||
#### `POST /api/enhance`
|
||||
|
|
@ -520,7 +605,7 @@ Enhance and optimize text prompts for better image generation results.
|
|||
### Authentication Errors (401)
|
||||
- `"Missing API key"` - No X-API-Key header provided
|
||||
- `"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)
|
||||
- `"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 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
|
||||
- **Response includes:** `Retry-After` header with seconds until reset
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue