ts fix
This commit is contained in:
parent
35d28bca80
commit
d798faec41
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<void> => {
|
||||
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<void> => {
|
||||
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}`);
|
||||
|
|
|
|||
|
|
@ -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<void> => {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<void> => {
|
||||
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<void> => {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -151,4 +151,18 @@ export interface StorageService {
|
|||
category: 'uploads' | 'generated' | 'references',
|
||||
filename: string,
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export class AliasService {
|
|||
});
|
||||
}
|
||||
|
||||
async validateAliasForAssignment(alias: string, projectId: string, flowId?: string): Promise<void> {
|
||||
async validateAliasForAssignment(alias: string, _projectId: string, _flowId?: string): Promise<void> {
|
||||
const formatResult = validateAliasFormat(alias);
|
||||
if (!formatResult.valid) {
|
||||
throw new Error(formatResult.error!.message);
|
||||
|
|
|
|||
|
|
@ -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<GenerationWithRelations> {
|
||||
async retry(id: string, _overrides?: { prompt?: string; aspectRatio?: string }): Promise<GenerationWithRelations> {
|
||||
// Ignore overrides, regenerate with original parameters
|
||||
return await this.regenerate(id);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue