копия задачи, доделать

This commit is contained in:
2026-02-25 23:17:24 +05:00
parent 0e838358f0
commit 908533929b
2 changed files with 532 additions and 63 deletions

View File

@@ -4,6 +4,7 @@ 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) {
@@ -34,11 +35,6 @@ module.exports = function(app, db, upload) {
/**
* 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;
@@ -51,7 +47,7 @@ module.exports = function(app, db, upload) {
}
try {
// Нормализуем URL (убираем слеш в конце если есть)
// Нормализуем URL
const baseUrl = api_url.replace(/\/$/, '');
// Пробуем подключиться к сервису
@@ -60,9 +56,9 @@ module.exports = function(app, db, upload) {
'X-API-Key': api_key
},
params: {
limit: 1 // Запрашиваем только одну задачу для проверки
limit: 1
},
timeout: 10000 // 10 секунд таймаут
timeout: 10000
});
if (response.data && response.data.success) {
@@ -152,6 +148,25 @@ module.exports = function(app, db, upload) {
});
});
/**
* 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 - Удалить сохраненное подключение
*/
@@ -180,12 +195,6 @@ module.exports = function(app, db, upload) {
/**
* 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;
@@ -194,13 +203,11 @@ module.exports = function(app, db, upload) {
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();
}
@@ -213,11 +220,9 @@ module.exports = function(app, db, upload) {
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
@@ -229,7 +234,6 @@ module.exports = function(app, db, upload) {
if (response.data && response.data.success) {
let tasks = response.data.tasks || [];
// Дополнительная фильтрация по поиску на стороне клиента
if (search && tasks.length > 0) {
const searchLower = search.toLowerCase();
tasks = tasks.filter(task =>
@@ -238,7 +242,6 @@ module.exports = function(app, db, upload) {
);
}
// Логируем действие
const { logActivity } = require('./database');
if (logActivity) {
logActivity(0, userId, 'API_CLIENT_GET_TASKS',
@@ -296,7 +299,6 @@ module.exports = function(app, db, upload) {
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;
@@ -357,11 +359,6 @@ module.exports = function(app, db, upload) {
/**
* 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;
@@ -404,7 +401,6 @@ module.exports = function(app, db, upload) {
);
if (response.data && response.data.success) {
// Логируем действие
const { logActivity } = require('./database');
if (logActivity) {
logActivity(0, userId, 'API_CLIENT_UPDATE_STATUS',
@@ -448,6 +444,206 @@ module.exports = function(app, db, upload) {
}
});
/**
* 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 - Загрузить файлы в задачу
*/
@@ -470,7 +666,6 @@ module.exports = function(app, db, upload) {
}
if (!targetUrl || !targetKey) {
// Очищаем временные файлы
req.files.forEach(file => {
if (file.path && fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
@@ -482,8 +677,6 @@ module.exports = function(app, db, upload) {
try {
const baseUrl = targetUrl.replace(/\/$/, '');
// Создаем FormData для отправки файлов
const FormData = require('form-data');
const formData = new FormData();
req.files.forEach(file => {
@@ -493,7 +686,6 @@ module.exports = function(app, db, upload) {
});
});
// Отправляем файлы на внешний сервис
const response = await axios.post(
`${baseUrl}/api/external/tasks/${taskId}/files`,
formData,
@@ -502,13 +694,12 @@ module.exports = function(app, db, upload) {
...formData.getHeaders(),
'X-API-Key': targetKey
},
timeout: 60000, // 60 секунд для загрузки файлов
timeout: 60000,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
);
// Удаляем временные файлы
req.files.forEach(file => {
if (file.path && fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
@@ -516,7 +707,6 @@ module.exports = function(app, db, upload) {
});
if (response.data && response.data.success) {
// Логируем действие
const { logActivity } = require('./database');
if (logActivity) {
logActivity(0, userId, 'API_CLIENT_UPLOAD_FILES',
@@ -532,7 +722,6 @@ module.exports = function(app, db, upload) {
res.status(400).json({ error: 'Не удалось загрузить файлы' });
}
} catch (error) {
// Удаляем временные файлы в случае ошибки
req.files.forEach(file => {
if (file.path && fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
@@ -597,7 +786,6 @@ module.exports = function(app, db, upload) {
}
try {
// Сначала получаем детали задачи, чтобы увидеть файлы
const baseUrl = targetUrl.replace(/\/$/, '');
const response = await axios.get(`${baseUrl}/api/external/tasks/${taskId}`, {
@@ -625,7 +813,7 @@ module.exports = function(app, db, upload) {
});
/**
* GET /api/client/tasks/:taskId/files/:fileId/download - Скачать файл (через редирект)
* 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;
@@ -647,7 +835,6 @@ module.exports = function(app, db, upload) {
try {
const baseUrl = targetUrl.replace(/\/$/, '');
// Делаем запрос на скачивание файла
const response = await axios({
method: 'GET',
url: `${baseUrl}/api/external/tasks/${taskId}/files/${fileId}/download`,
@@ -658,7 +845,6 @@ module.exports = function(app, db, upload) {
timeout: 30000
});
// Проксируем ответ клиенту
const contentType = response.headers['content-type'] || 'application/octet-stream';
const contentDisposition = response.headers['content-disposition'] || 'attachment';