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:
107
backend/src/controllers/apiKeyController.ts
Normal file
107
backend/src/controllers/apiKeyController.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { mainPool } from '../config/database';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export const getApiKeys = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await mainPool.query(
|
||||
`SELECT id, name, key, permissions, is_active, enable_logging, created_at, expires_at
|
||||
FROM api_keys
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Get API keys error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const createApiKey = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { name, permissions, expires_at, enable_logging } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'API key name is required' });
|
||||
}
|
||||
|
||||
// Generate a secure API key
|
||||
const apiKey = `kb_${crypto.randomBytes(32).toString('hex')}`;
|
||||
|
||||
const result = await mainPool.query(
|
||||
`INSERT INTO api_keys (name, key, user_id, permissions, expires_at, enable_logging)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
apiKey,
|
||||
req.user!.id,
|
||||
JSON.stringify(permissions || []),
|
||||
expires_at || null,
|
||||
enable_logging || false,
|
||||
]
|
||||
);
|
||||
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Create API key error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateApiKey = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, permissions, is_active, expires_at, enable_logging } = req.body;
|
||||
|
||||
const result = await mainPool.query(
|
||||
`UPDATE api_keys
|
||||
SET name = COALESCE($1, name),
|
||||
permissions = COALESCE($2, permissions),
|
||||
is_active = COALESCE($3, is_active),
|
||||
expires_at = COALESCE($4, expires_at),
|
||||
enable_logging = COALESCE($5, enable_logging)
|
||||
WHERE id = $6
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
permissions ? JSON.stringify(permissions) : null,
|
||||
is_active,
|
||||
expires_at,
|
||||
enable_logging,
|
||||
id,
|
||||
]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'API key not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Update API key error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteApiKey = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await mainPool.query(
|
||||
'DELETE FROM api_keys WHERE id = $1 RETURNING id',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'API key not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'API key deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete API key error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
72
backend/src/controllers/authController.ts
Normal file
72
backend/src/controllers/authController.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Request, Response } from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt, { SignOptions } from 'jsonwebtoken';
|
||||
import { mainPool } from '../config/database';
|
||||
import { config } from '../config/environment';
|
||||
|
||||
export const login = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'Не заполнены обязательные поля' });
|
||||
}
|
||||
|
||||
// Find user
|
||||
const result = await mainPool.query(
|
||||
'SELECT id, username, password_hash, role, is_superadmin FROM users WHERE username = $1',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({ error: 'Неверные учетные данные' });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
// Verify password
|
||||
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ error: 'Неверные учетные данные' });
|
||||
}
|
||||
|
||||
// Generate token
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id },
|
||||
config.jwt.secret,
|
||||
{ expiresIn: config.jwt.expiresIn as any }
|
||||
);
|
||||
|
||||
res.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
is_superadmin: user.is_superadmin,
|
||||
},
|
||||
token,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ error: 'Ошибка сервера' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getMe = async (req: any, res: Response) => {
|
||||
try {
|
||||
const result = await mainPool.query(
|
||||
'SELECT id, username, role, is_superadmin, created_at FROM users WHERE id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Пользователь не найден' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Get me error:', error);
|
||||
res.status(500).json({ error: 'Ошибка сервера' });
|
||||
}
|
||||
};
|
||||
66
backend/src/controllers/databaseController.ts
Normal file
66
backend/src/controllers/databaseController.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { databasePoolManager } from '../services/DatabasePoolManager';
|
||||
import { sqlExecutor } from '../services/SqlExecutor';
|
||||
|
||||
export const getDatabases = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const databases = await databasePoolManager.getAllDatabaseConfigs();
|
||||
|
||||
// Don't expose sensitive information like passwords
|
||||
const sanitized = databases.map(db => ({
|
||||
id: db.id,
|
||||
name: db.name,
|
||||
type: db.type,
|
||||
host: db.host,
|
||||
port: db.port,
|
||||
database: db.database_name,
|
||||
}));
|
||||
|
||||
res.json(sanitized);
|
||||
} catch (error) {
|
||||
console.error('Get databases error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const testDatabaseConnection = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { databaseId } = req.params;
|
||||
|
||||
const isConnected = await databasePoolManager.testConnection(databaseId);
|
||||
|
||||
res.json({
|
||||
success: isConnected,
|
||||
message: isConnected ? 'Connection successful' : 'Connection failed',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Test connection error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getDatabaseTables = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { databaseId } = req.params;
|
||||
|
||||
const tables = await sqlExecutor.getAllTables(databaseId);
|
||||
|
||||
res.json({ tables });
|
||||
} catch (error: any) {
|
||||
console.error('Get tables error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const getTableSchema = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { databaseId, tableName } = req.params;
|
||||
|
||||
const schema = await sqlExecutor.getTableSchema(databaseId, tableName);
|
||||
|
||||
res.json({ schema });
|
||||
} catch (error: any) {
|
||||
console.error('Get table schema error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
185
backend/src/controllers/databaseManagementController.ts
Normal file
185
backend/src/controllers/databaseManagementController.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { mainPool } from '../config/database';
|
||||
import { databasePoolManager } from '../services/DatabasePoolManager';
|
||||
|
||||
// Только админы могут управлять базами данных
|
||||
export const getDatabases = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await mainPool.query(
|
||||
'SELECT id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at FROM databases ORDER BY name'
|
||||
);
|
||||
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Get databases error:', error);
|
||||
res.status(500).json({ error: 'Ошибка получения списка баз данных' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getDatabase = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await mainPool.query(
|
||||
'SELECT id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at FROM databases WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'База данных не найдена' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Get database error:', error);
|
||||
res.status(500).json({ error: 'Ошибка получения базы данных' });
|
||||
}
|
||||
};
|
||||
|
||||
export const createDatabase = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { name, type, host, port, database_name, username, password, ssl } = req.body;
|
||||
|
||||
if (!name || !host || !port || !database_name || !username || !password) {
|
||||
return res.status(400).json({ error: 'Не заполнены обязательные поля' });
|
||||
}
|
||||
|
||||
const result = await mainPool.query(
|
||||
`INSERT INTO databases (name, type, host, port, database_name, username, password, ssl, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true)
|
||||
RETURNING *`,
|
||||
[name, type || 'postgresql', host, port, database_name, username, password, ssl || false]
|
||||
);
|
||||
|
||||
const newDb = result.rows[0];
|
||||
|
||||
// Добавить пул подключений
|
||||
await databasePoolManager.reloadPool(newDb.id);
|
||||
|
||||
// Не возвращаем пароль
|
||||
delete newDb.password;
|
||||
|
||||
res.status(201).json(newDb);
|
||||
} catch (error: any) {
|
||||
console.error('Create database error:', error);
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({ error: 'База данных с таким именем уже существует' });
|
||||
}
|
||||
res.status(500).json({ error: 'Ошибка создания базы данных' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, type, host, port, database_name, username, password, ssl, is_active } = req.body;
|
||||
|
||||
// Если пароль не передан, не обновляем его
|
||||
let query;
|
||||
let params;
|
||||
|
||||
if (password) {
|
||||
query = `
|
||||
UPDATE databases
|
||||
SET name = COALESCE($1, name),
|
||||
type = COALESCE($2, type),
|
||||
host = COALESCE($3, host),
|
||||
port = COALESCE($4, port),
|
||||
database_name = COALESCE($5, database_name),
|
||||
username = COALESCE($6, username),
|
||||
password = $7,
|
||||
ssl = COALESCE($8, ssl),
|
||||
is_active = COALESCE($9, is_active),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $10
|
||||
RETURNING id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at
|
||||
`;
|
||||
params = [name, type, host, port, database_name, username, password, ssl, is_active, id];
|
||||
} else {
|
||||
query = `
|
||||
UPDATE databases
|
||||
SET name = COALESCE($1, name),
|
||||
type = COALESCE($2, type),
|
||||
host = COALESCE($3, host),
|
||||
port = COALESCE($4, port),
|
||||
database_name = COALESCE($5, database_name),
|
||||
username = COALESCE($6, username),
|
||||
ssl = COALESCE($7, ssl),
|
||||
is_active = COALESCE($8, is_active),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $9
|
||||
RETURNING id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at
|
||||
`;
|
||||
params = [name, type, host, port, database_name, username, ssl, is_active, id];
|
||||
}
|
||||
|
||||
const result = await mainPool.query(query, params);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'База данных не найдена' });
|
||||
}
|
||||
|
||||
// Перезагрузить пул
|
||||
await databasePoolManager.reloadPool(id);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error: any) {
|
||||
console.error('Update database error:', error);
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({ error: 'База данных с таким именем уже существует' });
|
||||
}
|
||||
res.status(500).json({ error: 'Ошибка обновления базы данных' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteDatabase = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Проверяем, используется ли база данных в эндпоинтах
|
||||
const endpointCheck = await mainPool.query(
|
||||
'SELECT COUNT(*) FROM endpoints WHERE database_id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (parseInt(endpointCheck.rows[0].count) > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Невозможно удалить базу данных, используемую в эндпоинтах'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await mainPool.query(
|
||||
'DELETE FROM databases WHERE id = $1 RETURNING id',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'База данных не найдена' });
|
||||
}
|
||||
|
||||
// Удалить пул
|
||||
databasePoolManager.removePool(id);
|
||||
|
||||
res.json({ message: 'База данных удалена успешно' });
|
||||
} catch (error) {
|
||||
console.error('Delete database error:', error);
|
||||
res.status(500).json({ error: 'Ошибка удаления базы данных' });
|
||||
}
|
||||
};
|
||||
|
||||
export const testDatabaseConnection = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const isConnected = await databasePoolManager.testConnection(id);
|
||||
|
||||
res.json({
|
||||
success: isConnected,
|
||||
message: isConnected ? 'Подключение успешно' : 'Ошибка подключения',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Test connection error:', error);
|
||||
res.status(500).json({ error: 'Ошибка тестирования подключения' });
|
||||
}
|
||||
};
|
||||
303
backend/src/controllers/dynamicApiController.ts
Normal file
303
backend/src/controllers/dynamicApiController.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
317
backend/src/controllers/endpointController.ts
Normal file
317
backend/src/controllers/endpointController.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { mainPool } from '../config/database';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getEndpoints = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { search, folder_id } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT e.*, f.name as folder_name
|
||||
FROM endpoints e
|
||||
LEFT JOIN folders f ON e.folder_id = f.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params: any[] = [];
|
||||
|
||||
if (folder_id) {
|
||||
query += ` AND e.folder_id = $${params.length + 1}`;
|
||||
params.push(folder_id);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const searchIndex = params.length + 1;
|
||||
query += ` AND (
|
||||
e.name ILIKE $${searchIndex} OR
|
||||
e.description ILIKE $${searchIndex} OR
|
||||
e.sql_query ILIKE $${searchIndex} OR
|
||||
e.path ILIKE $${searchIndex}
|
||||
)`;
|
||||
params.push(`%${search}%`);
|
||||
}
|
||||
|
||||
query += ` ORDER BY e.created_at DESC`;
|
||||
|
||||
const result = await mainPool.query(query, params);
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Get endpoints error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await mainPool.query(
|
||||
`SELECT e.*, f.name as folder_name
|
||||
FROM endpoints e
|
||||
LEFT JOIN folders f ON e.folder_id = f.id
|
||||
WHERE e.id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Endpoint not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Get endpoint error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const createEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
method,
|
||||
path,
|
||||
database_id,
|
||||
sql_query,
|
||||
parameters,
|
||||
folder_id,
|
||||
is_public,
|
||||
enable_logging,
|
||||
execution_type,
|
||||
script_language,
|
||||
script_code,
|
||||
script_queries,
|
||||
} = req.body;
|
||||
|
||||
if (!name || !method || !path) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
const execType = execution_type || 'sql';
|
||||
|
||||
// Валидация для типа SQL
|
||||
if (execType === 'sql') {
|
||||
if (!database_id || !sql_query) {
|
||||
return res.status(400).json({ error: 'Database ID and SQL query are required for SQL execution type' });
|
||||
}
|
||||
}
|
||||
|
||||
// Валидация для типа Script
|
||||
if (execType === 'script') {
|
||||
if (!script_language || !script_code || !script_queries) {
|
||||
return res.status(400).json({ error: 'Script language, code, and queries are required for script execution type' });
|
||||
}
|
||||
}
|
||||
|
||||
const result = await mainPool.query(
|
||||
`INSERT INTO endpoints (
|
||||
name, description, method, path, database_id, sql_query, parameters,
|
||||
folder_id, user_id, is_public, enable_logging,
|
||||
execution_type, script_language, script_code, script_queries
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
description || '',
|
||||
method,
|
||||
path,
|
||||
database_id || null,
|
||||
sql_query || '',
|
||||
JSON.stringify(parameters || []),
|
||||
folder_id || null,
|
||||
req.user!.id,
|
||||
is_public || false,
|
||||
enable_logging || false,
|
||||
execType,
|
||||
script_language || null,
|
||||
script_code || null,
|
||||
JSON.stringify(script_queries || []),
|
||||
]
|
||||
);
|
||||
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error: any) {
|
||||
console.error('Create endpoint error:', error);
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({ error: 'Endpoint path already exists' });
|
||||
}
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
method,
|
||||
path,
|
||||
database_id,
|
||||
sql_query,
|
||||
parameters,
|
||||
folder_id,
|
||||
is_public,
|
||||
enable_logging,
|
||||
execution_type,
|
||||
script_language,
|
||||
script_code,
|
||||
script_queries,
|
||||
} = req.body;
|
||||
|
||||
const result = await mainPool.query(
|
||||
`UPDATE endpoints
|
||||
SET name = $1,
|
||||
description = $2,
|
||||
method = $3,
|
||||
path = $4,
|
||||
database_id = $5,
|
||||
sql_query = $6,
|
||||
parameters = $7,
|
||||
folder_id = $8,
|
||||
is_public = $9,
|
||||
enable_logging = $10,
|
||||
execution_type = $11,
|
||||
script_language = $12,
|
||||
script_code = $13,
|
||||
script_queries = $14,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $15
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
description,
|
||||
method,
|
||||
path,
|
||||
database_id || null,
|
||||
sql_query,
|
||||
parameters ? JSON.stringify(parameters) : null,
|
||||
folder_id || null,
|
||||
is_public,
|
||||
enable_logging,
|
||||
execution_type,
|
||||
script_language || null,
|
||||
script_code || null,
|
||||
script_queries ? JSON.stringify(script_queries) : null,
|
||||
id,
|
||||
]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Endpoint not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error: any) {
|
||||
console.error('Update endpoint error:', error);
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({ error: 'Endpoint path already exists' });
|
||||
}
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await mainPool.query(
|
||||
'DELETE FROM endpoints WHERE id = $1 RETURNING id',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Endpoint not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Endpoint deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete endpoint error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const testEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
database_id,
|
||||
sql_query,
|
||||
parameters,
|
||||
endpoint_parameters,
|
||||
execution_type,
|
||||
script_language,
|
||||
script_code,
|
||||
script_queries
|
||||
} = req.body;
|
||||
|
||||
const execType = execution_type || 'sql';
|
||||
|
||||
if (execType === 'sql') {
|
||||
if (!database_id) {
|
||||
return res.status(400).json({ error: 'Missing database_id for SQL execution' });
|
||||
}
|
||||
if (!sql_query) {
|
||||
return res.status(400).json({ error: 'Missing sql_query' });
|
||||
}
|
||||
|
||||
// Преобразуем именованные параметры ($paramName) в позиционные ($1, $2, $3...)
|
||||
let processedQuery = sql_query;
|
||||
|
||||
if (endpoint_parameters && Array.isArray(endpoint_parameters)) {
|
||||
endpoint_parameters.forEach((param: any, index: number) => {
|
||||
const paramName = param.name;
|
||||
const position = index + 1;
|
||||
|
||||
// Заменяем все вхождения $paramName на $position
|
||||
const regex = new RegExp(`\\$${paramName}\\b`, 'g');
|
||||
processedQuery = processedQuery.replace(regex, `$${position}`);
|
||||
});
|
||||
}
|
||||
|
||||
const { sqlExecutor } = require('../services/SqlExecutor');
|
||||
const result = await sqlExecutor.executeQuery(database_id, processedQuery, parameters || []);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
rowCount: result.rowCount,
|
||||
executionTime: result.executionTime,
|
||||
});
|
||||
} else if (execType === 'script') {
|
||||
if (!script_language || !script_code) {
|
||||
return res.status(400).json({ error: 'Missing script_language or script_code' });
|
||||
}
|
||||
|
||||
// Собираем параметры из тестовых значений
|
||||
const requestParams: Record<string, any> = {};
|
||||
if (endpoint_parameters && Array.isArray(endpoint_parameters) && parameters && Array.isArray(parameters)) {
|
||||
endpoint_parameters.forEach((param: any, index: number) => {
|
||||
requestParams[param.name] = parameters[index];
|
||||
});
|
||||
}
|
||||
|
||||
const { scriptExecutor } = require('../services/ScriptExecutor');
|
||||
const scriptResult = await scriptExecutor.execute(script_language, script_code, {
|
||||
databaseId: database_id,
|
||||
scriptQueries: script_queries || [],
|
||||
requestParams,
|
||||
endpointParameters: endpoint_parameters || [],
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: scriptResult.data || scriptResult,
|
||||
rowCount: scriptResult.rowCount || (Array.isArray(scriptResult.data) ? scriptResult.data.length : 0),
|
||||
executionTime: scriptResult.executionTime || 0,
|
||||
});
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Invalid execution_type' });
|
||||
}
|
||||
} catch (error: any) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
108
backend/src/controllers/folderController.ts
Normal file
108
backend/src/controllers/folderController.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { mainPool } from '../config/database';
|
||||
|
||||
export const getFolders = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await mainPool.query(
|
||||
`SELECT f.*,
|
||||
(SELECT COUNT(*) FROM endpoints WHERE folder_id = f.id) as endpoint_count,
|
||||
(SELECT COUNT(*) FROM folders WHERE parent_id = f.id) as subfolder_count
|
||||
FROM folders f
|
||||
ORDER BY f.name`
|
||||
);
|
||||
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Get folders error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getFolder = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await mainPool.query(
|
||||
'SELECT * FROM folders WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Folder not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Get folder error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const createFolder = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { name, parent_id } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Folder name is required' });
|
||||
}
|
||||
|
||||
const result = await mainPool.query(
|
||||
`INSERT INTO folders (name, parent_id, user_id)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *`,
|
||||
[name, parent_id || null, req.user!.id]
|
||||
);
|
||||
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Create folder error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFolder = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, parent_id } = req.body;
|
||||
|
||||
const result = await mainPool.query(
|
||||
`UPDATE folders
|
||||
SET name = COALESCE($1, name),
|
||||
parent_id = COALESCE($2, parent_id),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $3
|
||||
RETURNING *`,
|
||||
[name, parent_id, id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Folder not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Update folder error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFolder = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await mainPool.query(
|
||||
'DELETE FROM folders WHERE id = $1 RETURNING id',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Folder not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Folder deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete folder error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
122
backend/src/controllers/logsController.ts
Normal file
122
backend/src/controllers/logsController.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { mainPool } from '../config/database';
|
||||
|
||||
export const getLogs = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { endpoint_id, api_key_id, limit = 100, offset = 0 } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
rl.*,
|
||||
e.name as endpoint_name,
|
||||
e.path as endpoint_path,
|
||||
ak.name as api_key_name
|
||||
FROM request_logs rl
|
||||
LEFT JOIN endpoints e ON rl.endpoint_id = e.id
|
||||
LEFT JOIN api_keys ak ON rl.api_key_id = ak.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params: any[] = [];
|
||||
let paramCount = 0;
|
||||
|
||||
if (endpoint_id) {
|
||||
paramCount++;
|
||||
query += ` AND rl.endpoint_id = $${paramCount}`;
|
||||
params.push(endpoint_id);
|
||||
}
|
||||
|
||||
if (api_key_id) {
|
||||
paramCount++;
|
||||
query += ` AND rl.api_key_id = $${paramCount}`;
|
||||
params.push(api_key_id);
|
||||
}
|
||||
|
||||
query += ` ORDER BY rl.created_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
|
||||
params.push(limit, offset);
|
||||
|
||||
const result = await mainPool.query(query, params);
|
||||
|
||||
res.json(result.rows);
|
||||
} catch (error: any) {
|
||||
console.error('Get logs error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const getLogById = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await mainPool.query(
|
||||
`SELECT
|
||||
rl.*,
|
||||
e.name as endpoint_name,
|
||||
e.path as endpoint_path,
|
||||
ak.name as api_key_name
|
||||
FROM request_logs rl
|
||||
LEFT JOIN endpoints e ON rl.endpoint_id = e.id
|
||||
LEFT JOIN api_keys ak ON rl.api_key_id = ak.id
|
||||
WHERE rl.id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Log not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error: any) {
|
||||
console.error('Get log by id error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteLog = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await mainPool.query('DELETE FROM request_logs WHERE id = $1', [id]);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
console.error('Delete log error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const clearLogs = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { endpoint_id, api_key_id, before_date } = req.body;
|
||||
|
||||
let query = 'DELETE FROM request_logs WHERE 1=1';
|
||||
const params: any[] = [];
|
||||
let paramCount = 0;
|
||||
|
||||
if (endpoint_id) {
|
||||
paramCount++;
|
||||
query += ` AND endpoint_id = $${paramCount}`;
|
||||
params.push(endpoint_id);
|
||||
}
|
||||
|
||||
if (api_key_id) {
|
||||
paramCount++;
|
||||
query += ` AND api_key_id = $${paramCount}`;
|
||||
params.push(api_key_id);
|
||||
}
|
||||
|
||||
if (before_date) {
|
||||
paramCount++;
|
||||
query += ` AND created_at < $${paramCount}`;
|
||||
params.push(before_date);
|
||||
}
|
||||
|
||||
const result = await mainPool.query(query, params);
|
||||
|
||||
res.json({ success: true, deleted: result.rowCount });
|
||||
} catch (error: any) {
|
||||
console.error('Clear logs error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
119
backend/src/controllers/userController.ts
Normal file
119
backend/src/controllers/userController.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { mainPool } from '../config/database';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
export const getUsers = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await mainPool.query(
|
||||
'SELECT id, username, role, is_superadmin, created_at, updated_at FROM users ORDER BY created_at DESC'
|
||||
);
|
||||
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Get users error:', error);
|
||||
res.status(500).json({ error: 'Ошибка получения списка пользователей' });
|
||||
}
|
||||
};
|
||||
|
||||
export const createUser = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { username, password, role, is_superadmin } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'Не заполнены обязательные поля' });
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
const result = await mainPool.query(
|
||||
`INSERT INTO users (username, password_hash, role, is_superadmin)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, username, role, is_superadmin, created_at`,
|
||||
[username, passwordHash, role || 'admin', is_superadmin || false]
|
||||
);
|
||||
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error: any) {
|
||||
console.error('Create user error:', error);
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({ error: 'Пользователь уже существует' });
|
||||
}
|
||||
res.status(500).json({ error: 'Ошибка создания пользователя' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUser = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { username, password, role, is_superadmin } = req.body;
|
||||
|
||||
let query;
|
||||
let params;
|
||||
|
||||
if (password) {
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
query = `
|
||||
UPDATE users
|
||||
SET username = COALESCE($1, username),
|
||||
password_hash = $2,
|
||||
role = COALESCE($3, role),
|
||||
is_superadmin = COALESCE($4, is_superadmin),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $5
|
||||
RETURNING id, username, role, is_superadmin, created_at, updated_at
|
||||
`;
|
||||
params = [username, passwordHash, role, is_superadmin, id];
|
||||
} else {
|
||||
query = `
|
||||
UPDATE users
|
||||
SET username = COALESCE($1, username),
|
||||
role = COALESCE($2, role),
|
||||
is_superadmin = COALESCE($3, is_superadmin),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $4
|
||||
RETURNING id, username, role, is_superadmin, created_at, updated_at
|
||||
`;
|
||||
params = [username, role, is_superadmin, id];
|
||||
}
|
||||
|
||||
const result = await mainPool.query(query, params);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Пользователь не найден' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error: any) {
|
||||
console.error('Update user error:', error);
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({ error: 'Пользователь с таким именем уже существует' });
|
||||
}
|
||||
res.status(500).json({ error: 'Ошибка обновления пользователя' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUser = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Нельзя удалить самого себя
|
||||
if (id === req.user!.id) {
|
||||
return res.status(400).json({ error: 'Нельзя удалить самого себя' });
|
||||
}
|
||||
|
||||
const result = await mainPool.query(
|
||||
'DELETE FROM users WHERE id = $1 RETURNING id',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Пользователь не найден' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Пользователь удален успешно' });
|
||||
} catch (error) {
|
||||
console.error('Delete user error:', error);
|
||||
res.status(500).json({ error: 'Ошибка удаления пользователя' });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user