From d798faec41a5436abbfa32b793a7b4fe9ef8f822 Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Sun, 14 Dec 2025 18:11:20 +0700 Subject: [PATCH] ts fix --- apps/api-service/src/db.ts | 4 +- .../src/middleware/auth/requireMasterKey.ts | 6 ++- .../src/middleware/auth/requireProjectKey.ts | 9 ++-- .../src/middleware/auth/validateApiKey.ts | 6 ++- apps/api-service/src/routes/admin/keys.ts | 24 ++++++---- apps/api-service/src/routes/bootstrap.ts | 9 ++-- apps/api-service/src/routes/images.ts | 46 ++++++++++++++----- apps/api-service/src/routes/v1/flows.ts | 1 - apps/api-service/src/routes/v1/images.ts | 10 ++-- .../api-service/src/services/ApiKeyService.ts | 2 +- .../src/services/MinioStorageService.ts | 14 +++--- .../src/services/StorageService.ts | 14 ++++++ .../src/services/core/AliasService.ts | 2 +- .../src/services/core/GenerationService.ts | 2 +- 14 files changed, 100 insertions(+), 49 deletions(-) diff --git a/apps/api-service/src/db.ts b/apps/api-service/src/db.ts index 91cb9b9..0249ec6 100644 --- a/apps/api-service/src/db.ts +++ b/apps/api-service/src/db.ts @@ -1,4 +1,4 @@ -import { createDbClient } from '@banatie/database'; +import { createDbClient, type DbClient } from '@banatie/database'; import { config } from 'dotenv'; import path from 'path'; import { existsSync } from 'fs'; @@ -20,7 +20,7 @@ const DATABASE_URL = process.env['DATABASE_URL'] || 'postgresql://banatie_user:banatie_secure_password@localhost:5460/banatie_db'; -export const db = createDbClient(DATABASE_URL); +export const db: DbClient = createDbClient(DATABASE_URL); console.log( `[${new Date().toISOString()}] Database client initialized - ${new URL(DATABASE_URL).host}`, diff --git a/apps/api-service/src/middleware/auth/requireMasterKey.ts b/apps/api-service/src/middleware/auth/requireMasterKey.ts index d9ead9f..8d58350 100644 --- a/apps/api-service/src/middleware/auth/requireMasterKey.ts +++ b/apps/api-service/src/middleware/auth/requireMasterKey.ts @@ -6,10 +6,11 @@ import { Request, Response, NextFunction } from 'express'; */ export function requireMasterKey(req: Request, res: Response, next: NextFunction): void { if (!req.apiKey) { - return res.status(401).json({ + res.status(401).json({ error: 'Authentication required', message: 'This endpoint requires authentication', }); + return; } if (req.apiKey.keyType !== 'master') { @@ -17,10 +18,11 @@ 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}`, ); - return res.status(403).json({ + res.status(403).json({ error: 'Master key required', message: 'This endpoint requires a master API key', }); + return; } next(); diff --git a/apps/api-service/src/middleware/auth/requireProjectKey.ts b/apps/api-service/src/middleware/auth/requireProjectKey.ts index 86930d0..7dd2498 100644 --- a/apps/api-service/src/middleware/auth/requireProjectKey.ts +++ b/apps/api-service/src/middleware/auth/requireProjectKey.ts @@ -7,27 +7,30 @@ import { Request, Response, NextFunction } from 'express'; export function requireProjectKey(req: Request, res: Response, next: NextFunction): void { // This middleware assumes validateApiKey has already run and attached req.apiKey if (!req.apiKey) { - return res.status(401).json({ + res.status(401).json({ error: 'Authentication required', message: 'API key validation must be performed first', }); + return; } // Block master keys from generation endpoints if (req.apiKey.keyType === 'master') { - return res.status(403).json({ + res.status(403).json({ error: 'Forbidden', message: 'Master keys cannot be used for image generation. Please use a project-specific API key.', }); + return; } // Ensure project key has required IDs if (!req.apiKey.projectId) { - return res.status(400).json({ + res.status(400).json({ error: 'Invalid API key', message: 'Project key must be associated with a project', }); + return; } console.log( diff --git a/apps/api-service/src/middleware/auth/validateApiKey.ts b/apps/api-service/src/middleware/auth/validateApiKey.ts index 4853241..a69db91 100644 --- a/apps/api-service/src/middleware/auth/validateApiKey.ts +++ b/apps/api-service/src/middleware/auth/validateApiKey.ts @@ -23,20 +23,22 @@ export async function validateApiKey( const providedKey = req.headers['x-api-key'] as string; if (!providedKey) { - return res.status(401).json({ + res.status(401).json({ error: 'Missing API key', message: 'Provide your API key via X-API-Key header', }); + return; } try { const apiKey = await apiKeyService.validateKey(providedKey); if (!apiKey) { - return res.status(401).json({ + res.status(401).json({ error: 'Invalid API key', message: 'The provided API key is invalid, expired, or revoked', }); + return; } // Attach to request for use in routes diff --git a/apps/api-service/src/routes/admin/keys.ts b/apps/api-service/src/routes/admin/keys.ts index 1a43bd5..99b2956 100644 --- a/apps/api-service/src/routes/admin/keys.ts +++ b/apps/api-service/src/routes/admin/keys.ts @@ -1,9 +1,9 @@ -import express from 'express'; +import express, { Router } from 'express'; import { ApiKeyService } from '../../services/ApiKeyService'; import { validateApiKey } from '../../middleware/auth/validateApiKey'; import { requireMasterKey } from '../../middleware/auth/requireMasterKey'; -const router = express.Router(); +const router: Router = express.Router(); const apiKeyService = new ApiKeyService(); // All admin routes require master key @@ -14,12 +14,12 @@ router.use(requireMasterKey); * Create a new API key * POST /api/admin/keys */ -router.post('/', async (req, res) => { +router.post('/', async (req, res): Promise => { try { const { type, - projectId, - organizationId, + projectId: _projectId, + organizationId: _organizationId, organizationSlug, projectSlug, organizationName, @@ -30,24 +30,27 @@ router.post('/', async (req, res) => { // Validation if (!type || !['master', 'project'].includes(type)) { - return res.status(400).json({ + res.status(400).json({ error: 'Invalid type', message: 'Type must be either "master" or "project"', }); + return; } if (type === 'project' && !projectSlug) { - return res.status(400).json({ + res.status(400).json({ error: 'Missing projectSlug', message: 'Project keys require a projectSlug', }); + return; } if (type === 'project' && !organizationSlug) { - return res.status(400).json({ + res.status(400).json({ error: 'Missing organizationSlug', message: 'Project keys require an organizationSlug', }); + return; } // Create key @@ -148,17 +151,18 @@ router.get('/', async (req, res) => { * Revoke an API key * DELETE /api/admin/keys/:keyId */ -router.delete('/:keyId', async (req, res) => { +router.delete('/:keyId', async (req, res): Promise => { try { const { keyId } = req.params; const success = await apiKeyService.revokeKey(keyId); if (!success) { - return res.status(404).json({ + res.status(404).json({ error: 'Key not found', message: 'The specified API key does not exist', }); + return; } console.log(`[${new Date().toISOString()}] API key revoked: ${keyId} - by: ${req.apiKey!.id}`); diff --git a/apps/api-service/src/routes/bootstrap.ts b/apps/api-service/src/routes/bootstrap.ts index 5542c07..758c078 100644 --- a/apps/api-service/src/routes/bootstrap.ts +++ b/apps/api-service/src/routes/bootstrap.ts @@ -1,7 +1,7 @@ -import express from 'express'; +import express, { Router } from 'express'; import { ApiKeyService } from '../services/ApiKeyService'; -const router = express.Router(); +const router: Router = express.Router(); const apiKeyService = new ApiKeyService(); /** @@ -10,17 +10,18 @@ const apiKeyService = new ApiKeyService(); * * POST /api/bootstrap/initial-key */ -router.post('/initial-key', async (req, res) => { +router.post('/initial-key', async (_req, res): Promise => { try { // Check if any keys already exist const hasKeys = await apiKeyService.hasAnyKeys(); if (hasKeys) { console.warn(`[${new Date().toISOString()}] Bootstrap attempt when keys already exist`); - return res.status(403).json({ + res.status(403).json({ error: 'Bootstrap not allowed', message: 'API keys already exist. Use /api/admin/keys to create new keys.', }); + return; } // Create first master key diff --git a/apps/api-service/src/routes/images.ts b/apps/api-service/src/routes/images.ts index 988bb6a..225c069 100644 --- a/apps/api-service/src/routes/images.ts +++ b/apps/api-service/src/routes/images.ts @@ -1,11 +1,12 @@ import { Router, Request, Response } 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'; -export const imagesRouter = Router(); +export const imagesRouter: RouterType = Router(); /** * GET /api/images/:orgId/:projectId/:category/:filename @@ -13,15 +14,25 @@ export const imagesRouter = Router(); */ imagesRouter.get( '/images/:orgId/:projectId/:category/:filename', - asyncHandler(async (req: Request, res: Response) => { + asyncHandler(async (req: Request, res: Response): Promise => { const { orgId, projectId, category, filename } = req.params; + // Validate required params (these are guaranteed by route pattern) + if (!orgId || !projectId || !category || !filename) { + res.status(400).json({ + success: false, + message: 'Missing required parameters', + }); + return; + } + // Validate category if (!['uploads', 'generated', 'references'].includes(category)) { - return res.status(400).json({ + res.status(400).json({ success: false, message: 'Invalid category', }); + return; } const storageService = await StorageFactory.getInstance(); @@ -36,10 +47,11 @@ imagesRouter.get( ); if (!exists) { - return res.status(404).json({ + res.status(404).json({ success: false, message: 'File not found', }); + return; } // Determine content type from filename @@ -62,7 +74,8 @@ imagesRouter.get( // Handle conditional requests (304 Not Modified) const ifNoneMatch = req.headers['if-none-match']; if (ifNoneMatch === `"${orgId}-${projectId}-${filename}"`) { - return res.status(304).end(); // Not Modified + res.status(304).end(); // Not Modified + return; } // Stream the file directly through our API (memory efficient) @@ -88,7 +101,7 @@ imagesRouter.get( fileStream.pipe(res); } catch (error) { console.error('Failed to stream file:', error); - return res.status(404).json({ + res.status(404).json({ success: false, message: 'File not found', }); @@ -102,15 +115,25 @@ imagesRouter.get( */ imagesRouter.get( '/images/url/:orgId/:projectId/:category/:filename', - asyncHandler(async (req: Request, res: Response) => { + asyncHandler(async (req: Request, res: Response): Promise => { const { orgId, projectId, category, filename } = req.params; const { expiry = '3600' } = req.query; // Default 1 hour + // Validate required params (these are guaranteed by route pattern) + if (!orgId || !projectId || !category || !filename) { + res.status(400).json({ + success: false, + message: 'Missing required parameters', + }); + return; + } + if (!['uploads', 'generated', 'references'].includes(category)) { - return res.status(400).json({ + res.status(400).json({ success: false, message: 'Invalid category', }); + return; } const storageService = await StorageFactory.getInstance(); @@ -124,14 +147,14 @@ imagesRouter.get( parseInt(expiry as string, 10), ); - return res.json({ + res.json({ success: true, url: presignedUrl, expiresIn: parseInt(expiry as string, 10), }); } catch (error) { console.error('Failed to generate presigned URL:', error); - return res.status(404).json({ + res.status(404).json({ success: false, message: 'File not found or access denied', }); @@ -159,11 +182,12 @@ imagesRouter.get( // Validate query parameters if (isNaN(limit) || isNaN(offset)) { - return res.status(400).json({ + res.status(400).json({ success: false, message: 'Invalid query parameters', error: 'limit and offset must be valid numbers', }); + return; } // Extract org/project from validated API key diff --git a/apps/api-service/src/routes/v1/flows.ts b/apps/api-service/src/routes/v1/flows.ts index 4772d0d..1dcbfa7 100644 --- a/apps/api-service/src/routes/v1/flows.ts +++ b/apps/api-service/src/routes/v1/flows.ts @@ -9,7 +9,6 @@ import { validateAndNormalizePagination } from '@/utils/validators'; import { buildPaginatedResponse } from '@/utils/helpers'; import { toFlowResponse, toGenerationResponse, toImageResponse } from '@/types/responses'; import type { - CreateFlowResponse, ListFlowsResponse, GetFlowResponse, UpdateFlowAliasesResponse, diff --git a/apps/api-service/src/routes/v1/images.ts b/apps/api-service/src/routes/v1/images.ts index 06cea6a..6d50472 100644 --- a/apps/api-service/src/routes/v1/images.ts +++ b/apps/api-service/src/routes/v1/images.ts @@ -172,24 +172,26 @@ imagesRouter.post( pendingFlowId = null; } else { // Specific flowId provided - ensure flow exists (eager creation) - finalFlowId = flowId; + // Use flowId directly since TypeScript has narrowed it to string in this branch + const providedFlowId = flowId; + finalFlowId = providedFlowId; pendingFlowId = null; // Check if flow exists, create if not const existingFlow = await db.query.flows.findFirst({ - where: eq(flows.id, finalFlowId), + where: eq(flows.id, providedFlowId), }); if (!existingFlow) { await db.insert(flows).values({ - id: finalFlowId, + id: providedFlowId, projectId, aliases: {}, meta: {}, }); // Link any pending images to this new flow - await service.linkPendingImagesToFlow(finalFlowId, projectId); + await service.linkPendingImagesToFlow(providedFlowId, projectId); } } diff --git a/apps/api-service/src/services/ApiKeyService.ts b/apps/api-service/src/services/ApiKeyService.ts index c97eb8e..2a609b7 100644 --- a/apps/api-service/src/services/ApiKeyService.ts +++ b/apps/api-service/src/services/ApiKeyService.ts @@ -1,6 +1,6 @@ import crypto from 'crypto'; import { db } from '../db'; -import { apiKeys, organizations, projects, type ApiKey, type NewApiKey } from '@banatie/database'; +import { apiKeys, organizations, projects, type ApiKey } from '@banatie/database'; import { eq, and, desc } from 'drizzle-orm'; // Extended API key type with slugs for storage paths diff --git a/apps/api-service/src/services/MinioStorageService.ts b/apps/api-service/src/services/MinioStorageService.ts index a40225d..40e9901 100644 --- a/apps/api-service/src/services/MinioStorageService.ts +++ b/apps/api-service/src/services/MinioStorageService.ts @@ -203,7 +203,7 @@ export class MinioStorageService implements StorageService { console.log(`Uploading file to: ${this.bucketName}/${filePath}`); - const result = await this.client.putObject( + await this.client.putObject( this.bucketName, filePath, buffer, @@ -315,10 +315,11 @@ export class MinioStorageService implements StorageService { // Replace internal Docker hostname with public URL if configured if (this.publicUrl) { - const clientEndpoint = this.client.host + (this.client.port ? `:${this.client.port}` : ''); - const publicEndpoint = this.publicUrl.replace(/^https?:\/\//, ''); + // Access protected properties via type assertion for URL replacement + const client = this.client as unknown as { host: string; port: number; protocol: string }; + const clientEndpoint = client.host + (client.port ? `:${client.port}` : ''); - return presignedUrl.replace(`${this.client.protocol}//${clientEndpoint}`, this.publicUrl); + return presignedUrl.replace(`${client.protocol}//${clientEndpoint}`, this.publicUrl); } return presignedUrl; @@ -351,12 +352,11 @@ export class MinioStorageService implements StorageService { } files.push({ - key: `${this.bucketName}/${obj.name}`, filename, contentType: metadata.metaData?.['content-type'] || 'application/octet-stream', size: obj.size || 0, - url: this.getPublicUrl(orgId, projectId, categoryFromPath, filename), - createdAt: obj.lastModified || new Date(), + lastModified: obj.lastModified || new Date(), + path: obj.name, }); } catch (error) {} }); diff --git a/apps/api-service/src/services/StorageService.ts b/apps/api-service/src/services/StorageService.ts index 36c4967..f721b97 100644 --- a/apps/api-service/src/services/StorageService.ts +++ b/apps/api-service/src/services/StorageService.ts @@ -151,4 +151,18 @@ export interface StorageService { category: 'uploads' | 'generated' | 'references', filename: string, ): Promise; + + /** + * Get the public URL for a file + * @param orgId Organization ID + * @param projectId Project ID + * @param category File category + * @param filename Filename + */ + getPublicUrl( + orgId: string, + projectId: string, + category: 'uploads' | 'generated' | 'references', + filename: string, + ): string; } diff --git a/apps/api-service/src/services/core/AliasService.ts b/apps/api-service/src/services/core/AliasService.ts index 4141b0e..8099303 100644 --- a/apps/api-service/src/services/core/AliasService.ts +++ b/apps/api-service/src/services/core/AliasService.ts @@ -190,7 +190,7 @@ export class AliasService { }); } - async validateAliasForAssignment(alias: string, projectId: string, flowId?: string): Promise { + async validateAliasForAssignment(alias: string, _projectId: string, _flowId?: string): Promise { const formatResult = validateAliasFormat(alias); if (!formatResult.valid) { throw new Error(formatResult.error!.message); diff --git a/apps/api-service/src/services/core/GenerationService.ts b/apps/api-service/src/services/core/GenerationService.ts index 7fcd418..2f6aa67 100644 --- a/apps/api-service/src/services/core/GenerationService.ts +++ b/apps/api-service/src/services/core/GenerationService.ts @@ -532,7 +532,7 @@ export class GenerationService { } // Keep retry() for backward compatibility, delegate to regenerate() - async retry(id: string, overrides?: { prompt?: string; aspectRatio?: string }): Promise { + async retry(id: string, _overrides?: { prompt?: string; aspectRatio?: string }): Promise { // Ignore overrides, regenerate with original parameters return await this.regenerate(id); }