// 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 клиент для внешних сервисов подключен'); };