diff --git a/api-keys.js b/api-keys.js
new file mode 100644
index 0000000..6e6d3d0
--- /dev/null
+++ b/api-keys.js
@@ -0,0 +1,1176 @@
+// api-keys.js - API для управления ключами доступа к задачам с логированием
+
+const express = require('express');
+const crypto = require('crypto');
+const path = require('path');
+const fs = require('fs');
+const router = express.Router();
+
+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();
+ };
+
+ // Функция для логирования действий API
+ const logApiAction = (apiKeyId, userId, action, details, req, statusCode = 200, responseTime = 0) => {
+ const timestamp = new Date().toISOString();
+ const ip = req.ip || req.connection.remoteAddress;
+ const userAgent = req.get('User-Agent') || 'Unknown';
+
+ const logEntry = {
+ timestamp,
+ api_key_id: apiKeyId,
+ user_id: userId,
+ action,
+ details,
+ ip,
+ user_agent: userAgent,
+ method: req.method,
+ path: req.originalUrl,
+ status_code: statusCode,
+ response_time: responseTime
+ };
+
+ // Цветное логирование в консоль
+ const colors = {
+ GET: '\x1b[32m', // зеленый
+ POST: '\x1b[33m', // желтый
+ PUT: '\x1b[34m', // синий
+ DELETE: '\x1b[31m', // красный
+ reset: '\x1b[0m'
+ };
+
+ const methodColor = colors[req.method] || '\x1b[36m';
+
+ console.log('\n' + '='.repeat(50));
+ console.log(`${methodColor}🔑 API ${req.method} ${req.originalUrl}${colors.reset}`);
+ console.log(`📅 ${timestamp}`);
+ console.log(`👤 Пользователь: ID ${userId} (${req.apiUser?.name || 'Unknown'})`);
+ console.log(`🔑 API Key ID: ${apiKeyId}`);
+ console.log(`📋 Действие: ${action}`);
+ console.log(`📝 Детали: ${details}`);
+ console.log(`🌐 IP: ${ip}`);
+ console.log(`📱 User-Agent: ${userAgent.substring(0, 50)}...`);
+ console.log(`📊 Статус: ${statusCode} | Время: ${responseTime}ms`);
+ console.log('='.repeat(50));
+
+ // Сохраняем в базу данных
+ db.run(`
+ INSERT INTO api_logs (
+ api_key_id, user_id, action, details, ip, user_agent,
+ method, path, response_status, response_time, created_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `, [
+ apiKeyId, userId, action, details, ip, userAgent,
+ req.method, req.originalUrl, statusCode, responseTime, timestamp
+ ], (err) => {
+ if (err) {
+ console.error('❌ Ошибка сохранения лога API:', err.message);
+ }
+ });
+
+ // Логируем в общую активность для важных действий
+ const { logActivity } = require('./database');
+ if (logActivity && (
+ action.includes('STATUS') ||
+ action.includes('FILE') ||
+ action.includes('CREATE') ||
+ action.includes('UPDATE') ||
+ action.includes('DELETE')
+ )) {
+ const taskId = extractTaskIdFromDetails(details) || extractTaskIdFromUrl(req.originalUrl);
+ if (taskId) {
+ logActivity(
+ taskId,
+ userId,
+ `API_${action}`,
+ `[API Key ${apiKeyId}] ${details}`
+ );
+ }
+ }
+ };
+
+ // Вспомогательная функция для извлечения ID задачи из деталей
+ function extractTaskIdFromDetails(details) {
+ const match = details.match(/задачи?\s*[#№]?\s*(\d+)/i);
+ return match ? parseInt(match[1]) : null;
+ }
+
+ // Вспомогательная функция для извлечения ID задачи из URL
+ function extractTaskIdFromUrl(url) {
+ const match = url.match(/\/tasks?\/(\d+)/i);
+ return match ? parseInt(match[1]) : null;
+ }
+
+ // Функция для логирования попыток аутентификации
+ const logApiAttempt = (apiKeyId, userId, action, details, req, statusCode, responseTime) => {
+ const timestamp = new Date().toISOString();
+ const ip = req.ip || req.connection.remoteAddress;
+ const userAgent = req.get('User-Agent') || 'Unknown';
+
+ const color = statusCode >= 400 ? '\x1b[31m' : '\x1b[33m';
+
+ console.log('\n' + '='.repeat(50));
+ console.log(`${color}🔑 API ATTEMPT ${req.method} ${req.originalUrl}${'\x1b[0m'}`);
+ console.log(`📅 ${timestamp}`);
+ console.log(`👤 Пользователь ID: ${userId || 'N/A'}`);
+ console.log(`🔑 API Key ID: ${apiKeyId || 'N/A'}`);
+ console.log(`📋 Действие: ${action}`);
+ console.log(`📝 Детали: ${details}`);
+ console.log(`🌐 IP: ${ip}`);
+ console.log(`📊 Статус: ${statusCode} | Время: ${responseTime}ms`);
+ console.log('='.repeat(50));
+
+ if (apiKeyId && userId) {
+ db.run(`
+ INSERT INTO api_logs (
+ api_key_id, user_id, action, details, ip, user_agent,
+ method, path, response_status, response_time, created_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `, [
+ apiKeyId, userId, action, details, ip, userAgent,
+ req.method, req.originalUrl, statusCode, responseTime, timestamp
+ ], (err) => {
+ if (err) {
+ console.error('❌ Ошибка сохранения лога попытки:', err.message);
+ }
+ });
+ }
+ };
+
+ // Создание таблиц для API ключей и логов
+function createApiKeysTables() {
+ return new Promise((resolve, reject) => {
+ // Таблица API ключей
+ db.run(`
+ CREATE TABLE IF NOT EXISTS api_keys (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ key TEXT NOT NULL UNIQUE,
+ user_id INTEGER NOT NULL,
+ description TEXT,
+ allowed_ips TEXT,
+ is_active BOOLEAN DEFAULT true,
+ last_used_at DATETIME,
+ last_used_ip TEXT,
+ created_by INTEGER NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (created_by) REFERENCES users(id)
+ )
+ `, (err) => {
+ if (err) {
+ console.error('❌ Ошибка создания таблицы api_keys:', err.message);
+ reject(err);
+ return;
+ }
+
+ // Проверяем и добавляем отсутствующие колонки
+ db.all("PRAGMA table_info(api_keys)", (err, columns) => {
+ if (err) {
+ console.error('❌ Ошибка получения информации о таблице:', err);
+ } else {
+ const columnNames = columns.map(c => c.name);
+
+ // Добавляем last_used_ip если отсутствует
+ if (!columnNames.includes('last_used_ip')) {
+ db.run("ALTER TABLE api_keys ADD COLUMN last_used_ip TEXT", (err) => {
+ if (err) {
+ console.error('❌ Ошибка добавления колонки last_used_ip:', err.message);
+ } else {
+ console.log('✅ Добавлена колонка last_used_ip в таблицу api_keys');
+ }
+ });
+ }
+ }
+ });
+
+ // Таблица для логов API действий
+ db.run(`
+ CREATE TABLE IF NOT EXISTS api_logs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ api_key_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ action TEXT NOT NULL,
+ details TEXT,
+ ip TEXT,
+ user_agent TEXT,
+ method TEXT,
+ path TEXT,
+ response_status INTEGER,
+ response_time INTEGER,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ )
+ `, (err) => {
+ if (err) {
+ console.error('❌ Ошибка создания таблицы api_logs:', err.message);
+ reject(err);
+ return;
+ }
+
+ // Таблица для связи API ключей с задачами
+ db.run(`
+ CREATE TABLE IF NOT EXISTS api_task_mappings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ api_key_id INTEGER NOT NULL,
+ task_id INTEGER NOT NULL,
+ external_task_id TEXT,
+ synced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ last_status TEXT,
+ FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE,
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ UNIQUE(api_key_id, task_id)
+ )
+ `, (err) => {
+ if (err) {
+ console.error('❌ Ошибка создания таблицы api_task_mappings:', err.message);
+ reject(err);
+ return;
+ }
+
+ // Индексы
+ db.run('CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id)');
+ db.run('CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key)');
+ db.run('CREATE INDEX IF NOT EXISTS idx_api_logs_api_key_id ON api_logs(api_key_id)');
+ db.run('CREATE INDEX IF NOT EXISTS idx_api_logs_created_at ON api_logs(created_at)');
+ db.run('CREATE INDEX IF NOT EXISTS idx_api_task_mappings_task_id ON api_task_mappings(task_id)');
+
+ console.log('✅ Таблицы для API ключей и логов созданы');
+ resolve();
+ });
+ });
+ });
+ });
+}
+
+ // Генерация уникального API ключа
+ function generateApiKey() {
+ return crypto.randomBytes(32).toString('hex');
+ }
+
+ // Middleware для проверки API ключа с логированием
+ const requireApiKey = (req, res, next) => {
+ const startTime = Date.now();
+ const apiKey = req.headers['x-api-key'] || req.query.api_key;
+ const ip = req.ip || req.connection.remoteAddress;
+
+ console.log('\n\x1b[33m%s\x1b[0m', '🔑 Входящий API запрос');
+ console.log('🌐 IP:', ip);
+ console.log('🔑 Ключ:', apiKey ? apiKey.substring(0, 8) + '...' : 'не предоставлен');
+ console.log('🔄 Метод:', req.method);
+ console.log('📍 Путь:', req.originalUrl);
+
+ if (!apiKey) {
+ logApiAttempt(null, null, 'AUTH_FAILED', 'Ключ не предоставлен', req, 401, Date.now() - startTime);
+ return res.status(401).json({ error: 'API ключ обязателен' });
+ }
+
+ // Проверяем ключ в базе
+ db.get(`
+ SELECT ak.*, u.login as user_login, u.name as user_name, u.id as user_id
+ FROM api_keys ak
+ JOIN users u ON ak.user_id = u.id
+ WHERE ak.key = ? AND ak.is_active = 1
+ `, [apiKey], (err, keyData) => {
+ const responseTime = Date.now() - startTime;
+
+ if (err) {
+ console.error('❌ Ошибка проверки ключа:', err);
+ logApiAttempt(null, null, 'AUTH_ERROR', `Ошибка БД: ${err.message}`, req, 500, responseTime);
+ return res.status(500).json({ error: 'Ошибка проверки ключа' });
+ }
+
+ if (!keyData) {
+ console.log('❌ Недействительный ключ:', apiKey.substring(0, 8) + '...');
+
+ // Проверяем, есть ли такой ключ в базе (неактивный)
+ db.get('SELECT is_active FROM api_keys WHERE key = ?', [apiKey], (err, inactiveKey) => {
+ const reason = inactiveKey ? 'Ключ деактивирован' : 'Ключ не найден';
+ logApiAttempt(null, null, 'AUTH_FAILED', reason, req, 401, responseTime);
+ });
+
+ return res.status(401).json({ error: 'Недействительный API ключ' });
+ }
+
+ // Проверка IP, если настроено
+ if (keyData.allowed_ips) {
+ try {
+ const allowedIPs = JSON.parse(keyData.allowed_ips);
+ if (allowedIPs.length > 0 && !allowedIPs.includes(ip)) {
+ console.log('❌ IP не разрешен:', ip);
+ logApiAttempt(keyData.id, keyData.user_id, 'AUTH_FAILED', `IP ${ip} не разрешен`, req, 403, responseTime);
+ return res.status(403).json({ error: 'IP адрес не разрешен' });
+ }
+ } catch (e) {
+ console.error('Ошибка парсинга allowed_ips:', e);
+ }
+ }
+
+ // Обновляем информацию о последнем использовании
+ db.run(
+ 'UPDATE api_keys SET last_used_at = CURRENT_TIMESTAMP, last_used_ip = ? WHERE id = ?',
+ [ip, keyData.id]
+ );
+
+ console.log('✅ Ключ валидный. Пользователь:', keyData.user_name);
+ console.log('⏱️ Время проверки:', responseTime + 'ms');
+
+ req.apiKey = keyData;
+ req.apiUser = {
+ id: keyData.user_id,
+ login: keyData.user_login,
+ name: keyData.user_name
+ };
+
+ // Сохраняем startTime для логирования времени ответа
+ req.apiRequestStartTime = startTime;
+
+ next();
+ });
+ };
+
+ // Middleware для логирования ответов
+ const logResponse = (req, res, next) => {
+ const originalJson = res.json;
+ const originalSend = res.send;
+ const startTime = req.apiRequestStartTime || Date.now();
+
+ res.json = function(data) {
+ const responseTime = Date.now() - startTime;
+
+ if (req.apiKey) {
+ logApiAction(
+ req.apiKey.id,
+ req.apiUser.id,
+ `${req.method}_${req.path.replace(/[^a-zA-Z0-9]/g, '_')}`,
+ `Статус: ${res.statusCode}`,
+ req,
+ res.statusCode,
+ responseTime
+ );
+ }
+
+ console.log(`📤 Ответ отправлен за ${responseTime}ms`);
+ originalJson.call(this, data);
+ };
+
+ res.send = function(data) {
+ const responseTime = Date.now() - startTime;
+
+ if (req.apiKey) {
+ logApiAction(
+ req.apiKey.id,
+ req.apiUser.id,
+ `${req.method}_${req.path.replace(/[^a-zA-Z0-9]/g, '_')}`,
+ `Статус: ${res.statusCode}`,
+ req,
+ res.statusCode,
+ responseTime
+ );
+ }
+
+ console.log(`📤 Ответ отправлен за ${responseTime}ms`);
+ originalSend.call(this, data);
+ };
+
+ next();
+ };
+
+ // GET /api/api-keys/logs - Получить логи API (только админ)
+ router.get('/api/api-keys/logs', requireAuth, requireAdmin, (req, res) => {
+ const { limit = 100, offset = 0, api_key_id, user_id, action } = req.query;
+
+ let query = `
+ SELECT l.*, ak.name as api_key_name, u.name as user_name, u.login as user_login
+ FROM api_logs l
+ LEFT JOIN api_keys ak ON l.api_key_id = ak.id
+ LEFT JOIN users u ON l.user_id = u.id
+ WHERE 1=1
+ `;
+ const params = [];
+
+ if (api_key_id) {
+ query += ' AND l.api_key_id = ?';
+ params.push(api_key_id);
+ }
+
+ if (user_id) {
+ query += ' AND l.user_id = ?';
+ params.push(user_id);
+ }
+
+ if (action) {
+ query += ' AND l.action LIKE ?';
+ params.push(`%${action}%`);
+ }
+
+ query += ' ORDER BY l.created_at DESC LIMIT ? OFFSET ?';
+ params.push(parseInt(limit), parseInt(offset));
+
+ db.all(query, params, (err, logs) => {
+ if (err) {
+ console.error('❌ Ошибка получения логов:', err);
+ return res.status(500).json({ error: 'Ошибка получения логов' });
+ }
+
+ // Получаем общее количество
+ db.get('SELECT COUNT(*) as total FROM api_logs', [], (err, count) => {
+ if (err) {
+ return res.json({ logs: logs || [], total: 0 });
+ }
+
+ res.json({
+ success: true,
+ logs: logs || [],
+ total: count.total,
+ limit: parseInt(limit),
+ offset: parseInt(offset)
+ });
+ });
+ });
+ });
+
+ // GET /api/api-keys/:id/logs - Получить логи конкретного ключа
+ router.get('/api/api-keys/:id/logs', requireAuth, requireAdmin, (req, res) => {
+ const { id } = req.params;
+ const { limit = 50 } = req.query;
+
+ db.all(`
+ SELECT l.*, u.name as user_name
+ FROM api_logs l
+ LEFT JOIN users u ON l.user_id = u.id
+ WHERE l.api_key_id = ?
+ ORDER BY l.created_at DESC
+ LIMIT ?
+ `, [id, parseInt(limit)], (err, logs) => {
+ if (err) {
+ console.error('❌ Ошибка получения логов ключа:', err);
+ return res.status(500).json({ error: 'Ошибка получения логов' });
+ }
+
+ res.json({
+ success: true,
+ logs: logs || []
+ });
+ });
+ });
+
+ // GET /api/api-keys/stats - Статистика использования API
+ router.get('/api/api-keys/stats', requireAuth, requireAdmin, (req, res) => {
+ const stats = {};
+
+ // Общая статистика
+ db.get(`
+ SELECT
+ COUNT(DISTINCT api_key_id) as active_keys,
+ COUNT(*) as total_requests,
+ COUNT(DISTINCT user_id) as unique_users,
+ AVG(response_time) as avg_response_time,
+ COUNT(CASE WHEN response_status >= 400 THEN 1 END) as error_count,
+ COUNT(CASE WHEN DATE(created_at) = DATE('now') THEN 1 END) as today_requests
+ FROM api_logs
+ WHERE created_at >= DATE('now', '-30 days')
+ `, [], (err, total) => {
+ if (err) {
+ console.error('❌ Ошибка получения статистики:', err);
+ return res.status(500).json({ error: 'Ошибка получения статистики' });
+ }
+
+ stats.total = total || {};
+
+ // Статистика по методам
+ db.all(`
+ SELECT method, COUNT(*) as count
+ FROM api_logs
+ WHERE created_at >= DATE('now', '-30 days')
+ GROUP BY method
+ ORDER BY count DESC
+ `, [], (err, methods) => {
+ if (err) {
+ return res.json(stats);
+ }
+
+ stats.by_method = methods || [];
+
+ // Статистика по дням
+ db.all(`
+ SELECT DATE(created_at) as date, COUNT(*) as count
+ FROM api_logs
+ WHERE created_at >= DATE('now', '-7 days')
+ GROUP BY DATE(created_at)
+ ORDER BY date DESC
+ `, [], (err, daily) => {
+ if (err) {
+ return res.json(stats);
+ }
+
+ stats.daily = daily || [];
+
+ // Топ ключей
+ db.all(`
+ SELECT
+ l.api_key_id,
+ ak.name as api_key_name,
+ COUNT(*) as request_count,
+ COUNT(DISTINCT l.user_id) as users_count,
+ AVG(l.response_time) as avg_time
+ FROM api_logs l
+ LEFT JOIN api_keys ak ON l.api_key_id = ak.id
+ WHERE l.created_at >= DATE('now', '-30 days')
+ GROUP BY l.api_key_id
+ ORDER BY request_count DESC
+ LIMIT 10
+ `, [], (err, topKeys) => {
+ if (err) {
+ return res.json(stats);
+ }
+
+ stats.top_keys = topKeys || [];
+
+ res.json({
+ success: true,
+ stats,
+ timestamp: new Date().toISOString()
+ });
+ });
+ });
+ });
+ });
+ });
+
+ // GET /api/api-keys - Получить все ключи (только админ)
+ router.get('/api/api-keys', requireAuth, requireAdmin, (req, res) => {
+ db.all(`
+ SELECT ak.*, u.name as user_name, u.login as user_login,
+ creator.name as creator_name,
+ (SELECT COUNT(*) FROM api_logs WHERE api_key_id = ak.id) as request_count,
+ (SELECT created_at FROM api_logs WHERE api_key_id = ak.id ORDER BY created_at DESC LIMIT 1) as last_request
+ FROM api_keys ak
+ LEFT JOIN users u ON ak.user_id = u.id
+ LEFT JOIN users creator ON ak.created_by = creator.id
+ ORDER BY ak.created_at DESC
+ `, [], (err, keys) => {
+ if (err) {
+ console.error('❌ Ошибка получения API ключей:', err);
+ return res.status(500).json({ error: 'Ошибка получения ключей' });
+ }
+
+ // Маскируем ключи для безопасности
+ const maskedKeys = (keys || []).map(key => ({
+ ...key,
+ key: key.key.substring(0, 8) + '...' + key.key.substring(key.key.length - 8),
+ request_count: key.request_count || 0
+ }));
+
+ res.json(maskedKeys);
+ });
+ });
+
+ // POST /api/api-keys - Создать новый API ключ (только админ)
+ router.post('/api/api-keys', requireAuth, requireAdmin, (req, res) => {
+ const { name, user_id, description, allowed_ips } = req.body;
+ const startTime = Date.now();
+
+ if (!name || !user_id) {
+ return res.status(400).json({ error: 'Название и пользователь обязательны' });
+ }
+
+ // Проверяем существование пользователя
+ db.get('SELECT id, name FROM users WHERE id = ?', [user_id], (err, user) => {
+ if (err || !user) {
+ return res.status(404).json({ error: 'Пользователь не найден' });
+ }
+
+ const apiKey = generateApiKey();
+ const allowedIPsJson = allowed_ips ? JSON.stringify(allowed_ips.split(',').map(ip => ip.trim())) : null;
+
+ db.run(`
+ INSERT INTO api_keys (name, key, user_id, description, allowed_ips, created_by)
+ VALUES (?, ?, ?, ?, ?, ?)
+ `, [name, apiKey, user_id, description || '', allowedIPsJson, req.session.user.id],
+ function(err) {
+ const responseTime = Date.now() - startTime;
+
+ if (err) {
+ console.error('❌ Ошибка создания API ключа:', err);
+ return res.status(500).json({ error: 'Ошибка создания ключа' });
+ }
+
+ // Логируем создание ключа
+ logApiAction(
+ this.lastID,
+ req.session.user.id,
+ 'API_KEY_CREATED',
+ `Создан ключ "${name}" для пользователя ${user.name} (ID: ${user_id})`,
+ req,
+ 201,
+ responseTime
+ );
+
+ res.status(201).json({
+ success: true,
+ id: this.lastID,
+ key: apiKey,
+ message: 'API ключ успешно создан'
+ });
+ });
+ });
+ });
+
+ // PUT /api/api-keys/:id/toggle - Активировать/деактивировать ключ
+ router.put('/api/api-keys/:id/toggle', requireAuth, requireAdmin, (req, res) => {
+ const { id } = req.params;
+ const { is_active } = req.body;
+ const startTime = Date.now();
+
+ db.get('SELECT name FROM api_keys WHERE id = ?', [id], (err, key) => {
+ if (err || !key) {
+ return res.status(404).json({ error: 'Ключ не найден' });
+ }
+
+ db.run(
+ 'UPDATE api_keys SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
+ [is_active ? 1 : 0, id],
+ function(err) {
+ const responseTime = Date.now() - startTime;
+
+ if (err) {
+ console.error('❌ Ошибка обновления ключа:', err);
+ return res.status(500).json({ error: 'Ошибка обновления ключа' });
+ }
+
+ logApiAction(
+ id,
+ req.session.user.id,
+ is_active ? 'API_KEY_ACTIVATED' : 'API_KEY_DEACTIVATED',
+ `Ключ "${key.name}" ${is_active ? 'активирован' : 'деактивирован'}`,
+ req,
+ 200,
+ responseTime
+ );
+
+ res.json({
+ success: true,
+ message: `Ключ ${is_active ? 'активирован' : 'деактивирован'}`
+ });
+ }
+ );
+ });
+ });
+
+ // DELETE /api/api-keys/:id - Удалить ключ (только админ)
+ router.delete('/api/api-keys/:id', requireAuth, requireAdmin, (req, res) => {
+ const { id } = req.params;
+ const startTime = Date.now();
+
+ db.get('SELECT name FROM api_keys WHERE id = ?', [id], (err, key) => {
+ if (err || !key) {
+ return res.status(404).json({ error: 'Ключ не найден' });
+ }
+
+ db.run('DELETE FROM api_keys WHERE id = ?', [id], function(err) {
+ const responseTime = Date.now() - startTime;
+
+ if (err) {
+ console.error('❌ Ошибка удаления ключа:', err);
+ return res.status(500).json({ error: 'Ошибка удаления ключа' });
+ }
+
+ logApiAction(
+ id,
+ req.session.user.id,
+ 'API_KEY_DELETED',
+ `Удален ключ "${key.name}"`,
+ req,
+ 200,
+ responseTime
+ );
+
+ res.json({ success: true, message: 'Ключ удален' });
+ });
+ });
+ });
+
+ // ==================== Публичное API для внешних сервисов ====================
+
+ // GET /api/external/tasks - Получить задачи для внешнего сервиса
+ router.get('/api/external/tasks', requireApiKey, logResponse, (req, res) => {
+ const userId = req.apiUser.id;
+ const { status, limit = 50, offset = 0 } = req.query;
+ const startTime = Date.now();
+
+ let query = `
+ SELECT DISTINCT
+ t.id,
+ t.title,
+ t.description,
+ t.created_at,
+ t.due_date,
+ t.task_type,
+ u.name as creator_name,
+ (
+ SELECT json_group_array(json_object(
+ 'id', tf.id,
+ 'filename', tf.original_name,
+ 'file_size', tf.file_size,
+ 'uploaded_at', tf.uploaded_at
+ ))
+ FROM task_files tf
+ WHERE tf.task_id = t.id
+ ) as files,
+ ta.status as assignment_status,
+ ta.start_date,
+ ta.due_date as assignment_due_date
+ FROM tasks t
+ LEFT JOIN users u ON t.created_by = u.id
+ LEFT JOIN task_assignments ta ON t.id = ta.task_id AND ta.user_id = ?
+ WHERE ta.user_id = ?
+ AND t.status = 'active'
+ AND t.closed_at IS NULL
+ `;
+
+ const params = [userId, userId];
+
+ if (status) {
+ if (status === 'active') {
+ query += ` AND ta.status IN ('assigned', 'in_progress')`;
+ } else {
+ query += ` AND ta.status = ?`;
+ params.push(status);
+ }
+ }
+
+ query += ` ORDER BY t.due_date ASC, t.created_at DESC LIMIT ? OFFSET ?`;
+ params.push(limit, offset);
+
+ db.all(query, params, (err, tasks) => {
+ const responseTime = Date.now() - startTime;
+
+ if (err) {
+ console.error('❌ Ошибка получения задач:', err);
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'GET_TASKS_ERROR',
+ `Ошибка: ${err.message}`,
+ req,
+ 500,
+ responseTime
+ );
+ return res.status(500).json({ error: 'Ошибка получения задач' });
+ }
+
+ // Парсим JSON с файлами
+ const result = (tasks || []).map(task => {
+ try {
+ task.files = JSON.parse(task.files || '[]');
+ } catch (e) {
+ task.files = [];
+ }
+ return task;
+ });
+
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'GET_TASKS',
+ `Получено ${result.length} задач${status ? ` (статус: ${status})` : ''}`,
+ req,
+ 200,
+ responseTime
+ );
+
+ res.json({
+ success: true,
+ tasks: result,
+ meta: {
+ total: result.length,
+ limit,
+ offset,
+ user: req.apiUser.name
+ }
+ });
+ });
+ });
+
+ // GET /api/external/tasks/:taskId - Получить конкретную задачу
+ router.get('/api/external/tasks/:taskId', requireApiKey, logResponse, (req, res) => {
+ const { taskId } = req.params;
+ const userId = req.apiUser.id;
+ const startTime = Date.now();
+
+ // Проверяем, что задача назначена этому пользователю
+ db.get(`
+ SELECT t.*, u.name as creator_name,
+ (
+ SELECT json_group_array(json_object(
+ 'id', tf.id,
+ 'filename', tf.original_name,
+ 'file_path', tf.file_path,
+ 'file_size', tf.file_size,
+ 'uploaded_at', tf.uploaded_at,
+ 'uploader_name', up.name
+ ))
+ FROM task_files tf
+ LEFT JOIN users up ON tf.user_id = up.id
+ WHERE tf.task_id = t.id
+ ) as files,
+ ta.status as assignment_status,
+ ta.start_date,
+ ta.due_date as assignment_due_date
+ FROM tasks t
+ LEFT JOIN users u ON t.created_by = u.id
+ LEFT JOIN task_assignments ta ON t.id = ta.task_id AND ta.user_id = ?
+ WHERE t.id = ? AND ta.user_id IS NOT NULL
+ `, [userId, taskId], (err, task) => {
+ const responseTime = Date.now() - startTime;
+
+ if (err) {
+ console.error('❌ Ошибка получения задачи:', err);
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'GET_TASK_ERROR',
+ `Ошибка получения задачи ${taskId}: ${err.message}`,
+ req,
+ 500,
+ responseTime
+ );
+ return res.status(500).json({ error: 'Ошибка получения задачи' });
+ }
+
+ if (!task) {
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'GET_TASK_NOT_FOUND',
+ `Задача ${taskId} не найдена или не назначена`,
+ req,
+ 404,
+ responseTime
+ );
+ return res.status(404).json({ error: 'Задача не найдена или не назначена вам' });
+ }
+
+ try {
+ task.files = JSON.parse(task.files || '[]');
+ } catch (e) {
+ task.files = [];
+ }
+
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'GET_TASK',
+ `Получена задача ${taskId}: "${task.title}"`,
+ req,
+ 200,
+ responseTime
+ );
+
+ res.json({ success: true, task });
+ });
+ });
+
+ // PUT /api/external/tasks/:taskId/status - Изменить статус задачи
+ router.put('/api/external/tasks/:taskId/status', requireApiKey, logResponse, (req, res) => {
+ const { taskId } = req.params;
+ const { status, comment } = req.body;
+ const userId = req.apiUser.id;
+ const startTime = Date.now();
+
+ if (!status || !['in_progress', 'completed'].includes(status)) {
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'UPDATE_STATUS_ERROR',
+ `Попытка установить недопустимый статус "${status}" для задачи ${taskId}`,
+ req,
+ 400,
+ Date.now() - startTime
+ );
+ return res.status(400).json({
+ error: 'Статус должен быть "in_progress" или "completed"'
+ });
+ }
+
+ // Проверяем, что задача назначена этому пользователю
+ db.get(`
+ SELECT ta.id, ta.status as current_status, t.title, t.created_by
+ FROM task_assignments ta
+ JOIN tasks t ON ta.task_id = t.id
+ WHERE ta.task_id = ? AND ta.user_id = ?
+ `, [taskId, userId], (err, assignment) => {
+ if (err || !assignment) {
+ const responseTime = Date.now() - startTime;
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'UPDATE_STATUS_ERROR',
+ `Задача ${taskId} не назначена пользователю`,
+ req,
+ 404,
+ responseTime
+ );
+ return res.status(404).json({ error: 'Задача не назначена вам' });
+ }
+
+ // Обновляем статус
+ db.run(
+ `UPDATE task_assignments
+ SET status = ?, updated_at = CURRENT_TIMESTAMP
+ WHERE task_id = ? AND user_id = ?`,
+ [status, taskId, userId],
+ function(err) {
+ const responseTime = Date.now() - startTime;
+
+ if (err) {
+ console.error('❌ Ошибка обновления статуса:', err);
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'UPDATE_STATUS_ERROR',
+ `Ошибка обновления статуса задачи ${taskId}: ${err.message}`,
+ req,
+ 500,
+ responseTime
+ );
+ return res.status(500).json({ error: 'Ошибка обновления статуса' });
+ }
+
+ const details = `Статус задачи ${taskId} "${assignment.title}" изменен с ${assignment.current_status} на ${status}. Комментарий: ${comment || 'нет'}`;
+
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ `TASK_${status.toUpperCase()}`,
+ details,
+ req,
+ 200,
+ responseTime
+ );
+
+ // Логируем в общую активность
+ const { logActivity } = require('./database');
+ if (logActivity) {
+ logActivity(
+ parseInt(taskId),
+ userId,
+ `API_TASK_${status.toUpperCase()}`,
+ `[API Key ${req.apiKey.id}] ${comment || 'Без комментария'}`
+ );
+ }
+
+ res.json({
+ success: true,
+ message: `Статус изменен на "${status}"`,
+ old_status: assignment.current_status,
+ new_status: status,
+ task_title: assignment.title
+ });
+ }
+ );
+ });
+ });
+
+ // POST /api/external/tasks/:taskId/files - Загрузить файл к задаче
+ router.post('/api/external/tasks/:taskId/files', requireApiKey, logResponse, upload.single('file'), (req, res) => {
+ const { taskId } = req.params;
+ const userId = req.apiUser.id;
+ const startTime = Date.now();
+
+ if (!req.file) {
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'UPLOAD_FILE_ERROR',
+ `Попытка загрузить файл без файла к задаче ${taskId}`,
+ req,
+ 400,
+ Date.now() - startTime
+ );
+ return res.status(400).json({ error: 'Файл обязателен' });
+ }
+
+ // Проверяем, что задача назначена этому пользователю
+ db.get(`
+ SELECT ta.id, t.title
+ FROM task_assignments ta
+ JOIN tasks t ON ta.task_id = t.id
+ WHERE ta.task_id = ? AND ta.user_id = ?
+ `, [taskId, userId], (err, assignment) => {
+ if (err || !assignment) {
+ // Удаляем загруженный файл, если он не нужен
+ if (req.file && req.file.path && fs.existsSync(req.file.path)) {
+ fs.unlinkSync(req.file.path);
+ }
+
+ const responseTime = Date.now() - startTime;
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'UPLOAD_FILE_ERROR',
+ `Задача ${taskId} не назначена пользователю`,
+ req,
+ 404,
+ responseTime
+ );
+ return res.status(404).json({ error: 'Задача не назначена вам' });
+ }
+
+ const { createUserTaskFolder } = require('./database');
+ const userFolder = createUserTaskFolder(taskId, req.apiUser.login);
+
+ const newPath = path.join(userFolder, path.basename(req.file.filename));
+ fs.renameSync(req.file.path, newPath);
+
+ db.run(
+ `INSERT INTO task_files
+ (task_id, user_id, filename, original_name, file_path, file_size)
+ VALUES (?, ?, ?, ?, ?, ?)`,
+ [taskId, userId, path.basename(req.file.filename), req.file.originalname, newPath, req.file.size],
+ function(err) {
+ const responseTime = Date.now() - startTime;
+
+ if (err) {
+ console.error('❌ Ошибка сохранения файла:', err);
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'UPLOAD_FILE_ERROR',
+ `Ошибка сохранения файла для задачи ${taskId}: ${err.message}`,
+ req,
+ 500,
+ responseTime
+ );
+ return res.status(500).json({ error: 'Ошибка сохранения файла' });
+ }
+
+ const details = `Загружен файл "${req.file.originalname}" (${(req.file.size / 1024).toFixed(2)} KB) к задаче ${taskId} "${assignment.title}"`;
+
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'FILE_UPLOADED',
+ details,
+ req,
+ 200,
+ responseTime
+ );
+
+ // Логируем в общую активность
+ const { logActivity } = require('./database');
+ if (logActivity) {
+ logActivity(
+ parseInt(taskId),
+ userId,
+ 'API_FILE_UPLOADED',
+ `[API Key ${req.apiKey.id}] Загружен файл: ${req.file.originalname}`
+ );
+ }
+
+ res.json({
+ success: true,
+ file_id: this.lastID,
+ filename: req.file.originalname,
+ size: req.file.size,
+ message: 'Файл успешно загружен'
+ });
+ }
+ );
+ });
+ });
+
+ // GET /api/external/tasks/:taskId/files/:fileId/download - Скачать файл
+ router.get('/api/external/tasks/:taskId/files/:fileId/download', requireApiKey, logResponse, (req, res) => {
+ const { taskId, fileId } = req.params;
+ const userId = req.apiUser.id;
+ const startTime = Date.now();
+
+ db.get(`
+ SELECT tf.*, t.title as task_title
+ FROM task_files tf
+ JOIN task_assignments ta ON tf.task_id = ta.task_id
+ JOIN tasks t ON tf.task_id = t.id
+ WHERE tf.id = ? AND tf.task_id = ? AND ta.user_id = ?
+ `, [fileId, taskId, userId], (err, file) => {
+ const responseTime = Date.now() - startTime;
+
+ if (err || !file) {
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'DOWNLOAD_FILE_ERROR',
+ `Файл ${fileId} не найден для задачи ${taskId}`,
+ req,
+ 404,
+ responseTime
+ );
+ return res.status(404).json({ error: 'Файл не найден' });
+ }
+
+ if (!fs.existsSync(file.file_path)) {
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'DOWNLOAD_FILE_ERROR',
+ `Файл ${fileId} не найден на сервере для задачи ${taskId}`,
+ req,
+ 404,
+ responseTime
+ );
+ return res.status(404).json({ error: 'Файл не найден на сервере' });
+ }
+
+ logApiAction(
+ req.apiKey.id,
+ userId,
+ 'FILE_DOWNLOADED',
+ `Скачан файл "${file.original_name}" из задачи ${taskId} "${file.task_title}"`,
+ req,
+ 200,
+ responseTime
+ );
+
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(file.original_name)}`);
+ res.setHeader('Content-Type', 'application/octet-stream');
+ res.sendFile(file.file_path);
+ });
+ });
+
+ // ==================== Инициализация ====================
+
+ const init = async () => {
+ try {
+ await createApiKeysTables();
+ console.log('✅ Модуль API ключей с логированием инициализирован');
+ } catch (error) {
+ console.error('❌ Ошибка инициализации API ключей:', error.message);
+ }
+ };
+
+ init();
+
+ app.use(router);
+
+ return {
+ requireApiKey,
+ generateApiKey
+ };
+};
\ No newline at end of file
diff --git a/public/admin-api-management.html b/public/admin-api-management.html
new file mode 100644
index 0000000..3976d17
--- /dev/null
+++ b/public/admin-api-management.html
@@ -0,0 +1,1787 @@
+
+
+
+
+
+ Управление API ключами и пользователями
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Загрузка API ключей...
+
+
+
+
+
+
+
+
+
+
+
+
Загрузка пользователей...
+
+
+
+
+
+
+
📚 Документация API
+
+
+
Аутентификация
+
+
Все запросы требуют заголовок X-API-Key с вашим ключом.
+
+
+
+
+
Эндпоинты
+
+
+
+ GET /api/external/tasks
+
+
+
Получение списка задач, назначенных пользователю.
+
Параметры:
+
+ status - фильтр по статусу (assigned, in_progress, completed)
+ limit - количество записей (по умолчанию 50)
+ offset - смещение для пагинации
+
+
Пример ответа:
+
+{
+ "success": true,
+ "tasks": [
+ {
+ "id": 123,
+ "title": "Название задачи",
+ "description": "Описание задачи",
+ "created_at": "2024-01-01T10:00:00Z",
+ "due_date": "2024-01-02T19:01:00Z",
+ "task_type": "document",
+ "creator_name": "Иванов И.И.",
+ "files": []
+ }
+ ]
+}
+
+
+
+
+
+ PUT /api/external/tasks/{taskId}/status
+
+
+
Изменение статуса задачи.
+
Тело запроса:
+
+{
+ "status": "in_progress", // или "completed"
+ "comment": "Комментарий к изменению"
+}
+
+
+
+
+
+ POST /api/external/tasks/{taskId}/files
+
+
+
Загрузка файла к задаче.
+
Формат:
+
multipart/form-data с полем file
+
+
+
+
+
+ GET /api/external/tasks/{taskId}/files/{fileId}/download
+
+
+
+
+
+
+
Примеры использования
+
+
+
cURL:
+
+# Получение задач
+curl -H "X-API-Key: ваш_ключ" https://ваш-сервер/api/external/tasks
+
+# Изменение статуса
+curl -X PUT -H "X-API-Key: ваш_ключ" \
+ -H "Content-Type: application/json" \
+ -d '{"status":"in_progress","comment":"Начинаю работу"}' \
+ https://ваш-сервер/api/external/tasks/123/status
+
+# Загрузка файла
+curl -X POST -H "X-API-Key: ваш_ключ" \
+ -F "file=@document.pdf" \
+ https://ваш-сервер/api/external/tasks/123/files
+
+
JavaScript:
+
+const API_KEY = 'ваш_ключ';
+
+async function getTasks() {
+ const response = await fetch('/api/external/tasks', {
+ headers: { 'X-API-Key': API_KEY }
+ });
+ return response.json();
+}
+
+async function updateStatus(taskId, status) {
+ const response = await fetch(`/api/external/tasks/${taskId}/status`, {
+ method: 'PUT',
+ headers: {
+ 'X-API-Key': API_KEY,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ status, comment: 'Комментарий' })
+ });
+ return response.json();
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⚠️ Сохраните этот ключ в безопасном месте! Он больше не будет показан.
+
+
+
+
Ваш API ключ:
+
+
+
+
+
Быстрый старт:
+
+# Проверка подключения
+curl -H "X-API-Key: <ваш_ключ>" https://ваш-сервер/api/external/tasks
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/upravlenie.html b/public/upravlenie.html
deleted file mode 100644
index 0116d7e..0000000
--- a/public/upravlenie.html
+++ /dev/null
@@ -1,1058 +0,0 @@
-
-
-
-
-
- Управление межсервисным взаимодействием
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | ID |
- Service ID |
- Название |
- Тип |
- Логин API |
- Локальный пользователь |
- Статус |
- Последняя синхр. |
- Связь |
- Действия |
-
-
-
-
- | Загрузка... |
-
-
-
-
-
-
-
-
-
0
-
Всего подключений
-
-
-
-
-
-
-
Последние синхронизации
-
-
-
- | Сервис |
- Время |
- Статус |
-
-
-
-
- | Загрузка... |
-
-
-
-
-
Ошибки
-
-
-
- | Сервис |
- Ошибка |
- Время |
-
-
-
-
- | Нет ошибок |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/server.js b/server.js
index 01995c8..4550bae 100644
--- a/server.js
+++ b/server.js
@@ -22,7 +22,8 @@ const api2Groups = require('./api2-groups');
//
const chatAPI = require('./api-chat');
// Подключаем API для управления межсервисным взаимодействием
-const { setupUpravlenieEndpoints } = require('./upravlenie-service');
+//const { setupUpravlenieEndpoints } = require('./upravlenie-service');
+const apiKeysModule = require('./api-keys');
//
const app = express();
const PORT = process.env.PORT || 3000;
@@ -165,6 +166,28 @@ const requireAuth = (req, res, next) => {
next();
};
+// Middleware для проверки прав администратора
+const requireAdmin = (req, res, next) => {
+ if (!req.session || !req.session.user) {
+ return res.status(401).json({ error: 'Требуется аутентификация' });
+ }
+ if (req.session.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Недостаточно прав. Требуется роль администратора' });
+ }
+ next();
+};
+
+// Middleware для проверки прав на управление задачами (admin или tasks)
+const requireTasksAccess = (req, res, next) => {
+ if (!req.session || !req.session.user) {
+ return res.status(401).json({ error: 'Требуется аутентификация' });
+ }
+ if (req.session.user.role !== 'admin' && req.session.user.role !== 'tasks') {
+ return res.status(403).json({ error: 'Недостаточно прав. Требуется роль admin или tasks' });
+ }
+ next();
+};
+
// API для аутентификации
app.post('/api/login', async (req, res) => {
const { login, password } = req.body;
@@ -303,6 +326,7 @@ app.get('/api/user', (req, res) => {
res.status(401).json({ error: 'Не аутентифицирован' });
}
});
+
// Получаем актуальные группы пользователя из новой структуры
app.get('/api/user_v2', (req, res) => {
if (req.session.user) {
@@ -371,6 +395,7 @@ app.get('/api/users/group/help', requireAuth, (req, res) => {
res.json(rows);
});
});
+
// API для получения пользователей группы "doc"
app.get('/api/users/group/doc', requireAuth, (req, res) => {
db.all(`
@@ -389,6 +414,7 @@ app.get('/api/users/group/doc', requireAuth, (req, res) => {
res.json(rows);
});
});
+
// Middleware для проверки наличия БД в API endpoints
app.use((req, res, next) => {
if (!db && req.path.startsWith('/api/') && req.path !== '/api/health' && req.path !== '/api/login') {
@@ -739,11 +765,9 @@ app.get('/doc', (req, res) => {
if (!hasAccess) {
return res.status(403).send('в разработке');
}
- //if (!req.session.user || req.session.user.role !== 'admin') {
- // return res.status(403).send('в разработке');
- //}
res.sendFile(path.join(__dirname, 'public/doc.html'));
});
+
// Страница поддержка
app.get('/help', (req, res) => {
if (!req.session.user) {
@@ -983,6 +1007,7 @@ app.get('/api/groups', requireAuth, (req, res) => {
res.json(groups);
});
});
+
// API для всех пользователей с группами
app.get('/api/users/all', requireAuth, (req, res) => {
if (req.session.user.role !== 'admin') {
@@ -1032,6 +1057,75 @@ app.get('/api/users/all', requireAuth, (req, res) => {
});
});
+// API для обновления пользователя (только для админов)
+app.put('/api/users/:userId', requireAuth, requireAdmin, (req, res) => {
+ const { userId } = req.params;
+ const { name, email, role, auth_type, is_active } = req.body;
+
+ // Проверяем существование пользователя
+ db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => {
+ if (err) {
+ console.error('❌ Ошибка проверки пользователя:', err);
+ return res.status(500).json({ error: err.message });
+ }
+
+ if (!user) {
+ return res.status(404).json({ error: 'Пользователь не найден' });
+ }
+
+ // Формируем запрос на обновление
+ const updates = [];
+ const params = [];
+
+ if (name !== undefined) {
+ updates.push('name = ?');
+ params.push(name);
+ }
+
+ if (email !== undefined) {
+ updates.push('email = ?');
+ params.push(email);
+ }
+
+ if (role !== undefined) {
+ updates.push('role = ?');
+ params.push(role);
+ }
+
+ if (auth_type !== undefined) {
+ updates.push('auth_type = ?');
+ params.push(auth_type);
+ }
+
+ if (is_active !== undefined) {
+ updates.push('is_active = ?');
+ params.push(is_active ? 1 : 0);
+ }
+
+ if (updates.length === 0) {
+ return res.status(400).json({ error: 'Нет данных для обновления' });
+ }
+
+ updates.push('updated_at = CURRENT_TIMESTAMP');
+ params.push(userId);
+
+ const query = `UPDATE users SET ${updates.join(', ')} WHERE id = ?`;
+
+ db.run(query, params, function(err) {
+ if (err) {
+ console.error('❌ Ошибка обновления пользователя:', err);
+ return res.status(500).json({ error: err.message });
+ }
+
+ res.json({
+ success: true,
+ message: 'Пользователь успешно обновлен',
+ changes: this.changes
+ });
+ });
+ });
+});
+
// API для добавления пользователя в группу
app.post('/api/groups/:groupId/users/:userId', requireAuth, (req, res) => {
if (req.session.user.role !== 'admin') {
@@ -1344,6 +1438,7 @@ app.delete('/api/tasks/:taskId/files/batch-delete', requireAuth, (req, res) => {
});
});
});
+
// API для получения информации о файле
app.get('/api/files/:fileId', requireAuth, (req, res) => {
const { fileId } = req.params;
@@ -1370,6 +1465,7 @@ app.get('/api/files/:fileId', requireAuth, (req, res) => {
});
});
});
+
// Инициализация сервера
async function initializeServer() {
console.log('🚀 Инициализация сервера...');
@@ -1398,7 +1494,7 @@ async function initializeServer() {
apiDoc(app, db, upload);
console.log('✅ Endpoint\'ы документов настроены');
- // 5. Загружаем админ роутер динамически
+ // 6. Загружаем админ роутер динамически
try {
adminRouter = require('./admin-server');
console.log('Admin router loaded:', adminRouter);
@@ -1435,14 +1531,19 @@ async function initializeServer() {
console.log('⚠️ Создана заглушка для админ роутера из-за ошибки');
}
- // Подключаем API для внешних идентификаторов
+ // 7. Подключаем API для внешних идентификаторов
api2Groups(app, db);
console.log('✅ API для внешних идентификаторов подключено');
- // Подключаем API для управления межсервисным взаимодействием
- const upravlenieService = setupUpravlenieEndpoints(app, db);
- console.log('✅ API для управления межсервисным взаимодействием подключено');
- // 6. Помечаем сервер как готовый
+ // 8. Подключаем API для управления межсервисным взаимодействием
+ //const upravlenieService = setupUpravlenieEndpoints(app, db);
+ //console.log('✅ API для управления межсервисным взаимодействием подключено');
+
+ // 9. Подключаем модуль API ключей
+ apiKeysModule(app, db, upload);
+ console.log('✅ Модуль API ключей подключен');
+
+ // 10. Помечаем сервер как готовый
serverReady = true;
console.log('✅ Сервер полностью инициализирован');
@@ -1468,9 +1569,11 @@ initializeServer().then(() => {
console.log('🔐 LDAP авторизация доступна для пользователей школы');
console.log(`👥 Разрешенные группы: ${process.env.ALLOWED_GROUPS}`);
console.log('📢 Система уведомлений активна');
+
// Подключаем API для чата
chatAPI(app, db, upload);
console.log('✅ API для чата задач подключено');
+
// Запускаем фоновые задачи
setInterval(checkOverdueTasks, 60000);
setInterval(checkUpcomingDeadlines, 60000);
diff --git a/upravlenie-service.js b/upravlenie-service.js
deleted file mode 100644
index a6590aa..0000000
--- a/upravlenie-service.js
+++ /dev/null
@@ -1,1362 +0,0 @@
-// upravlenie-service.js
-const axios = require('axios');
-const FormData = require('form-data');
-const fs = require('fs');
-const path = require('path');
-const crypto = require('crypto');
-
-const SERVICE_ID_RANGE = { min: 1, max: 4062 };
-const SYNC_INTERVAL = 60000;
-const MAX_RETRY_COUNT = 3;
-const RETRY_DELAY = 5000;
-
-const TaskStatus = {
- PENDING: 'pending',
- IN_PROGRESS: 'in_progress',
- ASSIGNED: 'assigned',
- REWORK: 'rework',
- COMPLETED: 'completed',
- OVERDUE: 'overdue',
- CANCELLED: 'cancelled'
-};
-
-const SyncDirection = {
- INCOMING: 'incoming',
- OUTGOING: 'outgoing'
-};
-
-class UpravlenieService {
- constructor(db) {
- this.db = db;
- this.syncIntervals = new Map();
- this.syncInProgress = new Set();
- this.init();
- }
-
- async init() {
- console.log('🔧 Инициализация сервиса Upravlenie...');
- await this.createTable();
- await this.startAllSyncJobs();
- console.log('✅ Сервис Upravlenie инициализирован');
- }
-
- async createTable() {
- const createTableSQL = `
- CREATE TABLE IF NOT EXISTS upravlenie (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- service_id INTEGER NOT NULL CHECK(service_id >= 1 AND service_id <= 4062),
- service_name VARCHAR(255) NOT NULL,
- service_type VARCHAR(50) NOT NULL CHECK(service_type IN ('organizer', 'executor')),
- login VARCHAR(255) NOT NULL,
- password VARCHAR(255) NOT NULL,
- api_url VARCHAR(500),
- local_user_id INTEGER,
- local_user_login VARCHAR(255),
- sync_direction VARCHAR(50) DEFAULT 'outgoing' CHECK(sync_direction IN ('incoming', 'outgoing', 'both')),
- sync_enabled BOOLEAN DEFAULT 1,
- sync_interval INTEGER DEFAULT 60,
- last_sync_at TIMESTAMP,
- last_sync_status VARCHAR(50),
- last_sync_error TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- is_active BOOLEAN DEFAULT 1,
- FOREIGN KEY (local_user_id) REFERENCES users(id)
- )
- `;
-
- return new Promise((resolve, reject) => {
- this.db.run(createTableSQL, (err) => {
- if (err) {
- console.error('❌ Ошибка создания таблицы upravlenie:', err);
- reject(err);
- } else {
- console.log('✅ Таблица upravlenie создана/проверена');
- this.createIndexes().then(resolve).catch(reject);
- }
- });
- });
- }
-
- async createIndexes() {
- const indexes = [
- 'CREATE INDEX IF NOT EXISTS idx_upravlenie_service_id ON upravlenie(service_id)',
- 'CREATE INDEX IF NOT EXISTS idx_upravlenie_service_type ON upravlenie(service_type)',
- 'CREATE INDEX IF NOT EXISTS idx_upravlenie_local_user_id ON upravlenie(local_user_id)',
- 'CREATE INDEX IF NOT EXISTS idx_upravlenie_sync_enabled ON upravlenie(sync_enabled)',
- 'CREATE INDEX IF NOT EXISTS idx_upravlenie_last_sync ON upravlenie(last_sync_at)'
- ];
-
- return new Promise((resolve, reject) => {
- this.db.serialize(() => {
- let error = null;
- indexes.forEach(indexSQL => {
- this.db.run(indexSQL, (err) => {
- if (err) error = err;
- });
- });
-
- if (error) {
- console.error('❌ Ошибка создания индексов:', error);
- reject(error);
- } else {
- console.log('✅ Индексы для upravlenie созданы');
- resolve();
- }
- });
- });
- }
-
- async createConnection(data) {
- const {
- service_id, service_name, service_type, login, password,
- api_url, local_user_id, local_user_login, sync_direction = 'outgoing',
- sync_enabled = 1, sync_interval = 60
- } = data;
-
- if (service_id < SERVICE_ID_RANGE.min || service_id > SERVICE_ID_RANGE.max) {
- throw new Error(`service_id должен быть в диапазоне ${SERVICE_ID_RANGE.min}-${SERVICE_ID_RANGE.max}`);
- }
-
- if (service_type === 'executor' && !api_url) {
- throw new Error('Для исполнителя необходимо указать api_url организатора');
- }
-
- if (local_user_id) {
- const userExists = await this.checkLocalUserExists(local_user_id);
- if (!userExists) {
- throw new Error(`Пользователь с ID ${local_user_id} не существует`);
- }
- }
-
- const existing = await this.getConnectionByServiceId(service_id);
- if (existing && existing.is_active) {
- throw new Error(`Подключение с service_id ${service_id} уже существует`);
- }
-
- const sql = `
- INSERT INTO upravlenie (
- service_id, service_name, service_type, login, password,
- api_url, local_user_id, local_user_login, sync_direction,
- sync_enabled, sync_interval
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `;
-
- return new Promise((resolve, reject) => {
- this.db.run(sql, [
- service_id, service_name, service_type, login, password,
- api_url, local_user_id || null, local_user_login || null,
- sync_direction, sync_enabled, sync_interval
- ], function(err) {
- if (err) {
- reject(err);
- } else {
- const connectionId = this.lastID;
- console.log(`✅ Создано подключение ${service_name} (ID: ${connectionId}, service_id: ${service_id})`);
- resolve({ id: connectionId, ...data });
- }
- });
- });
- }
-
- async checkLocalUserExists(userId) {
- return new Promise((resolve) => {
- this.db.get('SELECT id FROM users WHERE id = ?', [userId], (err, row) => {
- resolve(!!row);
- });
- });
- }
-
- getConnection(id) {
- return new Promise((resolve, reject) => {
- this.db.get('SELECT * FROM upravlenie WHERE id = ?', [id], (err, row) => {
- if (err) reject(err);
- else resolve(row);
- });
- });
- }
-
- getConnectionByServiceId(serviceId) {
- return new Promise((resolve, reject) => {
- this.db.get(
- 'SELECT * FROM upravlenie WHERE service_id = ? AND is_active = 1',
- [serviceId],
- (err, row) => {
- if (err) reject(err);
- else resolve(row);
- }
- );
- });
- }
-
- getAllConnections(filters = {}) {
- return new Promise((resolve, reject) => {
- let sql = 'SELECT * FROM upravlenie WHERE 1=1';
- const params = [];
-
- if (filters.service_type) {
- sql += ' AND service_type = ?';
- params.push(filters.service_type);
- }
-
- if (filters.is_active !== undefined) {
- sql += ' AND is_active = ?';
- params.push(filters.is_active ? 1 : 0);
- }
-
- if (filters.sync_enabled !== undefined) {
- sql += ' AND sync_enabled = ?';
- params.push(filters.sync_enabled ? 1 : 0);
- }
-
- sql += ' ORDER BY service_name';
-
- this.db.all(sql, params, (err, rows) => {
- if (err) reject(err);
- else resolve(rows);
- });
- });
- }
-
- async updateConnection(id, data) {
- const fields = [];
- const values = [];
-
- const allowedFields = [
- 'service_name', 'service_type', 'login', 'password',
- 'api_url', 'local_user_id', 'local_user_login',
- 'sync_direction', 'sync_enabled', 'sync_interval',
- 'is_active'
- ];
-
- allowedFields.forEach(field => {
- if (data[field] !== undefined) {
- fields.push(`${field} = ?`);
- values.push(data[field]);
- }
- });
-
- if (fields.length === 0) {
- throw new Error('Нет данных для обновления');
- }
-
- fields.push('updated_at = CURRENT_TIMESTAMP');
- values.push(id);
-
- const sql = `UPDATE upravlenie SET ${fields.join(', ')} WHERE id = ?`;
-
- return new Promise((resolve, reject) => {
- this.db.run(sql, values, function(err) {
- if (err) {
- reject(err);
- } else {
- console.log(`✅ Подключение ${id} обновлено`);
- resolve({ id, changes: this.changes });
- }
- });
- });
- }
-
- async deleteConnection(id) {
- return this.updateConnection(id, { is_active: 0 });
- }
-
- async startAllSyncJobs() {
- const connections = await this.getAllConnections({
- is_active: true,
- sync_enabled: true
- });
-
- for (const conn of connections) {
- this.startSyncJob(conn.id, conn.sync_interval);
- }
-
- console.log(`✅ Запущено ${connections.length} задач синхронизации`);
- }
-
- startSyncJob(connectionId, intervalMinutes) {
- this.stopSyncJob(connectionId);
-
- const intervalMs = intervalMinutes * 60 * 1000;
- const interval = setInterval(() => {
- this.syncConnection(connectionId);
- }, intervalMs);
-
- this.syncIntervals.set(connectionId, interval);
- console.log(`✅ Запущена синхронизация для подключения ${connectionId} (интервал: ${intervalMinutes} мин)`);
-
- setTimeout(() => {
- this.syncConnection(connectionId);
- }, 1000);
- }
-
- stopSyncJob(connectionId) {
- if (this.syncIntervals.has(connectionId)) {
- clearInterval(this.syncIntervals.get(connectionId));
- this.syncIntervals.delete(connectionId);
- console.log(`✅ Остановлена синхронизация для подключения ${connectionId}`);
- }
- }
-
- async restartSyncJob(connectionId) {
- this.stopSyncJob(connectionId);
-
- const connection = await this.getConnection(connectionId);
- if (connection && connection.is_active && connection.sync_enabled) {
- this.startSyncJob(connectionId, connection.sync_interval);
- }
- }
-
- async syncConnection(connectionId) {
- if (this.syncInProgress.has(connectionId)) {
- console.log(`⚠️ Синхронизация подключения ${connectionId} уже выполняется`);
- return;
- }
-
- this.syncInProgress.add(connectionId);
-
- try {
- const connection = await this.getConnection(connectionId);
- if (!connection || !connection.is_active || !connection.sync_enabled) {
- return;
- }
-
- console.log(`🔄 Синхронизация подключения ${connection.service_name} (${connection.service_id})...`);
-
- await this.updateSyncStatus(connectionId, 'in_progress');
-
- if (connection.service_type === 'organizer') {
- await this.syncFromExecutors(connection);
- } else {
- await this.syncToOrganizer(connection);
- }
-
- await this.updateSyncStatus(connectionId, 'success');
- console.log(`✅ Синхронизация подключения ${connection.service_name} завершена`);
-
- } catch (error) {
- console.error(`❌ Ошибка синхронизации подключения ${connectionId}:`, error.message);
- await this.updateSyncStatus(connectionId, 'error', error.message);
- } finally {
- this.syncInProgress.delete(connectionId);
- }
- }
-
- updateSyncStatus(connectionId, status, error = null) {
- return new Promise((resolve, reject) => {
- this.db.run(
- `UPDATE upravlenie
- SET last_sync_at = CURRENT_TIMESTAMP,
- last_sync_status = ?,
- last_sync_error = ?
- WHERE id = ?`,
- [status, error, connectionId],
- (err) => {
- if (err) reject(err);
- else resolve();
- }
- );
- });
- }
-
- async syncToOrganizer(connection) {
- if (!connection.api_url) {
- throw new Error('Для исполнителя не указан api_url организатора');
- }
-
- console.log(`🔍 Исполнитель ${connection.service_name} пытается связаться с организатором по URL: ${connection.api_url}`);
-
- if (!connection.local_user_id) {
- console.warn(`⚠️ Для подключения ${connection.service_name} не указан локальный пользователь. Задачи не будут создаваться.`);
- return;
- }
-
- try {
- console.log(`📡 Проверка доступности организатора...`);
-
- const organizerTasks = await this.fetchTasksFromOrganizer(connection);
- console.log(`📥 Получено ${organizerTasks.length} задач от организатора`);
-
- const localTasks = await this.getTasksForLocalUser(connection.local_user_id);
-
- if (organizerTasks.length > 0) {
- await this.syncTasksWithOrganizer(connection, organizerTasks, localTasks);
- }
-
- if (localTasks.length > 0) {
- await this.sendTaskStatusesToOrganizer(connection, localTasks);
- console.log(`📤 Отправлено ${localTasks.length} статусов организатору`);
- }
-
- } catch (error) {
- console.error(`❌ Ошибка связи с организатором:`, error.message);
- if (error.code === 'ECONNREFUSED') {
- console.error(`🔴 Организатор не доступен по адресу ${connection.api_url}`);
- } else if (error.code === 'ETIMEDOUT') {
- console.error(`⏱️ Таймаут при подключении к организатору`);
- } else if (error.response) {
- console.error(`📡 Ответ организатора:`, {
- status: error.response.status,
- data: error.response.data
- });
- }
- throw error;
- }
- }
-
- async syncFromExecutors(connection) {
- const executors = await this.getAllConnections({
- service_type: 'executor',
- is_active: true,
- sync_enabled: true
- });
-
- if (executors.length === 0) {
- console.log(`📭 Нет активных исполнителей для организатора ${connection.service_name}`);
- return;
- }
-
- for (const executor of executors) {
- try {
- if (!executor.local_user_id) {
- console.warn(`⚠️ Для исполнителя ${executor.service_name} не указан локальный пользователь. Пропускаем...`);
- continue;
- }
-
- const taskStatuses = await this.fetchTaskStatusesFromExecutor(executor);
-
- if (taskStatuses.length > 0) {
- await this.updateTaskStatusesFromExecutor(executor, taskStatuses);
- console.log(`✅ Получено ${taskStatuses.length} статусов от исполнителя ${executor.service_name}`);
- }
-
- } catch (error) {
- console.error(`❌ Ошибка получения статусов от исполнителя ${executor.service_name}:`, error.message);
- }
- }
- }
-
- getTasksForLocalUser(userId) {
- return new Promise((resolve, reject) => {
- if (!userId) {
- return resolve([]);
- }
-
- this.db.all(`
- SELECT t.*, ta.status as assignment_status, ta.due_date as assignment_due_date
- FROM tasks t
- JOIN task_assignments ta ON t.id = ta.task_id
- WHERE ta.user_id = ? AND t.status = 'active' AND t.closed_at IS NULL
- ORDER BY t.created_at DESC
- `, [userId], (err, rows) => {
- if (err) reject(err);
- else resolve(rows);
- });
- });
- }
-
- async fetchTasksFromOrganizer(connection) {
- try {
- const response = await axios.get(`${connection.api_url}/api/external/tasks`, {
- params: {
- service_id: connection.service_id,
- user_login: connection.local_user_login
- },
- auth: {
- username: connection.login,
- password: connection.password
- },
- timeout: 10000
- });
-
- return response.data.tasks || [];
- } catch (error) {
- console.error(`❌ Ошибка получения задач от организатора:`, error.message);
- throw new Error(`Не удалось получить задачи: ${error.message}`);
- }
- }
-
- async fetchTaskStatusesFromExecutor(executor) {
- try {
- const response = await axios.get(`${executor.api_url}/api/external/task-statuses`, {
- params: {
- service_id: executor.service_id
- },
- auth: {
- username: executor.login,
- password: executor.password
- },
- timeout: 10000
- });
-
- return response.data.statuses || [];
- } catch (error) {
- console.error(`❌ Ошибка получения статусов от исполнителя:`, error.message);
- throw error;
- }
- }
-
- async sendTaskStatusesToOrganizer(connection, localTasks) {
- const statuses = localTasks.map(task => ({
- task_id: task.id,
- external_task_id: task.external_task_id,
- status: task.assignment_status,
- completed_at: task.closed_at,
- comment: task.rework_comment
- }));
-
- try {
- await axios.post(`${connection.api_url}/api/external/task-statuses`, {
- service_id: connection.service_id,
- statuses: statuses
- }, {
- auth: {
- username: connection.login,
- password: connection.password
- },
- timeout: 10000
- });
-
- console.log(`✅ Отправлено ${statuses.length} статусов организатору`);
- } catch (error) {
- console.error(`❌ Ошибка отправки статусов организатору:`, error.message);
- throw error;
- }
- }
-
- async syncTasksWithOrganizer(connection, organizerTasks, localTasks) {
- for (const organizerTask of organizerTasks) {
- const localTask = localTasks.find(t => t.external_task_id === organizerTask.id);
-
- if (!localTask) {
- await this.createTaskFromOrganizer(connection, organizerTask);
- } else {
- await this.updateTaskFromOrganizer(connection, organizerTask, localTask);
- }
- }
- }
-
- async createTaskFromOrganizer(connection, taskData) {
- return new Promise((resolve, reject) => {
- if (!connection.local_user_id) {
- reject(new Error('local_user_id не может быть пустым. Укажите локального пользователя для создания задач.'));
- return;
- }
-
- this.db.all("PRAGMA table_info(tasks)", (err, columns) => {
- if (err) {
- reject(err);
- return;
- }
-
- const columnNames = columns.map(c => c.name);
-
- let fields = ['title', 'description', 'status', 'created_by', 'start_date', 'due_date', 'task_type'];
- let placeholders = ['?', '?', '?', '?', '?', '?', '?'];
- let values = [
- taskData.title,
- taskData.description || '',
- 'active',
- connection.local_user_id,
- taskData.start_date || new Date().toISOString(),
- taskData.due_date || null,
- 'external'
- ];
-
- if (columnNames.includes('external_task_id')) {
- fields.push('external_task_id');
- placeholders.push('?');
- values.push(taskData.id);
- }
-
- if (columnNames.includes('external_service_id')) {
- fields.push('external_service_id');
- placeholders.push('?');
- values.push(connection.service_id);
- }
-
- const sql = `INSERT INTO tasks (${fields.join(', ')}) VALUES (${placeholders.join(', ')})`;
-
- this.db.run(sql, values, function(err) {
- if (err) {
- reject(err);
- return;
- }
-
- const newTaskId = this.lastID;
-
- this.db.run(
- `INSERT INTO task_assignments (task_id, user_id, status, start_date, due_date)
- VALUES (?, ?, ?, ?, ?)`,
- [
- newTaskId,
- connection.local_user_id,
- taskData.status || 'assigned',
- taskData.start_date || new Date().toISOString(),
- taskData.due_date || null
- ],
- (err) => {
- if (err) {
- reject(err);
- return;
- }
-
- console.log(`✅ Создана задача ${newTaskId} от организатора ${connection.service_name}`);
- resolve(newTaskId);
- }
- );
- });
- });
- });
- }
-
- async updateTaskFromOrganizer(connection, organizerTask, localTask) {
- return new Promise((resolve, reject) => {
- this.db.run(
- `UPDATE tasks
- SET title = ?, description = ?, due_date = ?, updated_at = CURRENT_TIMESTAMP
- WHERE id = ?`,
- [organizerTask.title, organizerTask.description || '', organizerTask.due_date || null, localTask.id],
- (err) => {
- if (err) reject(err);
- else {
- console.log(`✅ Обновлена задача ${localTask.id} от организатора`);
- resolve();
- }
- }
- );
- });
- }
-
- async updateTaskStatusesFromExecutor(executor, taskStatuses) {
- for (const status of taskStatuses) {
- await new Promise((resolve, reject) => {
- this.db.run(
- `UPDATE task_assignments
- SET status = ?, updated_at = CURRENT_TIMESTAMP
- WHERE task_id = ? AND user_id = ?`,
- [
- status.status,
- status.task_id,
- executor.local_user_id
- ],
- function(err) {
- if (err) reject(err);
- else {
- if (this.changes > 0) {
- console.log(`✅ Обновлен статус задачи ${status.task_id}: ${status.status}`);
- }
- resolve();
- }
- }
- );
- });
-
- if (status.status === 'completed') {
- await this.checkAndCloseTaskIfAllCompleted(status.task_id);
- }
- }
- }
-
- async checkAndCloseTaskIfAllCompleted(taskId) {
- return new Promise((resolve) => {
- this.db.all(
- 'SELECT status FROM task_assignments WHERE task_id = ?',
- [taskId],
- (err, assignments) => {
- if (!err && assignments && assignments.length > 0) {
- const allCompleted = assignments.every(a => a.status === 'completed');
-
- if (allCompleted) {
- this.db.run(
- 'UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
- [taskId],
- (err) => {
- if (!err) {
- console.log(`✅ Задача ${taskId} автоматически закрыта`);
- }
- resolve();
- }
- );
- } else {
- resolve();
- }
- } else {
- resolve();
- }
- }
- );
- });
- }
-
- async uploadFileToRemote(connection, taskId, fileId) {
- return new Promise((resolve, reject) => {
- this.db.get(
- 'SELECT * FROM task_files WHERE id = ?',
- [fileId],
- async (err, file) => {
- if (err || !file) {
- reject(err || new Error('Файл не найден'));
- return;
- }
-
- if (!fs.existsSync(file.file_path)) {
- reject(new Error('Файл не существует на диске'));
- return;
- }
-
- try {
- const formData = new FormData();
- formData.append('file', fs.createReadStream(file.file_path), {
- filename: file.original_name,
- contentType: 'application/octet-stream'
- });
- formData.append('task_id', taskId);
- formData.append('service_id', connection.service_id);
-
- const response = await axios.post(
- `${connection.api_url}/api/external/upload-file`,
- formData,
- {
- headers: formData.getHeaders(),
- auth: {
- username: connection.login,
- password: connection.password
- },
- timeout: 30000,
- maxContentLength: Infinity,
- maxBodyLength: Infinity
- }
- );
-
- console.log(`✅ Файл ${file.original_name} загружен в удаленный сервис`);
- resolve(response.data);
-
- } catch (error) {
- console.error(`❌ Ошибка загрузки файла в удаленный сервис:`, error.message);
- reject(error);
- }
- }
- );
- });
- }
-
- async downloadFileFromRemote(connection, remoteFileId, localTaskId) {
- try {
- const response = await axios.get(
- `${connection.api_url}/api/external/download-file/${remoteFileId}`,
- {
- params: { service_id: connection.service_id },
- auth: {
- username: connection.login,
- password: connection.password
- },
- responseType: 'stream',
- timeout: 30000
- }
- );
-
- const { createUserTaskFolder } = require('./database');
- const userFolder = createUserTaskFolder(localTaskId, connection.local_user_login || 'external');
-
- const fileName = response.headers['x-file-name'] || `remote_${Date.now()}.bin`;
- const filePath = path.join(userFolder, fileName);
-
- const writer = fs.createWriteStream(filePath);
- response.data.pipe(writer);
-
- await new Promise((resolve, reject) => {
- writer.on('finish', resolve);
- writer.on('error', reject);
- });
-
- const fileSize = fs.statSync(filePath).size;
-
- return new Promise((resolve, reject) => {
- this.db.run(
- `INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size)
- VALUES (?, ?, ?, ?, ?, ?)`,
- [
- localTaskId,
- connection.local_user_id,
- fileName,
- fileName,
- filePath,
- fileSize
- ],
- function(err) {
- if (err) reject(err);
- else {
- console.log(`✅ Файл ${fileName} загружен из удаленного сервиса`);
- resolve({ id: this.lastID, filePath, fileName });
- }
- }
- );
- });
-
- } catch (error) {
- console.error(`❌ Ошибка загрузки файла из удаленного сервиса:`, error.message);
- throw error;
- }
- }
-
- generateAuthToken(serviceId, login) {
- const secret = process.env.EXTERNAL_API_SECRET || 'default_secret_change_me';
- const timestamp = Math.floor(Date.now() / 1000);
- const data = `${serviceId}:${login}:${timestamp}`;
- const hash = crypto.createHmac('sha256', secret).update(data).digest('hex');
- return `${timestamp}:${hash}`;
- }
-
- verifyAuthToken(token, serviceId, login) {
- try {
- const secret = process.env.EXTERNAL_API_SECRET || 'default_secret_change_me';
- const [timestamp, hash] = token.split(':');
-
- const now = Math.floor(Date.now() / 1000);
- if (now - parseInt(timestamp) > 300) {
- return false;
- }
-
- const data = `${serviceId}:${login}:${timestamp}`;
- const expectedHash = crypto.createHmac('sha256', secret).update(data).digest('hex');
-
- return crypto.timingSafeEqual(
- Buffer.from(hash, 'hex'),
- Buffer.from(expectedHash, 'hex')
- );
- } catch (error) {
- return false;
- }
- }
-
- async manualSync(connectionId) {
- console.log(`🔄 Ручная синхронизация подключения ${connectionId}`);
- await this.syncConnection(connectionId);
- }
-
- async checkConnection(connectionId) {
- const connection = await this.getConnection(connectionId);
- if (!connection) {
- return { success: false, error: 'Подключение не найдено' };
- }
-
- const result = {
- id: connection.id,
- service_id: connection.service_id,
- service_name: connection.service_name,
- service_type: connection.service_type,
- local_user_id: connection.local_user_id,
- api_url: connection.api_url,
- status: 'unknown'
- };
-
- if (connection.service_type === 'executor') {
- if (!connection.api_url) {
- result.status = 'error';
- result.error = 'Не указан API URL организатора';
- return result;
- }
-
- try {
- const response = await axios.get(`${connection.api_url}/api/health`, {
- timeout: 5000,
- auth: {
- username: connection.login,
- password: connection.password
- }
- });
-
- result.status = 'connected';
- result.organizer_status = response.data;
-
- } catch (error) {
- result.status = 'disconnected';
- result.error = error.message;
-
- if (error.code === 'ECONNREFUSED') {
- result.error_details = 'Организатор не доступен по указанному адресу';
- } else if (error.response?.status === 401) {
- result.error_details = 'Ошибка аутентификации - проверьте логин/пароль';
- } else if (error.response?.status === 404) {
- result.error_details = 'API организатора не найдено - проверьте URL';
- } else {
- result.error_details = error.message;
- }
- }
- } else {
- const executors = await this.getAllConnections({
- service_type: 'executor',
- is_active: true
- });
-
- result.executors_count = executors.length;
- result.status = executors.length > 0 ? 'has_executors' : 'no_executors';
- }
-
- return result;
- }
-
- async getSyncStats() {
- const connections = await this.getAllConnections({ is_active: true });
-
- const stats = {
- total: connections.length,
- organizers: 0,
- executors: 0,
- lastSyncs: [],
- errors: []
- };
-
- connections.forEach(conn => {
- if (conn.service_type === 'organizer') {
- stats.organizers++;
- } else {
- stats.executors++;
- }
-
- if (conn.last_sync_at) {
- stats.lastSyncs.push({
- service: conn.service_name,
- last_sync: conn.last_sync_at,
- status: conn.last_sync_status
- });
- }
-
- if (conn.last_sync_error) {
- stats.errors.push({
- service: conn.service_name,
- error: conn.last_sync_error,
- time: conn.last_sync_at
- });
- }
- });
-
- return stats;
- }
-}
-
-function setupUpravlenieEndpoints(app, db) {
- const upravlenieService = new UpravlenieService(db);
-
- const requireAuth = (req, res, next) => {
- if (!req.session || !req.session.user) {
- return res.status(401).json({ error: 'Требуется аутентификация' });
- }
- next();
- };
-
- const requireAdmin = (req, res, next) => {
- if (!req.session || !req.session.user) {
- return res.status(401).json({ error: 'Требуется аутентификация' });
- }
- if (req.session.user.role !== 'admin') {
- return res.status(403).json({ error: 'Требуются права администратора' });
- }
- next();
- };
-
- app.get('/api/upravlenie/connections', requireAdmin, async (req, res) => {
- try {
- const filters = {
- service_type: req.query.service_type,
- is_active: req.query.is_active !== undefined ? req.query.is_active === 'true' : undefined,
- sync_enabled: req.query.sync_enabled !== undefined ? req.query.sync_enabled === 'true' : undefined
- };
-
- const connections = await upravlenieService.getAllConnections(filters);
- res.json(connections);
- } catch (error) {
- console.error('❌ Ошибка получения подключений:', error);
- res.status(500).json({ error: error.message });
- }
- });
-
- app.get('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => {
- try {
- const connection = await upravlenieService.getConnection(req.params.id);
- if (!connection) {
- return res.status(404).json({ error: 'Подключение не найдено' });
- }
- res.json(connection);
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
- });
-
- app.post('/api/upravlenie/connections', requireAdmin, async (req, res) => {
- try {
- const connection = await upravlenieService.createConnection(req.body);
- res.json({
- success: true,
- message: 'Подключение создано',
- connection: connection
- });
- } catch (error) {
- console.error('❌ Ошибка создания подключения:', error);
- res.status(400).json({ error: error.message });
- }
- });
-
- app.put('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => {
- try {
- const result = await upravlenieService.updateConnection(req.params.id, req.body);
- res.json({
- success: true,
- message: 'Подключение обновлено',
- changes: result.changes
- });
- } catch (error) {
- res.status(400).json({ error: error.message });
- }
- });
-
- app.delete('/api/upravlenie/connections/:id', requireAdmin, async (req, res) => {
- try {
- await upravlenieService.deleteConnection(req.params.id);
- res.json({ success: true, message: 'Подключение удалено' });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
- });
-
- app.post('/api/upravlenie/connections/:id/sync', requireAdmin, async (req, res) => {
- try {
- await upravlenieService.manualSync(req.params.id);
- res.json({ success: true, message: 'Синхронизация запущена' });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
- });
-
- app.get('/api/upravlenie/check/:id', requireAdmin, async (req, res) => {
- try {
- const result = await upravlenieService.checkConnection(req.params.id);
- res.json(result);
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
- });
-
- app.get('/api/upravlenie/stats', requireAdmin, async (req, res) => {
- try {
- const stats = await upravlenieService.getSyncStats();
- res.json(stats);
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
- });
-
- const authenticateExternal = async (req, res, next) => {
- const authHeader = req.headers.authorization;
-
- if (!authHeader || !authHeader.startsWith('Basic ')) {
- return res.status(401).json({ error: 'Требуется базовая аутентификация' });
- }
-
- const base64Credentials = authHeader.split(' ')[1];
- const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
- const [login, password] = credentials.split(':');
-
- try {
- const connection = await new Promise((resolve, reject) => {
- db.get(
- 'SELECT * FROM upravlenie WHERE login = ? AND password = ? AND is_active = 1',
- [login, password],
- (err, row) => {
- if (err) reject(err);
- else resolve(row);
- }
- );
- });
-
- if (!connection) {
- return res.status(401).json({ error: 'Неверные учетные данные' });
- }
-
- req.externalConnection = connection;
- next();
- } catch (error) {
- console.error('❌ Ошибка аутентификации:', error);
- res.status(500).json({ error: 'Ошибка аутентификации' });
- }
- };
-
- app.get('/api/external/tasks', authenticateExternal, async (req, res) => {
- try {
- const { service_id, user_login } = req.query;
- const connection = req.externalConnection;
-
- if (parseInt(service_id) !== connection.service_id) {
- return res.status(403).json({ error: 'Неверный service_id' });
- }
-
- let userId = null;
- if (user_login) {
- const user = await new Promise((resolve) => {
- db.get('SELECT id FROM users WHERE login = ?', [user_login], (err, row) => {
- resolve(row);
- });
- });
- userId = user?.id;
- }
-
- let tasks = [];
- if (userId) {
- tasks = await new Promise((resolve) => {
- db.all(`
- SELECT t.*, ta.status as assignment_status
- FROM tasks t
- JOIN task_assignments ta ON t.id = ta.task_id
- WHERE ta.user_id = ? AND t.status = 'active' AND t.closed_at IS NULL
- `, [userId], (err, rows) => {
- resolve(rows || []);
- });
- });
- } else {
- tasks = await new Promise((resolve) => {
- db.all(`
- SELECT t.*, GROUP_CONCAT(ta.user_id) as assigned_users
- FROM tasks t
- LEFT JOIN task_assignments ta ON t.id = ta.task_id
- WHERE t.status = 'active' AND t.closed_at IS NULL
- GROUP BY t.id
- `, [], (err, rows) => {
- resolve(rows || []);
- });
- });
- }
-
- res.json({
- service_id: connection.service_id,
- tasks: tasks.map(t => ({
- id: t.id,
- title: t.title,
- description: t.description,
- status: t.assignment_status || t.status,
- created_at: t.created_at,
- due_date: t.due_date,
- assigned_users: t.assigned_users
- }))
- });
-
- } catch (error) {
- console.error('❌ Ошибка получения внешних задач:', error);
- res.status(500).json({ error: error.message });
- }
- });
-
- app.post('/api/external/task-statuses', authenticateExternal, async (req, res) => {
- try {
- const { service_id, statuses } = req.body;
- const connection = req.externalConnection;
-
- if (parseInt(service_id) !== connection.service_id) {
- return res.status(403).json({ error: 'Неверный service_id' });
- }
-
- if (!Array.isArray(statuses)) {
- return res.status(400).json({ error: 'statuses должен быть массивом' });
- }
-
- for (const status of statuses) {
- await new Promise((resolve, reject) => {
- db.run(
- `UPDATE task_assignments
- SET status = ?, updated_at = CURRENT_TIMESTAMP
- WHERE task_id = ?`,
- [status.status, status.task_id],
- function(err) {
- if (err) reject(err);
- else resolve();
- }
- );
- });
-
- if (status.status === 'completed') {
- await checkAndCloseTaskIfAllCompleted(db, status.task_id);
- }
- }
-
- res.json({
- success: true,
- message: `Получено ${statuses.length} статусов`,
- processed: statuses.length
- });
-
- } catch (error) {
- console.error('❌ Ошибка получения статусов от исполнителя:', error);
- res.status(500).json({ error: error.message });
- }
- });
-
- app.get('/api/external/task-statuses', authenticateExternal, async (req, res) => {
- try {
- const { service_id } = req.query;
- const connection = req.externalConnection;
-
- if (parseInt(service_id) !== connection.service_id) {
- return res.status(403).json({ error: 'Неверный service_id' });
- }
-
- const statuses = await new Promise((resolve) => {
- db.all(`
- SELECT
- t.id as task_id,
- t.external_task_id,
- ta.status,
- t.closed_at as completed_at,
- t.rework_comment as comment
- FROM tasks t
- JOIN task_assignments ta ON t.id = ta.task_id
- WHERE t.external_service_id = ?
- `, [service_id], (err, rows) => {
- resolve(rows || []);
- });
- });
-
- res.json({
- service_id: connection.service_id,
- statuses: statuses
- });
-
- } catch (error) {
- console.error('❌ Ошибка получения статусов для организатора:', error);
- res.status(500).json({ error: error.message });
- }
- });
-
- app.post('/api/external/upload-file', authenticateExternal, async (req, res) => {
- if (!req.files || !req.files.file) {
- return res.status(400).json({ error: 'Файл не загружен' });
- }
-
- try {
- const { task_id, service_id } = req.body;
- const file = req.files.file;
- const connection = req.externalConnection;
-
- if (parseInt(service_id) !== connection.service_id) {
- return res.status(403).json({ error: 'Неверный service_id' });
- }
-
- const { createUserTaskFolder } = require('./database');
- const userFolder = createUserTaskFolder(task_id, connection.local_user_login || 'external');
-
- const fileName = `${Date.now()}_${file.name}`;
- const filePath = path.join(userFolder, fileName);
-
- await file.mv(filePath);
-
- const result = await new Promise((resolve, reject) => {
- db.run(
- `INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size)
- VALUES (?, ?, ?, ?, ?, ?)`,
- [
- task_id,
- connection.local_user_id,
- fileName,
- file.name,
- filePath,
- file.size
- ],
- function(err) {
- if (err) reject(err);
- else resolve({ id: this.lastID });
- }
- );
- });
-
- res.json({
- success: true,
- file_id: result.id,
- message: 'Файл успешно загружен'
- });
-
- } catch (error) {
- console.error('❌ Ошибка загрузки внешнего файла:', error);
- res.status(500).json({ error: error.message });
- }
- });
-
- app.get('/api/external/download-file/:fileId', authenticateExternal, async (req, res) => {
- try {
- const { fileId } = req.params;
- const { service_id } = req.query;
- const connection = req.externalConnection;
-
- if (parseInt(service_id) !== connection.service_id) {
- return res.status(403).json({ error: 'Неверный service_id' });
- }
-
- const file = await new Promise((resolve, reject) => {
- db.get(
- 'SELECT * FROM task_files WHERE id = ?',
- [fileId],
- (err, row) => {
- if (err) reject(err);
- else resolve(row);
- }
- );
- });
-
- if (!file) {
- return res.status(404).json({ error: 'Файл не найден' });
- }
-
- if (!fs.existsSync(file.file_path)) {
- return res.status(404).json({ error: 'Файл не существует на диске' });
- }
-
- res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(file.original_name)}"`);
- res.setHeader('X-File-Name', encodeURIComponent(file.original_name));
- res.setHeader('Content-Type', 'application/octet-stream');
-
- fs.createReadStream(file.file_path).pipe(res);
-
- } catch (error) {
- console.error('❌ Ошибка скачивания файла:', error);
- res.status(500).json({ error: error.message });
- }
- });
-
- async function checkAndCloseTaskIfAllCompleted(db, taskId) {
- return new Promise((resolve) => {
- db.all(
- 'SELECT status FROM task_assignments WHERE task_id = ?',
- [taskId],
- (err, assignments) => {
- if (!err && assignments && assignments.length > 0) {
- const allCompleted = assignments.every(a => a.status === 'completed');
-
- if (allCompleted) {
- db.run(
- 'UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
- [taskId],
- (err) => {
- if (!err) {
- console.log(`✅ Задача ${taskId} автоматически закрыта`);
- }
- resolve();
- }
- );
- } else {
- resolve();
- }
- } else {
- resolve();
- }
- }
- );
- });
- }
-
- return upravlenieService;
-}
-
-module.exports = {
- UpravlenieService,
- setupUpravlenieEndpoints,
- TaskStatus,
- SyncDirection
-};
\ No newline at end of file