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,146 @@
import { Pool } from 'pg';
import { DatabaseConfig } from '../types';
import { mainPool } from '../config/database';
class DatabasePoolManager {
private pools: Map<string, Pool> = new Map();
async initialize() {
// Load databases from DB instead of env
await this.loadDatabasesFromDB();
}
private async loadDatabasesFromDB() {
try {
const result = await mainPool.query(
'SELECT * FROM databases WHERE is_active = true'
);
for (const row of result.rows) {
const dbConfig: DatabaseConfig = {
id: row.id,
name: row.name,
type: row.type,
host: row.host,
port: row.port,
database_name: row.database_name,
username: row.username,
password: row.password,
ssl: row.ssl,
is_active: row.is_active,
};
this.addPool(dbConfig);
}
console.log(`✅ Loaded ${result.rows.length} database connection(s) from DB`);
} catch (error) {
console.error('❌ Failed to load databases from DB:', error);
}
}
addPool(dbConfig: DatabaseConfig) {
if (this.pools.has(dbConfig.id)) {
console.warn(`Pool with id ${dbConfig.id} already exists. Skipping.`);
return;
}
const pool = new Pool({
host: dbConfig.host,
port: dbConfig.port,
database: dbConfig.database_name,
user: dbConfig.username,
password: dbConfig.password,
ssl: dbConfig.ssl ? { rejectUnauthorized: false } : false,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
pool.on('error', (err) => {
console.error(`Database pool error for ${dbConfig.id}:`, err);
});
this.pools.set(dbConfig.id, pool);
console.log(`✅ Pool created for database: ${dbConfig.name} (${dbConfig.id})`);
}
removePool(databaseId: string) {
const pool = this.pools.get(databaseId);
if (pool) {
pool.end();
this.pools.delete(databaseId);
console.log(`Pool removed for database: ${databaseId}`);
}
}
getPool(databaseId: string): Pool | undefined {
return this.pools.get(databaseId);
}
async getAllDatabaseConfigs(): Promise<DatabaseConfig[]> {
try {
const result = await mainPool.query(
'SELECT id, name, type, host, port, database_name, is_active FROM databases WHERE is_active = true'
);
return result.rows;
} catch (error) {
console.error('Error fetching databases:', error);
return [];
}
}
async testConnection(databaseId: string): Promise<boolean> {
const pool = this.getPool(databaseId);
if (!pool) {
return false;
}
try {
const client = await pool.connect();
client.release();
return true;
} catch (error) {
console.error(`Connection test failed for ${databaseId}:`, error);
return false;
}
}
async reloadPool(databaseId: string) {
// Remove old pool
this.removePool(databaseId);
// Load new config from DB
const result = await mainPool.query(
'SELECT * FROM databases WHERE id = $1 AND is_active = true',
[databaseId]
);
if (result.rows.length > 0) {
const row = result.rows[0];
const dbConfig: DatabaseConfig = {
id: row.id,
name: row.name,
type: row.type,
host: row.host,
port: row.port,
database_name: row.database_name,
username: row.username,
password: row.password,
ssl: row.ssl,
is_active: row.is_active,
};
this.addPool(dbConfig);
}
}
async closeAll() {
const promises = Array.from(this.pools.values()).map((pool) => pool.end());
await Promise.all(promises);
this.pools.clear();
console.log('All database pools closed');
}
}
export const databasePoolManager = new DatabasePoolManager();

View File

@@ -0,0 +1,218 @@
import { spawn } from 'child_process';
import { sqlExecutor } from './SqlExecutor';
import { ScriptQuery, EndpointParameter } from '../types';
interface ScriptContext {
databaseId: string;
scriptQueries: ScriptQuery[];
requestParams: Record<string, any>;
endpointParameters: EndpointParameter[];
}
export class ScriptExecutor {
/**
* Выполняет JavaScript скрипт
*/
async executeJavaScript(code: string, context: ScriptContext): Promise<any> {
try {
// Создаем функцию execQuery, доступную в скрипте
const execQuery = async (queryName: string, additionalParams: Record<string, any> = {}) => {
const query = context.scriptQueries.find(q => q.name === queryName);
if (!query) {
throw new Error(`Query '${queryName}' not found`);
}
const allParams = { ...context.requestParams, ...additionalParams };
let processedQuery = query.sql;
const paramValues: any[] = [];
const paramMatches = query.sql.match(/\$\w+/g) || [];
const uniqueParams = [...new Set(paramMatches.map(p => p.substring(1)))];
uniqueParams.forEach((paramName, index) => {
const regex = new RegExp(`\\$${paramName}\\b`, 'g');
processedQuery = processedQuery.replace(regex, `$${index + 1}`);
const value = allParams[paramName];
paramValues.push(value !== undefined ? value : null);
});
const dbId = (query as any).database_id || context.databaseId;
const result = await sqlExecutor.executeQuery(dbId, processedQuery, paramValues);
return {
data: result.rows,
rowCount: result.rowCount,
executionTime: result.executionTime,
};
};
// Создаем асинхронную функцию из кода пользователя
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
const userFunction = new AsyncFunction('params', 'execQuery', code);
// Устанавливаем таймаут
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Script execution timeout (30s)')), 30000);
});
// Выполняем скрипт с таймаутом
const result = await Promise.race([
userFunction(context.requestParams, execQuery),
timeoutPromise
]);
return result;
} catch (error: any) {
throw new Error(`JavaScript execution error: ${error.message}`);
}
}
/**
* Выполняет Python скрипт в отдельном процессе
*/
async executePython(code: string, context: ScriptContext): Promise<any> {
return new Promise((resolve, reject) => {
// Создаем wrapper код для Python
const wrapperCode = `
import json
import sys
from datetime import datetime
# Параметры запроса
params = ${JSON.stringify(context.requestParams)}
# Функция для выполнения SQL запросов
def exec_query(query_name, additional_params=None):
if additional_params is None:
additional_params = {}
# Отправляем запрос на выполнение через stdout
request = {
'type': 'exec_query',
'query_name': query_name,
'additional_params': additional_params
}
print('__QUERY_REQUEST__' + json.dumps(request) + '__END_REQUEST__', file=sys.stderr, flush=True)
# Читаем результат
response_line = input()
response = json.loads(response_line)
if 'error' in response:
raise Exception(response['error'])
return response
# Функция-обертка для пользовательского кода
def __user_script():
${code.split('\n').map(line => line.trim() === '' ? '' : ' ' + line).join('\n')}
# Выполняем пользовательский код и выводим результат
result = __user_script()
print(json.dumps(result))
`;
const python = spawn('python', ['-c', wrapperCode]);
let output = '';
let errorOutput = '';
let queryRequests: any[] = [];
python.stdout.on('data', (data) => {
output += data.toString();
});
python.stderr.on('data', async (data) => {
const text = data.toString();
errorOutput += text;
// Проверяем на запросы к БД
const requestMatches = text.matchAll(/__QUERY_REQUEST__(.*?)__END_REQUEST__/g);
for (const match of requestMatches) {
try {
const request = JSON.parse(match[1]);
// Выполняем запрос
const query = context.scriptQueries.find(q => q.name === request.query_name);
if (!query) {
python.stdin.write(JSON.stringify({ error: `Query '${request.query_name}' not found` }) + '\n');
continue;
}
const allParams = { ...context.requestParams, ...request.additional_params };
// Преобразуем параметры
let processedQuery = query.sql;
const paramValues: any[] = [];
const paramMatches = query.sql.match(/\$\w+/g) || [];
const uniqueParams = [...new Set(paramMatches.map(p => p.substring(1)))];
uniqueParams.forEach((paramName, index) => {
const regex = new RegExp(`\\$${paramName}\\b`, 'g');
processedQuery = processedQuery.replace(regex, `$${index + 1}`);
const value = allParams[paramName];
paramValues.push(value !== undefined ? value : null);
});
// Используем database_id из запроса, если указан, иначе из контекста
const dbId = (query as any).database_id || context.databaseId;
const result = await sqlExecutor.executeQuery(
dbId,
processedQuery,
paramValues
);
python.stdin.write(JSON.stringify({
data: result.rows,
rowCount: result.rowCount,
executionTime: result.executionTime,
}) + '\n');
} catch (error: any) {
python.stdin.write(JSON.stringify({ error: error.message }) + '\n');
}
}
});
python.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Python execution error: ${errorOutput}`));
} else {
try {
// Последняя строка вывода - результат
const lines = output.trim().split('\n');
const resultLine = lines[lines.length - 1];
const result = JSON.parse(resultLine);
resolve(result);
} catch (error) {
reject(new Error(`Failed to parse Python output: ${output}`));
}
}
});
// Таймаут 30 секунд
setTimeout(() => {
python.kill();
reject(new Error('Python script execution timeout (30s)'));
}, 30000);
});
}
/**
* Выполняет скрипт на выбранном языке
*/
async execute(
language: 'javascript' | 'python',
code: string,
context: ScriptContext
): Promise<any> {
if (language === 'javascript') {
return this.executeJavaScript(code, context);
} else if (language === 'python') {
return this.executePython(code, context);
} else {
throw new Error(`Unsupported script language: ${language}`);
}
}
}
export const scriptExecutor = new ScriptExecutor();

View File

@@ -0,0 +1,96 @@
import { QueryResult } from '../types';
import { databasePoolManager } from './DatabasePoolManager';
export class SqlExecutor {
async executeQuery(
databaseId: string,
sqlQuery: string,
params: any[] = []
): Promise<QueryResult> {
const pool = databasePoolManager.getPool(databaseId);
if (!pool) {
throw new Error(`Database with id ${databaseId} not found or not configured`);
}
const startTime = Date.now();
try {
// Security: Prevent multiple statements and dangerous commands
this.validateQuery(sqlQuery);
const result = await pool.query(sqlQuery, params);
const executionTime = Date.now() - startTime;
return {
rows: result.rows,
rowCount: result.rowCount || 0,
executionTime,
};
} catch (error: any) {
console.error('SQL Execution Error:', error);
throw new Error(`SQL Error: ${error.message}`);
}
}
private validateQuery(sqlQuery: string) {
const normalized = sqlQuery.trim().toLowerCase();
// Prevent multiple statements (basic check)
if (normalized.includes(';') && normalized.indexOf(';') < normalized.length - 1) {
throw new Error('Multiple SQL statements are not allowed');
}
// Prevent dangerous commands (you can extend this list)
const dangerousCommands = ['drop', 'truncate', 'delete from', 'alter', 'create', 'grant', 'revoke'];
const isDangerous = dangerousCommands.some(cmd => normalized.startsWith(cmd));
if (isDangerous && !normalized.startsWith('select')) {
// For safety, you might want to allow only SELECT queries
// Or implement a whitelist/permission system for write operations
console.warn('Potentially dangerous query detected:', sqlQuery);
}
}
async testQuery(databaseId: string, sqlQuery: string): Promise<{ success: boolean; error?: string }> {
try {
await this.executeQuery(databaseId, sqlQuery);
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async getTableSchema(databaseId: string, tableName: string): Promise<any[]> {
const query = `
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM
information_schema.columns
WHERE
table_name = $1
ORDER BY
ordinal_position;
`;
const result = await this.executeQuery(databaseId, query, [tableName]);
return result.rows;
}
async getAllTables(databaseId: string): Promise<string[]> {
const query = `
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
`;
const result = await this.executeQuery(databaseId, query);
return result.rows.map(row => row.table_name);
}
}
export const sqlExecutor = new SqlExecutor();