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:
146
backend/src/services/DatabasePoolManager.ts
Normal file
146
backend/src/services/DatabasePoolManager.ts
Normal 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();
|
||||
218
backend/src/services/ScriptExecutor.ts
Normal file
218
backend/src/services/ScriptExecutor.ts
Normal 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();
|
||||
96
backend/src/services/SqlExecutor.ts
Normal file
96
backend/src/services/SqlExecutor.ts
Normal 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();
|
||||
Reference in New Issue
Block a user