931 lines
37 KiB
JavaScript
931 lines
37 KiB
JavaScript
// 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/sync - Синхронизировать ЛОКАЛЬНУЮ задачу с целевым сервисом
|
||
*/
|
||
router.post('/api/client/tasks/:taskId/sync', requireAuth, async (req, res) => {
|
||
const { taskId } = req.params;
|
||
const {
|
||
target_connection_id,
|
||
target_api_url,
|
||
target_api_key,
|
||
sync_files = true
|
||
} = req.body;
|
||
const userId = req.session.user.id;
|
||
|
||
// Определяем целевой сервис
|
||
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 ключ)'
|
||
});
|
||
}
|
||
|
||
const baseUrl = targetUrl.replace(/\/$/, '');
|
||
|
||
try {
|
||
// 1. Получаем задачу из ЛОКАЛЬНОЙ базы данных
|
||
const localTask = await new Promise((resolve, reject) => {
|
||
db.get(`
|
||
SELECT t.*, u.name as creator_name, u.login as creator_login
|
||
FROM tasks t
|
||
LEFT JOIN users u ON t.created_by = u.id
|
||
WHERE t.id = ?
|
||
`, [taskId], (err, row) => {
|
||
if (err) reject(err);
|
||
else resolve(row);
|
||
});
|
||
});
|
||
|
||
if (!localTask) {
|
||
return res.status(404).json({ error: 'Локальная задача не найдена' });
|
||
}
|
||
|
||
// Преобразуем в формат, аналогичный внешнему API
|
||
const sourceTask = {
|
||
id: localTask.id,
|
||
title: localTask.title,
|
||
description: localTask.description || '',
|
||
due_date: localTask.due_date,
|
||
task_type: localTask.task_type || 'regular',
|
||
files: await getLocalTaskFiles(taskId)
|
||
};
|
||
|
||
// 2. Проверяем, существует ли задача в целевой системе (поиск по ID)
|
||
let existingTask = null;
|
||
try {
|
||
const checkResponse = await axios.get(
|
||
`${baseUrl}/api/external/tasks/${taskId}`,
|
||
{
|
||
headers: { 'X-API-Key': targetKey },
|
||
timeout: 5000
|
||
}
|
||
);
|
||
if (checkResponse.data && checkResponse.data.success) {
|
||
existingTask = checkResponse.data.task;
|
||
}
|
||
} catch (checkError) {
|
||
// Задача не найдена - это нормально, будем создавать новую
|
||
console.log('Задача не найдена в целевой системе, будет создана новая');
|
||
}
|
||
|
||
let result;
|
||
const syncedFiles = [];
|
||
const warnings = [];
|
||
|
||
if (existingTask) {
|
||
// 3. Обновляем существующую задачу
|
||
const updateData = {
|
||
title: sourceTask.title,
|
||
description: sourceTask.description,
|
||
due_date: sourceTask.due_date,
|
||
task_type: sourceTask.task_type
|
||
};
|
||
|
||
const updateResponse = await axios.put(
|
||
`${baseUrl}/api/external/tasks/${taskId}`,
|
||
updateData,
|
||
{
|
||
headers: {
|
||
'X-API-Key': targetKey,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
timeout: 15000
|
||
}
|
||
);
|
||
|
||
if (!updateResponse.data || !updateResponse.data.success) {
|
||
return res.status(500).json({ error: 'Не удалось обновить задачу в целевом сервисе' });
|
||
}
|
||
|
||
result = { taskId: taskId, action: 'updated' };
|
||
} else {
|
||
// 4. Создаём новую задачу
|
||
const taskData = {
|
||
title: sourceTask.title,
|
||
description: sourceTask.description,
|
||
due_date: sourceTask.due_date,
|
||
task_type: sourceTask.task_type
|
||
};
|
||
|
||
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: 'Не удалось создать задачу в целевом сервисе' });
|
||
}
|
||
|
||
result = {
|
||
taskId: createResponse.data.taskId,
|
||
action: 'created'
|
||
};
|
||
}
|
||
|
||
// 5. Синхронизируем файлы, если нужно
|
||
if (sync_files && sourceTask.files && sourceTask.files.length > 0) {
|
||
for (const file of sourceTask.files) {
|
||
try {
|
||
// Читаем файл с диска
|
||
if (!fs.existsSync(file.file_path)) {
|
||
throw new Error(`Файл не найден на диске: ${file.file_path}`);
|
||
}
|
||
const fileBuffer = fs.readFileSync(file.file_path);
|
||
const fileName = file.original_name || path.basename(file.file_path);
|
||
|
||
// Загружаем в целевую систему
|
||
const formData = new FormData();
|
||
formData.append('files', fileBuffer, {
|
||
filename: fileName,
|
||
contentType: file.file_type || 'application/octet-stream'
|
||
});
|
||
|
||
const uploadResponse = await axios.post(
|
||
`${baseUrl}/api/external/tasks/${result.taskId}/files`,
|
||
formData,
|
||
{
|
||
headers: {
|
||
...formData.getHeaders(),
|
||
'X-API-Key': targetKey
|
||
},
|
||
timeout: 60000,
|
||
maxContentLength: Infinity,
|
||
maxBodyLength: Infinity
|
||
}
|
||
);
|
||
|
||
syncedFiles.push({
|
||
original_name: fileName,
|
||
success: uploadResponse.data && uploadResponse.data.success
|
||
});
|
||
} catch (fileError) {
|
||
console.error(`❌ Ошибка синхронизации файла:`, fileError.message);
|
||
syncedFiles.push({
|
||
original_name: file.original_name || 'unknown',
|
||
success: false,
|
||
error: fileError.message
|
||
});
|
||
warnings.push(`Не удалось синхронизировать файл: ${file.original_name || file.filename}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Логируем действие
|
||
const { logActivity } = require('./database');
|
||
if (logActivity) {
|
||
logActivity(0, userId, 'API_CLIENT_SYNC_TASK',
|
||
`Локальная задача ${taskId} ${existingTask ? 'обновлена' : 'создана'} в ${baseUrl}. Новый ID: ${result.taskId}`);
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Задача успешно ${existingTask ? 'обновлена' : 'создана'} в целевой системе`,
|
||
data: {
|
||
sync_type: result.action,
|
||
original_task_id: taskId,
|
||
target_task_id: result.taskId,
|
||
target_service: baseUrl,
|
||
synced_files: syncedFiles,
|
||
assignees: [], // исполнители не передаём
|
||
warnings: warnings,
|
||
source: 'local'
|
||
}
|
||
});
|
||
|
||
} 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 клиент для внешних сервисов подключен');
|
||
};
|
||
|
||
// Вспомогательная функция для получения файлов локальной задачи
|
||
async function getLocalTaskFiles(taskId) {
|
||
return new Promise((resolve, reject) => {
|
||
db.all(`
|
||
SELECT tf.*, u.name as user_name
|
||
FROM task_files tf
|
||
LEFT JOIN users u ON tf.user_id = u.id
|
||
WHERE tf.task_id = ?
|
||
ORDER BY tf.uploaded_at DESC
|
||
`, [taskId], (err, files) => {
|
||
if (err) reject(err);
|
||
else resolve(files || []);
|
||
});
|
||
});
|
||
} |