Files
minicrm/api-client.js
2026-03-09 14:20:28 +05:00

676 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// api-client.js - API для внешнего клиента управления задачами
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
const axios = require('axios');
const FormData = require('form-data');
module.exports = function(app, db, upload) {
// Middleware для проверки аутентификации
const requireAuth = (req, res, next) => {
if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
next();
};
// Middleware для проверки прав администратора
const requireAdmin = (req, res, next) => {
if (!req.session || !req.session.user || req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
next();
};
// ==================== СТРАНИЦА КЛИЕНТА ====================
// GET /client - Страница клиента для работы с API
app.get('/client', requireAuth, (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'client.html'));
});
// ==================== API ДЛЯ РАБОТЫ С ВНЕШНИМИ СЕРВИСАМИ ====================
/**
* POST /api/client/connect - Проверка подключения к внешнему сервису
*/
router.post('/api/client/connect', requireAuth, async (req, res) => {
const { api_url, api_key } = req.body;
const userId = req.session.user.id;
if (!api_url || !api_key) {
return res.status(400).json({
error: 'Не указан URL сервиса или API ключ'
});
}
try {
// Нормализуем URL
const baseUrl = api_url.replace(/\/$/, '');
// Пробуем подключиться к сервису
const response = await axios.get(`${baseUrl}/api/external/tasks`, {
headers: {
'X-API-Key': api_key
},
params: {
limit: 1
},
timeout: 10000
});
if (response.data && response.data.success) {
// Сохраняем подключение в сессии
if (!req.session.clientConnections) {
req.session.clientConnections = {};
}
const connectionId = Date.now().toString();
req.session.clientConnections[connectionId] = {
id: connectionId,
name: `Подключение ${new Date().toLocaleString()}`,
url: baseUrl,
api_key: api_key,
created_at: new Date().toISOString(),
last_used: new Date().toISOString()
};
// Логируем действие
const { logActivity } = require('./database');
if (logActivity) {
logActivity(0, userId, 'API_CLIENT_CONNECT',
`Подключение к ${baseUrl} (ID: ${connectionId})`);
}
res.json({
success: true,
message: 'Подключение успешно установлено',
connection: req.session.clientConnections[connectionId],
server_info: {
tasks_count: response.data.meta?.total || 0,
user: response.data.meta?.user || 'Unknown'
}
});
} else {
res.status(400).json({
error: 'Неверный ответ от сервера',
details: response.data
});
}
} catch (error) {
console.error('❌ Ошибка подключения к внешнему сервису:', error.message);
let errorMessage = 'Ошибка подключения к серверу';
let statusCode = 500;
if (error.code === 'ECONNREFUSED') {
errorMessage = 'Сервер недоступен (отказ в соединении)';
statusCode = 503;
} else if (error.code === 'ETIMEDOUT') {
errorMessage = 'Превышено время ожидания ответа от сервера';
statusCode = 504;
} else if (error.response) {
if (error.response.status === 401) {
errorMessage = 'Неверный API ключ';
statusCode = 401;
} else {
errorMessage = `Ошибка сервера: ${error.response.status}`;
statusCode = error.response.status;
}
}
res.status(statusCode).json({
error: errorMessage,
details: error.message
});
}
});
/**
* GET /api/client/connections - Получить список сохраненных подключений
*/
router.get('/api/client/connections', requireAuth, (req, res) => {
const connections = req.session.clientConnections || {};
// Маскируем API ключи
const maskedConnections = Object.values(connections).map(conn => ({
...conn,
api_key: conn.api_key ?
conn.api_key.substring(0, 8) + '...' + conn.api_key.substring(conn.api_key.length - 8) :
null
}));
res.json({
success: true,
connections: maskedConnections
});
});
/**
* GET /api/client/connections/list - Получить список всех подключений для выбора
*/
router.get('/api/client/connections/list', requireAuth, (req, res) => {
const connections = req.session.clientConnections || {};
const connectionsList = Object.values(connections).map(conn => ({
id: conn.id,
name: conn.name,
url: conn.url,
last_used: conn.last_used
}));
res.json({
success: true,
connections: connectionsList
});
});
/**
* DELETE /api/client/connections/:id - Удалить сохраненное подключение
*/
router.delete('/api/client/connections/:id', requireAuth, (req, res) => {
const { id } = req.params;
if (req.session.clientConnections && req.session.clientConnections[id]) {
delete req.session.clientConnections[id];
const { logActivity } = require('./database');
if (logActivity) {
logActivity(0, req.session.user.id, 'API_CLIENT_DISCONNECT',
`Удалено подключение ${id}`);
}
res.json({
success: true,
message: 'Подключение удалено'
});
} else {
res.status(404).json({
error: 'Подключение не найдено'
});
}
});
/**
* GET /api/client/tasks - Получить список задач из внешнего сервиса
*/
router.get('/api/client/tasks', requireAuth, async (req, res) => {
const { connection_id, api_url, api_key, status, search, limit = 50, offset = 0 } = req.query;
const userId = req.session.user.id;
let targetUrl = api_url;
let targetKey = api_key;
if (connection_id && req.session.clientConnections && req.session.clientConnections[connection_id]) {
const connection = req.session.clientConnections[connection_id];
targetUrl = connection.url;
targetKey = connection.api_key;
connection.last_used = new Date().toISOString();
}
if (!targetUrl || !targetKey) {
return res.status(400).json({
error: 'Не указан URL сервиса или API ключ'
});
}
try {
const baseUrl = targetUrl.replace(/\/$/, '');
const params = { limit, offset };
if (status) params.status = status;
const response = await axios.get(`${baseUrl}/api/external/tasks`, {
headers: {
'X-API-Key': targetKey
},
params: params,
timeout: 15000
});
if (response.data && response.data.success) {
let tasks = response.data.tasks || [];
if (search && tasks.length > 0) {
const searchLower = search.toLowerCase();
tasks = tasks.filter(task =>
task.title.toLowerCase().includes(searchLower) ||
(task.description && task.description.toLowerCase().includes(searchLower))
);
}
const { logActivity } = require('./database');
if (logActivity) {
logActivity(0, userId, 'API_CLIENT_GET_TASKS',
`Получено ${tasks.length} задач из ${baseUrl}`);
}
res.json({
success: true,
tasks: tasks,
meta: {
total: tasks.length,
limit,
offset,
source: connection_id ? 'saved_connection' : 'direct',
server_info: response.data.meta
}
});
} else {
res.status(400).json({
error: 'Неверный ответ от сервера'
});
}
} catch (error) {
console.error('❌ Ошибка получения задач:', error.message);
let errorMessage = 'Ошибка получения задач';
let statusCode = 500;
if (error.code === 'ECONNREFUSED') {
errorMessage = 'Сервер недоступен';
statusCode = 503;
} else if (error.code === 'ETIMEDOUT') {
errorMessage = 'Превышено время ожидания';
statusCode = 504;
} else if (error.response) {
if (error.response.status === 401) {
errorMessage = 'Неверный API ключ';
statusCode = 401;
} else {
errorMessage = `Ошибка сервера: ${error.response.status}`;
statusCode = error.response.status;
}
}
res.status(statusCode).json({
error: errorMessage,
details: error.message
});
}
});
/**
* GET /api/client/tasks/:taskId - Получить детальную информацию о задаче
*/
router.get('/api/client/tasks/:taskId', requireAuth, async (req, res) => {
const { taskId } = req.params;
const { connection_id, api_url, api_key } = req.query;
let targetUrl = api_url;
let targetKey = api_key;
if (connection_id && req.session.clientConnections && req.session.clientConnections[connection_id]) {
const connection = req.session.clientConnections[connection_id];
targetUrl = connection.url;
targetKey = connection.api_key;
}
if (!targetUrl || !targetKey) {
return res.status(400).json({ error: 'Не указан URL сервиса или API ключ' });
}
try {
const baseUrl = targetUrl.replace(/\/$/, '');
const response = await axios.get(`${baseUrl}/api/external/tasks/${taskId}`, {
headers: {
'X-API-Key': targetKey
},
timeout: 10000
});
if (response.data && response.data.success) {
res.json({
success: true,
task: response.data.task
});
} else {
res.status(404).json({ error: 'Задача не найдена' });
}
} catch (error) {
console.error('❌ Ошибка получения задачи:', error.message);
let errorMessage = 'Ошибка получения задачи';
let statusCode = 500;
if (error.response) {
if (error.response.status === 404) {
errorMessage = 'Задача не найдена';
statusCode = 404;
} else if (error.response.status === 401) {
errorMessage = 'Неверный API ключ';
statusCode = 401;
} else {
errorMessage = `Ошибка сервера: ${error.response.status}`;
statusCode = error.response.status;
}
}
res.status(statusCode).json({
error: errorMessage,
details: error.message
});
}
});
/**
* PUT /api/client/tasks/:taskId/status - Изменить статус задачи
*/
router.put('/api/client/tasks/:taskId/status', requireAuth, async (req, res) => {
const { taskId } = req.params;
const { connection_id, api_url, api_key } = req.query;
const { status, comment } = req.body;
const userId = req.session.user.id;
if (!status || !['in_progress', 'completed'].includes(status)) {
return res.status(400).json({
error: 'Статус должен быть "in_progress" или "completed"'
});
}
let targetUrl = api_url;
let targetKey = api_key;
if (connection_id && req.session.clientConnections && req.session.clientConnections[connection_id]) {
const connection = req.session.clientConnections[connection_id];
targetUrl = connection.url;
targetKey = connection.api_key;
}
if (!targetUrl || !targetKey) {
return res.status(400).json({ error: 'Не указан URL сервиса или API ключ' });
}
try {
const baseUrl = targetUrl.replace(/\/$/, '');
const response = await axios.put(
`${baseUrl}/api/external/tasks/${taskId}/status`,
{ status, comment },
{
headers: {
'X-API-Key': targetKey,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
if (response.data && response.data.success) {
const { logActivity } = require('./database');
if (logActivity) {
logActivity(0, userId, 'API_CLIENT_UPDATE_STATUS',
`Статус задачи ${taskId} изменен на ${status} в ${baseUrl}`);
}
res.json({
success: true,
message: `Статус задачи ${taskId} изменен на "${status}"`,
data: response.data
});
} else {
res.status(400).json({ error: 'Не удалось изменить статус' });
}
} catch (error) {
console.error('❌ Ошибка изменения статуса:', error.message);
let errorMessage = 'Ошибка изменения статуса';
let statusCode = 500;
if (error.response) {
if (error.response.status === 401) {
errorMessage = 'Неверный API ключ';
statusCode = 401;
} else if (error.response.status === 403) {
errorMessage = 'Нет прав для изменения статуса';
statusCode = 403;
} else if (error.response.status === 404) {
errorMessage = 'Задача не найдена';
statusCode = 404;
} else {
errorMessage = error.response.data?.error || `Ошибка сервера: ${error.response.status}`;
statusCode = error.response.status;
}
}
res.status(statusCode).json({
error: errorMessage,
details: error.message
});
}
});
/**
* POST /api/client/tasks/:taskId/files - Загрузить файлы в задачу
*/
router.post('/api/client/tasks/:taskId/files', requireAuth, upload.array('files', 15), async (req, res) => {
const { taskId } = req.params;
const { connection_id, api_url, api_key } = req.query;
const userId = req.session.user.id;
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'Нет файлов для загрузки' });
}
let targetUrl = api_url;
let targetKey = api_key;
if (connection_id && req.session.clientConnections && req.session.clientConnections[connection_id]) {
const connection = req.session.clientConnections[connection_id];
targetUrl = connection.url;
targetKey = connection.api_key;
}
if (!targetUrl || !targetKey) {
req.files.forEach(file => {
if (file.path && fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
return res.status(400).json({ error: 'Не указан URL сервиса или API ключ' });
}
try {
const baseUrl = targetUrl.replace(/\/$/, '');
const formData = new FormData();
req.files.forEach(file => {
formData.append('files', fs.createReadStream(file.path), {
filename: file.originalname,
contentType: file.mimetype
});
});
const response = await axios.post(
`${baseUrl}/api/external/tasks/${taskId}/files`,
formData,
{
headers: {
...formData.getHeaders(),
'X-API-Key': targetKey
},
timeout: 60000,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
);
req.files.forEach(file => {
if (file.path && fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
if (response.data && response.data.success) {
const { logActivity } = require('./database');
if (logActivity) {
logActivity(0, userId, 'API_CLIENT_UPLOAD_FILES',
`Загружено ${req.files.length} файлов в задачу ${taskId} в ${baseUrl}`);
}
res.json({
success: true,
message: `Успешно загружено ${req.files.length} файлов`,
data: response.data
});
} else {
res.status(400).json({ error: 'Не удалось загрузить файлы' });
}
} catch (error) {
req.files.forEach(file => {
if (file.path && fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
console.error('❌ Ошибка загрузки файлов:', error.message);
let errorMessage = 'Ошибка загрузки файлов';
let statusCode = 500;
if (error.response) {
if (error.response.status === 401) {
errorMessage = 'Неверный API ключ';
statusCode = 401;
} else if (error.response.status === 403) {
errorMessage = 'Нет прав для загрузки файлов';
statusCode = 403;
} else if (error.response.status === 404) {
errorMessage = 'Задача не найдена';
statusCode = 404;
} else if (error.response.status === 413) {
errorMessage = 'Файлы слишком большие';
statusCode = 413;
} else {
errorMessage = error.response.data?.error || `Ошибка сервера: ${error.response.status}`;
statusCode = error.response.status;
}
} else if (error.code === 'ECONNREFUSED') {
errorMessage = 'Сервер недоступен';
statusCode = 503;
} else if (error.code === 'ETIMEDOUT') {
errorMessage = 'Превышено время ожидания при загрузке';
statusCode = 504;
}
res.status(statusCode).json({
error: errorMessage,
details: error.message
});
}
});
/**
* GET /api/client/tasks/:taskId/files - Получить список файлов задачи
*/
router.get('/api/client/tasks/:taskId/files', requireAuth, async (req, res) => {
const { taskId } = req.params;
const { connection_id, api_url, api_key } = req.query;
let targetUrl = api_url;
let targetKey = api_key;
if (connection_id && req.session.clientConnections && req.session.clientConnections[connection_id]) {
const connection = req.session.clientConnections[connection_id];
targetUrl = connection.url;
targetKey = connection.api_key;
}
if (!targetUrl || !targetKey) {
return res.status(400).json({ error: 'Не указан URL сервиса или API ключ' });
}
try {
const baseUrl = targetUrl.replace(/\/$/, '');
const response = await axios.get(`${baseUrl}/api/external/tasks/${taskId}`, {
headers: {
'X-API-Key': targetKey
},
timeout: 10000
});
if (response.data && response.data.success) {
res.json({
success: true,
files: response.data.task.files || []
});
} else {
res.status(404).json({ error: 'Задача не найдена' });
}
} catch (error) {
console.error('❌ Ошибка получения файлов:', error.message);
res.status(500).json({
error: 'Ошибка получения файлов',
details: error.message
});
}
});
/**
* GET /api/client/tasks/:taskId/files/:fileId/download - Скачать файл
*/
router.get('/api/client/tasks/:taskId/files/:fileId/download', requireAuth, async (req, res) => {
const { taskId, fileId } = req.params;
const { connection_id, api_url, api_key } = req.query;
let targetUrl = api_url;
let targetKey = api_key;
if (connection_id && req.session.clientConnections && req.session.clientConnections[connection_id]) {
const connection = req.session.clientConnections[connection_id];
targetUrl = connection.url;
targetKey = connection.api_key;
}
if (!targetUrl || !targetKey) {
return res.status(400).json({ error: 'Не указан URL сервиса или API ключ' });
}
try {
const baseUrl = targetUrl.replace(/\/$/, '');
const response = await axios({
method: 'GET',
url: `${baseUrl}/api/external/tasks/${taskId}/files/${fileId}/download`,
headers: {
'X-API-Key': targetKey
},
responseType: 'stream',
timeout: 30000
});
const contentType = response.headers['content-type'] || 'application/octet-stream';
const contentDisposition = response.headers['content-disposition'] || 'attachment';
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Disposition', contentDisposition);
response.data.pipe(res);
} catch (error) {
console.error('❌ Ошибка скачивания файла:', error.message);
if (error.response) {
res.status(error.response.status).json({
error: 'Ошибка при скачивании файла',
details: error.response.statusText
});
} else {
res.status(500).json({
error: 'Ошибка при скачивании файла',
details: error.message
});
}
}
});
// Подключаем роутер
app.use(router);
console.log('✅ API клиент для внешних сервисов подключен');
};