// 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 };