Files
minicrm/upravlenie-service.js
2026-02-25 16:21:37 +05:00

1362 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
};