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:
GEgorov
2025-10-07 00:04:04 +03:00
commit 8943f5a070
79 changed files with 17032 additions and 0 deletions

View 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;
}
};

View 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,
};
}

View 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 [];
}
}

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