примерно получилось

This commit is contained in:
2026-02-25 22:44:27 +05:00
parent 5b536dfbe3
commit 0e838358f0
5 changed files with 2173 additions and 1 deletions

690
api-client.js Normal file
View File

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