feat: add file upload endpoint

This commit is contained in:
Oleg Proskurin 2025-10-11 00:08:51 +07:00
parent 6944e6b750
commit 237443194f
6 changed files with 234 additions and 4 deletions

View File

@ -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

View File

@ -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);

View File

@ -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) {

View File

@ -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);
}
}),
);

View File

@ -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;

View File

@ -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