// upravlenie-service.js /** * Модуль управления подключениями между сервисами * Позволяет организовать цепочку сервисов (организатор -> исполнители) * Каждый сервис имеет общий service_id (1-4062) для идентификации в цепочке */ 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; // 1 минута const MAX_RETRY_COUNT = 3; const RETRY_DELAY = 5000; // 5 секунд // Статусы задач для синхронизации 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 инициализирован'); } /** * Создание таблицы 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); } }); }); } /** * Создание индексов для таблицы upravlenie */ 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; // Валидация service_id 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}`); } // Для организатора api_url может быть пустым if (service_type === 'executor' && !api_url) { throw new Error('Для исполнителя необходимо указать api_url организатора'); } // Проверяем уникальность service_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})`); // Запускаем синхронизацию для нового подключения if (sync_enabled) { this.startSyncJob(connectionId, sync_interval); } resolve({ id: connectionId, ...data }); } }); }); } /** * Получение подключения по ID */ 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); }); }); } /** * Получение подключения по service_id */ 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} обновлено`); // Перезапускаем задачу синхронизации если изменились настройки if (data.sync_enabled !== undefined || data.sync_interval !== undefined) { this.restartSyncJob(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 }); connections.forEach(conn => { 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 организатора'); } // Получаем задачи, назначенные локальному пользователю const localTasks = await this.getTasksForLocalUser(connection.local_user_id); // Получаем задачи от организатора const organizerTasks = await this.fetchTasksFromOrganizer(connection); // Обновляем локальные задачи await this.syncTasksWithOrganizer(connection, organizerTasks, localTasks); // Отправляем обновленные статусы организатору await this.sendTaskStatusesToOrganizer(connection, localTasks); } /** * Синхронизация от организатора к исполнителям */ async syncFromExecutors(connection) { // Получаем все активные подключения исполнителей с таким же service_id const executors = await this.getAllConnections({ service_type: 'executor', is_active: true, sync_enabled: true }); for (const executor of executors) { try { // Получаем статусы задач от исполнителя const taskStatuses = await this.fetchTaskStatusesFromExecutor(executor); // Обновляем статусы в локальной БД await this.updateTaskStatusesFromExecutor(executor, taskStatuses); } 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); }); }); } /** * Получение задач от организатора через API */ 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) => { this.db.serialize(() => { // Создаем задачу this.db.run( `INSERT INTO tasks ( title, description, status, created_by, external_task_id, external_service_id, start_date, due_date, task_type ) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, 'external')`, [ taskData.title, taskData.description || '', connection.local_user_id, taskData.id, connection.service_id, taskData.start_date || new Date().toISOString(), taskData.due_date || null ], 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 = ( SELECT id FROM tasks WHERE external_task_id = ? AND external_service_id = ? ) AND user_id = ?`, [ status.status, status.external_task_id, executor.service_id, executor.local_user_id ], function(err) { if (err) reject(err); else { if (this.changes > 0) { console.log(`✅ Обновлен статус задачи ${status.external_task_id}: ${status.status}`); } 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); 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(':'); // Проверяем, что токен не старше 5 минут 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 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; } } // Функция для настройки API endpoints function setupUpravlenieEndpoints(app, db) { const upravlenieService = new UpravlenieService(db); // Middleware для проверки аутентификации const requireAuth = (req, res, next) => { if (!req.session.user) { return res.status(401).json({ error: 'Требуется аутентификация' }); } next(); }; // Middleware для проверки прав администратора const requireAdmin = (req, res, next) => { if (!req.session.user || req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Требуются права администратора' }); } next(); }; // ==================== ВНУТРЕННИЕ API (для администрирования) ==================== /** * GET /api/upravlenie/connections * Получение всех подключений */ 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 }); } }); /** * GET /api/upravlenie/connections/:id * Получение подключения по ID */ 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 }); } }); /** * POST /api/upravlenie/connections * Создание нового подключения */ app.post('/api/upravlenie/connections', requireAdmin, async (req, res) => { try { const connection = await upravlenieService.createConnection(req.body); res.json({ success: true, message: 'Подключение создано', connection }); } catch (error) { console.error('❌ Ошибка создания подключения:', error); res.status(400).json({ error: error.message }); } }); /** * PUT /api/upravlenie/connections/:id * Обновление подключения */ 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 }); } }); /** * DELETE /api/upravlenie/connections/:id * Удаление подключения */ 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 }); } }); /** * POST /api/upravlenie/connections/:id/sync * Ручная синхронизация подключения */ 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 }); } }); /** * GET /api/upravlenie/stats * Статистика синхронизации */ 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 }); } }); // ==================== ВНЕШНИЕ API (для взаимодействия между сервисами) ==================== /** * Middleware для аутентификации внешних запросов */ 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) { res.status(500).json({ error: 'Ошибка аутентификации' }); } }; /** * GET /api/external/tasks * Получение задач для внешнего сервиса * (используется исполнителем для получения задач от организатора) */ app.get('/api/external/tasks', authenticateExternal, async (req, res) => { try { const { service_id, user_login } = req.query; const connection = req.externalConnection; // Проверяем соответствие service_id if (parseInt(service_id) !== connection.service_id) { return res.status(403).json({ error: 'Неверный service_id' }); } // Если указан user_login, ищем задачи для этого пользователя 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 }); } }); /** * POST /api/external/task-statuses * Получение статусов задач от исполнителя */ 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(); } ); }); // Если статус "completed" - закрываем задачу если все исполнители выполнили if (status.status === 'completed') { await checkAndCloseTaskIfAllCompleted(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 }); } }); /** * GET /api/external/task-statuses * Получение статусов задач для организатора */ 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 }); } }); /** * POST /api/external/upload-file * Загрузка файла от исполнителя */ 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 }); } }); /** * GET /api/external/download-file/:fileId * Скачивание файла для исполнителя */ 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(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 };