Files
minicrm/api-client.js

876 lines
34 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/copy - Скопировать задачу в целевой сервис
*/
router.post('/api/client/tasks/:taskId/copy', requireAuth, async (req, res) => {
const { taskId } = req.params;
const {
target_connection_id,
target_api_url,
target_api_key,
new_assignees,
due_date,
copy_files = true
} = req.body;
const { connection_id } = req.query;
const userId = req.session.user.id;
if (!connection_id && !req.session.clientConnections) {
return res.status(400).json({
error: 'Не указан источник (текущее подключение)'
});
}
let targetUrl = target_api_url;
let targetKey = target_api_key;
if (target_connection_id && req.session.clientConnections && req.session.clientConnections[target_connection_id]) {
const connection = req.session.clientConnections[target_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 sourceConnection = req.session.clientConnections[connection_id];
if (!sourceConnection) {
return res.status(400).json({ error: 'Исходное подключение не найдено' });
}
// 1. Получаем исходную задачу
const sourceResponse = await axios.get(
`${sourceConnection.url}/api/external/tasks/${taskId}`,
{
headers: {
'X-API-Key': sourceConnection.api_key
},
timeout: 10000
}
);
if (!sourceResponse.data || !sourceResponse.data.success) {
return res.status(404).json({ error: 'Исходная задача не найдена' });
}
const sourceTask = sourceResponse.data.task;
// 2. Создаем копию задачи в целевом сервисе
const newTaskTitle = `Копия: ${sourceTask.title}`;
const taskData = {
title: newTaskTitle,
description: sourceTask.description || '',
due_date: due_date || sourceTask.due_date,
task_type: sourceTask.task_type || 'regular'
};
if (new_assignees && new_assignees.length > 0) {
taskData.assignedUsers = new_assignees;
}
const createResponse = await axios.post(
`${baseUrl}/api/external/tasks/create`,
taskData,
{
headers: {
'X-API-Key': targetKey,
'Content-Type': 'application/json'
},
timeout: 15000
}
);
if (!createResponse.data || !createResponse.data.success) {
return res.status(500).json({
error: 'Не удалось создать задачу в целевом сервисе'
});
}
const newTaskId = createResponse.data.taskId;
// 3. Копируем файлы, если нужно
const copiedFiles = [];
if (copy_files && sourceTask.files && sourceTask.files.length > 0) {
for (const file of sourceTask.files) {
try {
const fileResponse = await axios({
method: 'GET',
url: `${sourceConnection.url}/api/external/tasks/${taskId}/files/${file.id}/download`,
headers: {
'X-API-Key': sourceConnection.api_key
},
responseType: 'arraybuffer',
timeout: 30000
});
const formData = new FormData();
formData.append('files', Buffer.from(fileResponse.data), {
filename: file.filename || file.original_name || 'file',
contentType: file.file_type || 'application/octet-stream'
});
const uploadResponse = await axios.post(
`${baseUrl}/api/external/tasks/${newTaskId}/files`,
formData,
{
headers: {
...formData.getHeaders(),
'X-API-Key': targetKey
},
timeout: 60000,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
);
copiedFiles.push({
original_name: file.filename || file.original_name,
success: uploadResponse.data && uploadResponse.data.success
});
} catch (fileError) {
console.error(`❌ Ошибка копирования файла:`, fileError.message);
copiedFiles.push({
original_name: file.filename || file.original_name,
success: false,
error: fileError.message
});
}
}
}
const { logActivity } = require('./database');
if (logActivity) {
logActivity(0, userId, 'API_CLIENT_COPY_TASK',
`Скопирована задача ${taskId} из ${sourceConnection.url} в ${baseUrl}. Новый ID: ${newTaskId}`);
}
res.json({
success: true,
message: `Задача успешно скопирована${copiedFiles.length > 0 ? `, скопировано файлов: ${copiedFiles.filter(f => f.success).length}` : ''}`,
data: {
original_task_id: taskId,
new_task_id: newTaskId,
new_task_title: newTaskTitle,
target_service: baseUrl,
copied_files: copiedFiles,
assignees: new_assignees || 'не изменены'
}
});
} 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;
}
} 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
});
}
});
/**
* 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 клиент для внешних сервисов подключен');
};