Compare commits
5 Commits
691e472a2e
...
b0943606e1
| Author | SHA1 | Date |
|---|---|---|
|
|
b0943606e1 | |
|
|
7416c6d441 | |
|
|
e0924f9c4b | |
|
|
3cbb366a9d | |
|
|
9a9c7260e2 |
|
|
@ -250,7 +250,7 @@ See `secrets.env.example` in each directory for template.
|
||||||
|
|
||||||
- `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
|
- `POST /api/upload` - Upload single image file to project storage
|
||||||
- `GET /api/images` - List generated images
|
- `GET /api/images/generated` - List generated images
|
||||||
|
|
||||||
**Authentication**: All protected endpoints require `X-API-Key` header
|
**Authentication**: All protected endpoints require `X-API-Key` header
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,12 +121,12 @@ X-API-Key: {{$dotenv apiKey}}
|
||||||
|
|
||||||
### 12. List Generated Images
|
### 12. List Generated Images
|
||||||
# @name listImages
|
# @name listImages
|
||||||
GET {{$dotenv baseUrl}}/api/images
|
GET {{$dotenv baseUrl}}/api/images/generated
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-API-Key: {{$dotenv apiKey}}
|
X-API-Key: {{$dotenv apiKey}}
|
||||||
|
|
||||||
### 13. List Images with Filters (limit 5, generated only)
|
### 13. List Images with Filters (limit 5, generated only)
|
||||||
GET {{$dotenv baseUrl}}/api/images?limit=5&category=generated
|
GET {{$dotenv baseUrl}}/api/images/generated?limit=5
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-API-Key: {{$dotenv apiKey}}
|
X-API-Key: {{$dotenv apiKey}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
## 6. ERROR HANDLING ENDPOINTS (4/4) ✅
|
|
||||||
# Test Case Expected Actual Status
|
|
||||||
15 No API Key 401 ❌ 200 с error ⚠️ Не правильный статус
|
|
||||||
16 Invalid API Key 401 ❌ 200 с error ⚠️ Не правильный статус
|
|
||||||
17 Missing Prompt 400 ❌ 200 с error ⚠️ Не правильный статус
|
|
||||||
19 Wrong Key Type 403 ❌ 200 с error ⚠️ Не правильный статус
|
|
||||||
Детали:
|
|
||||||
✅ Error messages корректные
|
|
||||||
⚠️ HTTP status codes всегда 200 (должны быть 401, 400, 403)
|
|
||||||
Примеры ответов:
|
|
||||||
// Test 15 - No API Key
|
|
||||||
{
|
|
||||||
"error": "Missing API key",
|
|
||||||
"message": "Provide your API key via X-API-Key header"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 16 - Invalid Key
|
|
||||||
{
|
|
||||||
"error": "Invalid API key",
|
|
||||||
"message": "The provided API key is invalid, expired, or revoked"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 17 - Missing Prompt
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Validation failed",
|
|
||||||
"message": "Prompt is required"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 19 - Wrong Key Type
|
|
||||||
{
|
|
||||||
"error": "Master key required",
|
|
||||||
"message": "This endpoint requires a master API key"
|
|
||||||
}
|
|
||||||
|
|
||||||
## Docs
|
|
||||||
Endpoint /api/images не существует
|
|
||||||
В документации упоминается /api/images
|
|
||||||
Реально работает только /api/images/generated
|
|
||||||
Нужно: Обновить документацию или добавить endpoint
|
|
||||||
|
|
@ -6,11 +6,10 @@ import { Request, Response, NextFunction } from 'express';
|
||||||
*/
|
*/
|
||||||
export function requireMasterKey(req: Request, res: Response, next: NextFunction): void {
|
export function requireMasterKey(req: Request, res: Response, next: NextFunction): void {
|
||||||
if (!req.apiKey) {
|
if (!req.apiKey) {
|
||||||
res.status(401).json({
|
return res.status(401).json({
|
||||||
error: 'Authentication required',
|
error: 'Authentication required',
|
||||||
message: 'This endpoint requires authentication',
|
message: 'This endpoint requires authentication',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.apiKey.keyType !== 'master') {
|
if (req.apiKey.keyType !== 'master') {
|
||||||
|
|
@ -18,11 +17,10 @@ export function requireMasterKey(req: Request, res: Response, next: NextFunction
|
||||||
`[${new Date().toISOString()}] Non-master key attempted admin action: ${req.apiKey.id} (${req.apiKey.keyType}) - ${req.path}`,
|
`[${new Date().toISOString()}] Non-master key attempted admin action: ${req.apiKey.id} (${req.apiKey.keyType}) - ${req.path}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
res.status(403).json({
|
return res.status(403).json({
|
||||||
error: 'Master key required',
|
error: 'Master key required',
|
||||||
message: 'This endpoint requires a master API key',
|
message: 'This endpoint requires a master API key',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|
|
||||||
|
|
@ -7,30 +7,27 @@ import { Request, Response, NextFunction } from 'express';
|
||||||
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
|
// This middleware assumes validateApiKey has already run and attached req.apiKey
|
||||||
if (!req.apiKey) {
|
if (!req.apiKey) {
|
||||||
res.status(401).json({
|
return res.status(401).json({
|
||||||
error: 'Authentication required',
|
error: 'Authentication required',
|
||||||
message: 'API key validation must be performed first',
|
message: 'API key validation must be performed first',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block master keys from generation endpoints
|
// Block master keys from generation endpoints
|
||||||
if (req.apiKey.keyType === 'master') {
|
if (req.apiKey.keyType === 'master') {
|
||||||
res.status(403).json({
|
return res.status(403).json({
|
||||||
error: 'Forbidden',
|
error: 'Forbidden',
|
||||||
message:
|
message:
|
||||||
'Master keys cannot be used for image generation. Please use a project-specific API key.',
|
'Master keys cannot be used for image generation. Please use a project-specific API key.',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure project key has required IDs
|
// Ensure project key has required IDs
|
||||||
if (!req.apiKey.projectId) {
|
if (!req.apiKey.projectId) {
|
||||||
res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'Invalid API key',
|
error: 'Invalid API key',
|
||||||
message: 'Project key must be associated with a project',
|
message: 'Project key must be associated with a project',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,20 @@ export async function validateApiKey(
|
||||||
const providedKey = req.headers['x-api-key'] as string;
|
const providedKey = req.headers['x-api-key'] as string;
|
||||||
|
|
||||||
if (!providedKey) {
|
if (!providedKey) {
|
||||||
res.status(401).json({
|
return res.status(401).json({
|
||||||
error: 'Missing API key',
|
error: 'Missing API key',
|
||||||
message: 'Provide your API key via X-API-Key header',
|
message: 'Provide your API key via X-API-Key header',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiKey = await apiKeyService.validateKey(providedKey);
|
const apiKey = await apiKeyService.validateKey(providedKey);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
res.status(401).json({
|
return res.status(401).json({
|
||||||
error: 'Invalid API key',
|
error: 'Invalid API key',
|
||||||
message: 'The provided API key is invalid, expired, or revoked',
|
message: 'The provided API key is invalid, expired, or revoked',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach to request for use in routes
|
// Attach to request for use in routes
|
||||||
|
|
|
||||||
|
|
@ -430,7 +430,7 @@ export class MinioStorageService implements StorageService {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const stream = this.client.listObjects(this.bucketName, searchPrefix, true);
|
const stream = this.client.listObjects(this.bucketName, searchPrefix, true);
|
||||||
|
|
||||||
stream.on('data', async (obj) => {
|
stream.on('data', (obj) => {
|
||||||
if (!obj.name || !obj.size) return;
|
if (!obj.name || !obj.size) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -439,14 +439,24 @@ export class MinioStorageService implements StorageService {
|
||||||
|
|
||||||
if (!filename) return;
|
if (!filename) return;
|
||||||
|
|
||||||
const metadata = await this.client.statObject(this.bucketName, obj.name);
|
// Infer content type from file extension (more efficient than statObject)
|
||||||
|
const ext = filename.toLowerCase().split('.').pop();
|
||||||
|
const contentType =
|
||||||
|
{
|
||||||
|
png: 'image/png',
|
||||||
|
jpg: 'image/jpeg',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
gif: 'image/gif',
|
||||||
|
webp: 'image/webp',
|
||||||
|
svg: 'image/svg+xml',
|
||||||
|
}[ext || ''] || 'application/octet-stream';
|
||||||
|
|
||||||
files.push({
|
files.push({
|
||||||
filename,
|
filename,
|
||||||
size: obj.size,
|
size: obj.size,
|
||||||
contentType: metadata.metaData?.['content-type'] || 'application/octet-stream',
|
contentType,
|
||||||
lastModified: obj.lastModified || new Date(),
|
lastModified: obj.lastModified || new Date(),
|
||||||
etag: metadata.etag,
|
etag: obj.etag || '',
|
||||||
path: obj.name,
|
path: obj.name,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -454,7 +464,10 @@ export class MinioStorageService implements StorageService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('end', () => resolve(files));
|
stream.on('end', () => {
|
||||||
|
resolve(files);
|
||||||
|
});
|
||||||
|
|
||||||
stream.on('error', (error) => {
|
stream.on('error', (error) => {
|
||||||
console.error('[MinIO listFiles] Stream error:', error);
|
console.error('[MinIO listFiles] Stream error:', error);
|
||||||
reject(error);
|
reject(error);
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ All authenticated endpoints (those requiring API keys) are rate limited:
|
||||||
- Public endpoints (`GET /health`, `GET /api/info`)
|
- Public endpoints (`GET /health`, `GET /api/info`)
|
||||||
- Bootstrap endpoint (`POST /api/bootstrap/initial-key`)
|
- Bootstrap endpoint (`POST /api/bootstrap/initial-key`)
|
||||||
- Admin endpoints (require master key, but no rate limit)
|
- Admin endpoints (require master key, but no rate limit)
|
||||||
- Image serving endpoints (`GET /api/images/*`)
|
- Image serving endpoints (`GET /api/images/:orgId/:projectId/:category/:filename`)
|
||||||
|
|
||||||
Rate limit information included in response headers:
|
Rate limit information included in response headers:
|
||||||
- `X-RateLimit-Limit`: Maximum requests per window
|
- `X-RateLimit-Limit`: Maximum requests per window
|
||||||
|
|
@ -91,7 +91,8 @@ Rate limit information included in response headers:
|
||||||
| `/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/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/:orgId/:projectId/:category/:filename` | GET | None | No | Serve specific image file |
|
||||||
|
| `/api/images/generated` | GET | API Key | 100/hour | List generated images |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue