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,303 @@
import { Response } from 'express';
import { ApiKeyRequest } from '../middleware/apiKey';
import { mainPool } from '../config/database';
import { sqlExecutor } from '../services/SqlExecutor';
import { scriptExecutor } from '../services/ScriptExecutor';
import { EndpointParameter, ScriptQuery } from '../types';
export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response) => {
const startTime = Date.now();
let shouldLog = false;
let endpointId: string | null = null;
try {
// Extract the path from the request (remove /api/v1 prefix)
const requestPath = req.path; // This already has the path without /api/v1
const requestMethod = req.method.toUpperCase();
// Fetch endpoint configuration by path and method
const endpointResult = await mainPool.query(
'SELECT * FROM endpoints WHERE path = $1 AND method = $2',
[requestPath, requestMethod]
);
if (endpointResult.rows.length === 0) {
return res.status(404).json({
error: 'Endpoint not found',
path: requestPath,
method: requestMethod
});
}
const endpoint = endpointResult.rows[0];
endpointId = endpoint.id;
// Check if logging is enabled (on endpoint OR on API key, but log only once)
const endpointLogging = endpoint.enable_logging || false;
const apiKeyLogging = req.apiKey?.enable_logging || false;
shouldLog = endpointLogging || apiKeyLogging;
// Check if endpoint is public or if API key has permission
if (!endpoint.is_public) {
if (!req.apiKey) {
return res.status(401).json({ error: 'API key required for this endpoint' });
}
let hasPermission = req.apiKey.permissions.includes(endpointId!) ||
req.apiKey.permissions.includes('*');
// If no direct permission, check folder permissions
if (!hasPermission && endpoint.folder_id) {
// Check if this folder or any parent folder has permission
let currentFolderId: string | null = endpoint.folder_id;
while (currentFolderId && !hasPermission) {
if (req.apiKey.permissions.includes(`folder:${currentFolderId}`)) {
hasPermission = true;
break;
}
// Get parent folder
const folderResult = await mainPool.query(
'SELECT parent_id FROM folders WHERE id = $1',
[currentFolderId]
);
currentFolderId = folderResult.rows.length > 0 ? folderResult.rows[0].parent_id : null;
}
}
if (!hasPermission) {
return res.status(403).json({ error: 'Access denied to this endpoint' });
}
}
// Parse parameters - PostgreSQL может вернуть как JSON строку, так и уже распарсенный объект
let parameters: EndpointParameter[] = [];
if (endpoint.parameters) {
if (typeof endpoint.parameters === 'string') {
try {
parameters = JSON.parse(endpoint.parameters);
} catch (e) {
parameters = [];
}
} else if (Array.isArray(endpoint.parameters)) {
parameters = endpoint.parameters;
}
}
// Build request parameters object
const requestParams: Record<string, any> = {};
// Extract and validate parameters from request
for (const param of parameters) {
let value;
if (param.in === 'query') {
value = req.query[param.name];
} else if (param.in === 'body') {
value = req.body[param.name];
} else if (param.in === 'path') {
value = req.params[param.name];
}
// Use default value if not provided
if (value === undefined || value === null) {
if (param.required) {
return res.status(400).json({
error: `Missing required parameter: ${param.name}`,
});
}
value = param.default_value;
}
// Type conversion
if (value !== undefined && value !== null) {
switch (param.type) {
case 'number':
value = Number(value);
if (isNaN(value)) {
return res.status(400).json({
error: `Parameter ${param.name} must be a number`,
});
}
break;
case 'boolean':
value = value === 'true' || value === true;
break;
case 'date':
value = new Date(value);
if (isNaN(value.getTime())) {
return res.status(400).json({
error: `Parameter ${param.name} must be a valid date`,
});
}
break;
}
}
requestParams[param.name] = value;
}
let result;
const executionType = endpoint.execution_type || 'sql';
if (executionType === 'script') {
// Execute script
const scriptLanguage = endpoint.script_language;
const scriptCode = endpoint.script_code;
let scriptQueries: ScriptQuery[] = [];
if (endpoint.script_queries) {
if (typeof endpoint.script_queries === 'string') {
try {
scriptQueries = JSON.parse(endpoint.script_queries);
} catch (e) {
scriptQueries = [];
}
} else if (Array.isArray(endpoint.script_queries)) {
scriptQueries = endpoint.script_queries;
}
}
if (!scriptLanguage || !scriptCode) {
return res.status(500).json({ error: 'Script configuration is incomplete' });
}
const scriptResult = await scriptExecutor.execute(scriptLanguage, scriptCode, {
databaseId: endpoint.database_id,
scriptQueries,
requestParams,
endpointParameters: parameters,
});
result = {
rows: scriptResult.data || scriptResult,
rowCount: scriptResult.rowCount || (Array.isArray(scriptResult.data) ? scriptResult.data.length : 0),
executionTime: scriptResult.executionTime || 0,
};
} else {
// Execute SQL query
const queryParams: any[] = [];
parameters.forEach((param) => {
queryParams.push(requestParams[param.name]);
});
// Преобразуем именованные параметры ($paramName) в позиционные ($1, $2, $3...)
let processedQuery = endpoint.sql_query;
parameters.forEach((param, index) => {
const paramName = param.name;
const position = index + 1;
// Заменяем все вхождения $paramName на $position
const regex = new RegExp(`\\$${paramName}\\b`, 'g');
processedQuery = processedQuery.replace(regex, `$${position}`);
});
result = await sqlExecutor.executeQuery(
endpoint.database_id,
processedQuery,
queryParams
);
}
const responseData = {
success: true,
data: result.rows,
rowCount: result.rowCount,
executionTime: result.executionTime,
};
// Log if needed
if (shouldLog && endpointId) {
const executionTime = Date.now() - startTime;
await logRequest({
endpoint_id: endpointId,
api_key_id: req.apiKey?.id || null,
method: req.method,
path: req.path,
request_params: req.query || {},
request_body: req.body || {},
response_status: 200,
response_data: responseData,
execution_time: executionTime,
error_message: null,
ip_address: req.ip || req.socket.remoteAddress || 'unknown',
user_agent: req.headers['user-agent'] || 'unknown',
});
}
res.json(responseData);
} catch (error: any) {
console.error('Dynamic API execution error:', error);
const errorResponse = {
success: false,
error: error.message,
};
// Log error if needed
if (shouldLog && endpointId) {
const executionTime = Date.now() - startTime;
await logRequest({
endpoint_id: endpointId,
api_key_id: req.apiKey?.id || null,
method: req.method,
path: req.path,
request_params: req.query || {},
request_body: req.body || {},
response_status: 500,
response_data: errorResponse,
execution_time: executionTime,
error_message: error.message,
ip_address: req.ip || req.socket.remoteAddress || 'unknown',
user_agent: req.headers['user-agent'] || 'unknown',
});
}
res.status(500).json(errorResponse);
}
};
async function logRequest(data: {
endpoint_id: string;
api_key_id: string | null;
method: string;
path: string;
request_params: any;
request_body: any;
response_status: number;
response_data: any;
execution_time: number;
error_message: string | null;
ip_address: string;
user_agent: string;
}) {
try {
await mainPool.query(
`INSERT INTO request_logs (
endpoint_id, api_key_id, method, path,
request_params, request_body, response_status,
response_data, execution_time, error_message,
ip_address, user_agent
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
[
data.endpoint_id,
data.api_key_id,
data.method,
data.path,
JSON.stringify(data.request_params),
JSON.stringify(data.request_body),
data.response_status,
JSON.stringify(data.response_data),
data.execution_time,
data.error_message,
data.ip_address,
data.user_agent,
]
);
} catch (error) {
console.error('Failed to log request:', error);
}
}