Files
minicrm/api-client.js

690 lines
28 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');
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 - Проверка подключения к внешнему сервису
* Тело запроса:
* {
* "api_url": "https://example.com",
* "api_key": "ваш_ключ"
* }
*/
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 // 10 секунд таймаут
});
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
});
});
/**
* 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 - Получить список задач из внешнего сервиса
* Параметры запроса:
* - connection_id: ID сохраненного подключения (из сессии)
* - api_url: URL сервиса (если нет connection_id)
* - api_key: API ключ (если нет connection_id)
* - status: фильтр по статусу (опционально)
* - search: поиск по тексту (опционально)
*/
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;
// Если указан connection_id, берем данные из сессии
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;
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;
}
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 - Изменить статус задачи
* Тело запроса:
* {
* "status": "in_progress" | "completed",
* "comment": "Комментарий к изменению статуса" (опционально)
* }
*/
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(/\/$/, '');
// Создаем FormData для отправки файлов
const FormData = require('form-data');
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, // 60 секунд для загрузки файлов
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 клиент для внешних сервисов подключен');
};