Compare commits

..

5 Commits

Author SHA1 Message Date
Oleg Proskurin b0943606e1 fix: ts issue 2025-10-12 23:01:52 +07:00
Oleg Proskurin 7416c6d441 chore: code cleanup 2025-10-12 22:59:24 +07:00
Oleg Proskurin e0924f9c4b fix: get images endpoint 2025-10-12 22:55:37 +07:00
Oleg Proskurin 3cbb366a9d chore: update docs 2025-10-12 22:19:10 +07:00
Oleg Proskurin 9a9c7260e2 fix: return correct codes 2025-10-12 22:13:34 +07:00
8 changed files with 31 additions and 64 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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