new file: .claude/settings.local.json
new file: .gitignore new file: backend/.env.example new file: backend/.gitignore new file: backend/ecosystem.config.js new file: backend/nodemon.json new file: backend/package-lock.json new file: backend/package.json new file: backend/src/config/database.ts new file: backend/src/config/dynamicSwagger.ts new file: backend/src/config/environment.ts new file: backend/src/config/swagger.ts new file: backend/src/controllers/apiKeyController.ts new file: backend/src/controllers/authController.ts new file: backend/src/controllers/databaseController.ts new file: backend/src/controllers/databaseManagementController.ts new file: backend/src/controllers/dynamicApiController.ts new file: backend/src/controllers/endpointController.ts new file: backend/src/controllers/folderController.ts new file: backend/src/controllers/logsController.ts new file: backend/src/controllers/userController.ts new file: backend/src/middleware/apiKey.ts new file: backend/src/middleware/auth.ts new file: backend/src/middleware/logging.ts new file: backend/src/migrations/001_initial_schema.sql new file: backend/src/migrations/002_add_logging.sql new file: backend/src/migrations/003_add_scripting.sql new file: backend/src/migrations/004_add_superadmin.sql new file: backend/src/migrations/run.ts new file: backend/src/migrations/seed.ts new file: backend/src/routes/apiKeys.ts new file: backend/src/routes/auth.ts new file: backend/src/routes/databaseManagement.ts new file: backend/src/routes/databases.ts new file: backend/src/routes/dynamic.ts new file: backend/src/routes/endpoints.ts new file: backend/src/routes/folders.ts new file: backend/src/routes/logs.ts new file: backend/src/routes/users.ts new file: backend/src/server.ts new file: backend/src/services/DatabasePoolManager.ts new file: backend/src/services/ScriptExecutor.ts new file: backend/src/services/SqlExecutor.ts new file: backend/src/types/index.ts new file: backend/tsconfig.json new file: frontend/.gitignore new file: frontend/index.html new file: frontend/nginx.conf new file: frontend/package-lock.json new file: frontend/package.json new file: frontend/postcss.config.js new file: frontend/src/App.tsx new file: frontend/src/components/CodeEditor.tsx
This commit is contained in:
30
backend/src/config/database.ts
Normal file
30
backend/src/config/database.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Pool } from 'pg';
|
||||
import { config } from './environment';
|
||||
|
||||
// Main database pool for KIS API Builder metadata
|
||||
export const mainPool = new Pool({
|
||||
host: config.mainDatabase.host,
|
||||
port: config.mainDatabase.port,
|
||||
database: config.mainDatabase.database,
|
||||
user: config.mainDatabase.user,
|
||||
password: config.mainDatabase.password,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
mainPool.on('error', (err) => {
|
||||
console.error('Unexpected error on idle client', err);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
export const initializeDatabase = async () => {
|
||||
try {
|
||||
const client = await mainPool.connect();
|
||||
console.log('✅ Connected to main database successfully');
|
||||
client.release();
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to database:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
237
backend/src/config/dynamicSwagger.ts
Normal file
237
backend/src/config/dynamicSwagger.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { mainPool } from './database';
|
||||
|
||||
interface SwaggerPath {
|
||||
[method: string]: {
|
||||
tags: string[];
|
||||
summary: string;
|
||||
description: string;
|
||||
security: { apiKey: [] }[];
|
||||
parameters: any[];
|
||||
responses: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface SwaggerSpec {
|
||||
openapi: string;
|
||||
info: any;
|
||||
servers: any[];
|
||||
components: any;
|
||||
paths: { [path: string]: SwaggerPath };
|
||||
tags: any[];
|
||||
}
|
||||
|
||||
export async function generateDynamicSwagger(): Promise<SwaggerSpec> {
|
||||
// Загружаем все эндпоинты из базы с полным путем папки
|
||||
const endpointsResult = await mainPool.query(`
|
||||
WITH RECURSIVE folder_path AS (
|
||||
-- Базовый случай: папки без родителя
|
||||
SELECT id, name, parent_id, name::text as full_path
|
||||
FROM folders
|
||||
WHERE parent_id IS NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Рекурсивный случай: добавляем дочерние папки
|
||||
SELECT f.id, f.name, f.parent_id, (fp.full_path || ' / ' || f.name)::text as full_path
|
||||
FROM folders f
|
||||
INNER JOIN folder_path fp ON f.parent_id = fp.id
|
||||
)
|
||||
SELECT
|
||||
e.id,
|
||||
e.name,
|
||||
e.description,
|
||||
e.method,
|
||||
e.path,
|
||||
e.parameters,
|
||||
e.is_public,
|
||||
fp.full_path as folder_name
|
||||
FROM endpoints e
|
||||
LEFT JOIN folder_path fp ON e.folder_id = fp.id
|
||||
ORDER BY fp.full_path, e.name
|
||||
`);
|
||||
|
||||
const endpoints = endpointsResult.rows;
|
||||
|
||||
// Группируем теги по папкам
|
||||
const tags: any[] = [];
|
||||
const folderSet = new Set<string>();
|
||||
|
||||
endpoints.forEach((endpoint: any) => {
|
||||
const folderName = endpoint.folder_name || 'Без категории';
|
||||
if (!folderSet.has(folderName)) {
|
||||
folderSet.add(folderName);
|
||||
tags.push({
|
||||
name: folderName,
|
||||
description: `Эндпоинты в папке "${folderName}"`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Генерируем пути
|
||||
const paths: { [path: string]: SwaggerPath } = {};
|
||||
|
||||
endpoints.forEach((endpoint: any) => {
|
||||
const method = endpoint.method.toLowerCase();
|
||||
const swaggerPath = endpoint.path.replace(/\/api\/v1/, '');
|
||||
const folderName = endpoint.folder_name || 'Без категории';
|
||||
|
||||
// Парсим параметры - PostgreSQL может вернуть как JSON строку, так и уже распарсенный объект
|
||||
const parameters: any[] = [];
|
||||
const bodyParams: any = {};
|
||||
const bodyRequired: string[] = [];
|
||||
|
||||
let params: any[] = [];
|
||||
if (endpoint.parameters) {
|
||||
if (typeof endpoint.parameters === 'string') {
|
||||
try {
|
||||
params = JSON.parse(endpoint.parameters);
|
||||
} catch (e) {
|
||||
params = [];
|
||||
}
|
||||
} else if (Array.isArray(endpoint.parameters)) {
|
||||
params = endpoint.parameters;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.length > 0) {
|
||||
params.forEach((param: any) => {
|
||||
if (param.in === 'body') {
|
||||
// Body параметры идут в requestBody
|
||||
bodyParams[param.name] = {
|
||||
type: param.type || 'string',
|
||||
description: param.description || '',
|
||||
default: param.default_value,
|
||||
};
|
||||
if (param.required) {
|
||||
bodyRequired.push(param.name);
|
||||
}
|
||||
} else {
|
||||
// Query и path параметры
|
||||
parameters.push({
|
||||
name: param.name,
|
||||
in: param.in || 'query',
|
||||
required: param.required || false,
|
||||
description: param.description || '',
|
||||
schema: {
|
||||
type: param.type || 'string',
|
||||
default: param.default_value,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!paths[swaggerPath]) {
|
||||
paths[swaggerPath] = {} as SwaggerPath;
|
||||
}
|
||||
|
||||
const endpointSpec: any = {
|
||||
tags: [folderName],
|
||||
summary: endpoint.name,
|
||||
description: endpoint.description || '',
|
||||
security: endpoint.is_public ? [] : [{ apiKey: [] }],
|
||||
parameters,
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Успешный ответ',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
data: { type: 'array', items: { type: 'object' } },
|
||||
rowCount: { type: 'number' },
|
||||
executionTime: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'400': {
|
||||
description: 'Ошибка в запросе',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'401': {
|
||||
description: 'Не авторизован (неверный или отсутствующий API ключ)',
|
||||
},
|
||||
'403': {
|
||||
description: 'Доступ запрещен (нет прав на этот эндпоинт)',
|
||||
},
|
||||
'500': {
|
||||
description: 'Внутренняя ошибка сервера',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Добавляем requestBody если есть body параметры
|
||||
if (Object.keys(bodyParams).length > 0) {
|
||||
endpointSpec.requestBody = {
|
||||
required: bodyRequired.length > 0,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: bodyParams,
|
||||
required: bodyRequired,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
paths[swaggerPath][method] = endpointSpec;
|
||||
});
|
||||
|
||||
return {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'KIS API Builder - Созданные API эндпоинты',
|
||||
version: '1.0.0',
|
||||
description: `
|
||||
# KIS API Builder - Документация
|
||||
|
||||
## Авторизация
|
||||
|
||||
Для доступа к эндпоинтам используйте API ключ, полученный от администратора системы.
|
||||
|
||||
1. Нажмите кнопку **Authorize** справа вверху
|
||||
2. Введите ваш API ключ в поле **x-api-key**
|
||||
3. Нажмите **Authorize**
|
||||
|
||||
Теперь вы можете тестировать доступные вам эндпоинты прямо в этой документации.
|
||||
|
||||
## Примечание
|
||||
|
||||
Публичные эндпоинты доступны без API ключа.
|
||||
`.trim(),
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: '/api/v1',
|
||||
description: 'API эндпоинты',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
apiKey: {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'x-api-key',
|
||||
description: 'API ключ для доступа к эндпоинтам',
|
||||
},
|
||||
},
|
||||
},
|
||||
paths,
|
||||
tags,
|
||||
};
|
||||
}
|
||||
47
backend/src/config/environment.ts
Normal file
47
backend/src/config/environment.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { DatabaseConfig } from '../types';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const config = {
|
||||
port: parseInt(process.env.PORT || '3000'),
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
|
||||
// Main database (for KIS API Builder metadata)
|
||||
mainDatabase: {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
database: process.env.DB_NAME || 'api_builder',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
},
|
||||
|
||||
// JWT
|
||||
jwt: {
|
||||
secret: (process.env.JWT_SECRET || 'default-secret-change-in-production') as string,
|
||||
expiresIn: (process.env.JWT_EXPIRES_IN || '24h') as string,
|
||||
},
|
||||
|
||||
// Rate limiting
|
||||
rateLimit: {
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes
|
||||
maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'),
|
||||
},
|
||||
|
||||
// Target databases (where API queries will execute)
|
||||
targetDatabases: parseTargetDatabases(),
|
||||
};
|
||||
|
||||
function parseTargetDatabases(): DatabaseConfig[] {
|
||||
try {
|
||||
const dbConfig = process.env.TARGET_DATABASES;
|
||||
if (!dbConfig) {
|
||||
console.warn('No TARGET_DATABASES configured. Using empty array.');
|
||||
return [];
|
||||
}
|
||||
return JSON.parse(dbConfig);
|
||||
} catch (error) {
|
||||
console.error('Error parsing TARGET_DATABASES:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
96
backend/src/config/swagger.ts
Normal file
96
backend/src/config/swagger.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import swaggerJsdoc from 'swagger-jsdoc';
|
||||
import { config } from './environment';
|
||||
|
||||
const options: swaggerJsdoc.Options = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'KIS API Builder - Dynamic API System',
|
||||
version: '1.0.0',
|
||||
description: 'System for constructing and managing dynamic API endpoints with SQL queries',
|
||||
contact: {
|
||||
name: 'KIS API Builder Support',
|
||||
},
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: `http://localhost:${config.port}`,
|
||||
description: 'Development server',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'x-api-key',
|
||||
},
|
||||
},
|
||||
schemas: {
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
username: { type: 'string' },
|
||||
email: { type: 'string', format: 'email' },
|
||||
role: { type: 'string', enum: ['admin', 'user'] },
|
||||
created_at: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
},
|
||||
Endpoint: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] },
|
||||
path: { type: 'string' },
|
||||
database_id: { type: 'string' },
|
||||
sql_query: { type: 'string' },
|
||||
parameters: { type: 'array' },
|
||||
folder_id: { type: 'string', format: 'uuid', nullable: true },
|
||||
is_public: { type: 'boolean' },
|
||||
created_at: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
},
|
||||
Folder: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
name: { type: 'string' },
|
||||
parent_id: { type: 'string', format: 'uuid', nullable: true },
|
||||
created_at: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
},
|
||||
ApiKey: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
name: { type: 'string' },
|
||||
key: { type: 'string' },
|
||||
permissions: { type: 'array', items: { type: 'string' } },
|
||||
is_active: { type: 'boolean' },
|
||||
created_at: { type: 'string', format: 'date-time' },
|
||||
expires_at: { type: 'string', format: 'date-time', nullable: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: [
|
||||
{ name: 'Authentication', description: 'Authentication endpoints' },
|
||||
{ name: 'Endpoints', description: 'API endpoint management' },
|
||||
{ name: 'Folders', description: 'Folder management for organizing endpoints' },
|
||||
{ name: 'API Keys', description: 'API key management' },
|
||||
{ name: 'Databases', description: 'Database connection information' },
|
||||
{ name: 'Dynamic API', description: 'Dynamically created API endpoints' },
|
||||
],
|
||||
},
|
||||
apis: ['./src/routes/*.ts'],
|
||||
};
|
||||
|
||||
export const swaggerSpec = swaggerJsdoc(options);
|
||||
Reference in New Issue
Block a user