diff --git a/api-client.js b/api-client.js new file mode 100644 index 0000000..09dea02 --- /dev/null +++ b/api-client.js @@ -0,0 +1,690 @@ +// 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 клиент для внешних сервисов подключен'); +}; \ No newline at end of file diff --git a/package.json b/package.json index e3e8002..e430874 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dotenv": "~16.3.1", "express": "^4.21.2", "express-session": "^1.18.2", - "form-data": "^4.0.0", + "form-data": "^4.0.5", "mime-types": "^3.0.2", "multer": "^2.0.2", "node-fetch": "~2.6.7", diff --git a/public/client.html b/public/client.html new file mode 100644 index 0000000..62ab776 --- /dev/null +++ b/public/client.html @@ -0,0 +1,1467 @@ + + + + + + Клиент внешнего API - Управление задачами + + + + +
+ +
+

+ + Клиент внешнего API +

+ +
+ + +
+
+ + Подключение к внешнему сервису +
+ +
+
+ + +
+
+ + +
+ +
+ + +
+
+ Сохраненные подключения: + +
+
+
Загрузка...
+
+
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+

+ + Задачи из внешнего сервиса +

+
+ 0 + +
+
+ + +
+
+ +

Подключитесь к серверу для загрузки задач

+
+
+ + + +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/public/navbar.js b/public/navbar.js index 05e61cd..6b2076d 100644 --- a/public/navbar.js +++ b/public/navbar.js @@ -128,6 +128,16 @@ if (currentUser && currentUser.role === 'admin') { id: "admin-btn" }); } + // 👇 Кнопка админ-панели ТОЛЬКО для admin 👇 + if (currentUser && currentUser.role === 'admin') { + navButtons.push({ + onclick: "window.location.href = '/client.html'", + className: "nav-btn profile", + icon: "fas fa-cog", + text: "client", + id: "admin-btn" + }); + } // Кнопка выхода navButtons.push({ onclick: "logout()", diff --git a/server.js b/server.js index 4550bae..6905b51 100644 --- a/server.js +++ b/server.js @@ -1543,6 +1543,11 @@ async function initializeServer() { apiKeysModule(app, db, upload); console.log('✅ Модуль API ключей подключен'); +// 10. Подключаем API клиент для внешних сервисов +const apiClient = require('./api-client'); +apiClient(app, db, upload); +console.log('✅ API клиент для внешних сервисов подключен'); + // 10. Помечаем сервер как готовый serverReady = true;