// api-doc.js - API endpoints для согласования документов const path = require('path'); const fs = require('fs'); const archiver = require('archiver'); module.exports = function(app, db, upload) { // API для типов документов app.get('/api/document-types', requireAuth, (req, res) => { db.all("SELECT * FROM document_types ORDER BY name", [], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // API для создания документа app.post('/api/documents', requireAuth, upload.array('files', 15), async (req, res) => { try { const userId = req.session.user.id; const { title, description, dueDate, documentTypeId, documentNumber, documentDate, pagesCount, urgencyLevel, comment } = req.body; // Валидация if (!title || title.trim() === '') { return res.status(400).json({ error: 'Название документа обязательно' }); } // Получаем пользователей для согласования из .env const doc1User = process.env.DOC1_USER; const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; if (!doc1User) { return res.status(400).json({ error: 'Не настроен DOC1_USER в системе' }); } // Находим пользователя DOC1 db.get("SELECT id FROM users WHERE login = ?", [doc1User], async (err, doc1) => { if (err || !doc1) { return res.status(400).json({ error: `Пользователь DOC1 (${doc1User}) не найден` }); } // Создаем задачу для документа db.run(` INSERT INTO tasks (title, description, due_date, created_by, status, created_at) VALUES (?, ?, ?, ?, 'active', datetime('now')) `, [ `Документ: ${title}`, description || '', dueDate || null, userId ], function(err) { if (err) { return res.status(500).json({ error: err.message }); } const taskId = this.lastID; // Создаем запись документа db.run(` INSERT INTO documents ( task_id, document_type_id, document_number, document_date, pages_count, urgency_level, comment, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now')) `, [ taskId, documentTypeId || null, documentNumber || null, documentDate || null, pagesCount || null, urgencyLevel || 'normal', comment || null ], function(err) { if (err) { console.error('❌ Ошибка создания документа:', err); db.run("DELETE FROM tasks WHERE id = ?", [taskId]); return res.status(500).json({ error: 'Ошибка создания записи документа' }); } const documentId = this.lastID; // Назначаем DOC1 для предварительного согласования db.run(` INSERT INTO task_assignments (task_id, user_id, status, created_at) VALUES (?, ?, 'assigned', datetime('now')) `, [taskId, doc1.id], function(err) { if (err) { console.error('❌ Ошибка назначения DOC1:', err); db.run("DELETE FROM documents WHERE id = ?", [documentId]); db.run("DELETE FROM tasks WHERE id = ?", [taskId]); return res.status(500).json({ error: 'Ошибка назначения документа на согласование' }); } const doc1AssignmentId = this.lastID; // Загружаем файлы если есть if (req.files && req.files.length > 0) { const uploadPromises = req.files.map(file => { return new Promise((resolve, reject) => { const filePath = file.path; const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8'); db.run(` INSERT INTO task_files (task_id, user_id, file_path, original_name, file_size, uploaded_at) VALUES (?, ?, ?, ?, ?, datetime('now')) `, [taskId, userId, filePath, originalName, file.size], function(err) { if (err) { console.error('❌ Ошибка сохранения файла:', err); reject(err); } else { resolve(); } }); }); }); Promise.all(uploadPromises) .then(() => { // Логируем создание документа const { logActivity } = require('./database'); logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ: ${title}`); // Отправляем уведомление DOC1 sendDocumentNotification(doc1.id, taskId, 'new_document', title); res.json({ success: true, message: 'Документ успешно создан и отправлен на предварительное согласование' }); }) .catch(error => { // Все равно возвращаем успех, так как документ создан const { logActivity } = require('./database'); logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ: ${title}`); // Отправляем уведомление DOC1 sendDocumentNotification(doc1.id, taskId, 'new_document', title); res.json({ success: true, message: 'Документ создан, но были проблемы с загрузкой файлов' }); }); } else { // Логируем создание документа const { logActivity } = require('./database'); logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ: ${title}`); // Отправляем уведомление DOC1 sendDocumentNotification(doc1.id, taskId, 'new_document', title); res.json({ success: true, message: 'Документ успешно создан и отправлен на предварительное согласование' }); } }); }); }); }); } catch (error) { console.error('❌ Общая ошибка создания документа:', error); res.status(500).json({ error: 'Ошибка создания документа', details: error.message }); } }); // Получение моих документов app.get('/api/documents/my', requireAuth, (req, res) => { const userId = req.session.user.id; db.all(` SELECT t.id, t.title, t.description, t.due_date, t.created_at, t.status, t.closed_at, d.id as document_id, d.document_type_id, dt.name as document_type_name, d.document_number, d.document_date, d.pages_count, d.urgency_level, d.comment, d.refusal_reason, ta.status as assignment_status FROM tasks t LEFT JOIN documents d ON t.id = d.task_id LEFT JOIN document_types dt ON d.document_type_id = dt.id LEFT JOIN task_assignments ta ON t.id = ta.task_id WHERE t.created_by = ? AND t.title LIKE 'Документ:%' ORDER BY t.created_at DESC `, [userId], async (err, tasks) => { if (err) { return res.status(500).json({ error: err.message }); } // Загружаем файлы для каждой задачи const tasksWithFiles = await Promise.all(tasks.map(async (task) => { try { const files = await new Promise((resolve, reject) => { db.all(` SELECT tf.*, u.name as user_name FROM task_files tf LEFT JOIN users u ON tf.user_id = u.id WHERE tf.task_id = ? ORDER BY tf.uploaded_at DESC `, [task.id], (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); task.files = files || []; } catch (error) { task.files = []; } return task; })); res.json(tasksWithFiles); }); }); // Получение документов для согласования (DOC1 и DOC2) app.get('/api/documents/approval', requireAuth, (req, res) => { const userId = req.session.user.id; // Проверяем, является ли пользователь DOC1 или DOC2 db.get("SELECT login FROM users WHERE id = ?", [userId], (err, user) => { if (err || !user) { return res.status(403).json({ error: 'Пользователь не найден' }); } const userLogin = user.login; const doc1User = process.env.DOC1_USER; const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; const isDoc1 = userLogin === doc1User; const isDoc2 = doc2Users.includes(userLogin); if (!isDoc1 && !isDoc2) { return res.status(403).json({ error: 'Недостаточно прав для согласования документов' }); } // Определяем статус для фильтрации let statusFilter = ''; if (isDoc1) { // DOC1 видит документы на предварительном согласовании statusFilter = "AND ta.status IN ('assigned', 'pre_approved')"; } else if (isDoc2) { // DOC2 видит предварительно согласованные документы statusFilter = "AND ta.status = 'pre_approved'"; } db.all(` SELECT t.id, t.title, t.description, t.due_date, t.created_at, d.id as document_id, d.document_type_id, dt.name as document_type_name, d.document_number, d.document_date, d.pages_count, d.urgency_level, d.comment, d.refusal_reason, ta.status as assignment_status, u.name as creator_name, u.login as creator_login FROM tasks t JOIN documents d ON t.id = d.task_id LEFT JOIN document_types dt ON d.document_type_id = dt.id JOIN task_assignments ta ON t.id = ta.task_id JOIN users u ON t.created_by = u.id WHERE ta.user_id = ? AND t.title LIKE 'Документ:%' AND t.status = 'active' AND t.closed_at IS NULL ${statusFilter} ORDER BY CASE d.urgency_level WHEN 'very_urgent' THEN 1 WHEN 'urgent' THEN 2 ELSE 3 END, t.due_date ASC, t.created_at DESC `, [userId], async (err, tasks) => { if (err) { return res.status(500).json({ error: err.message }); } // Загружаем файлы для каждой задачи const tasksWithFiles = await Promise.all(tasks.map(async (task) => { try { const files = await new Promise((resolve, reject) => { db.all(` SELECT tf.*, u.name as user_name FROM task_files tf LEFT JOIN users u ON tf.user_id = u.id WHERE tf.task_id = ? ORDER BY tf.uploaded_at DESC `, [task.id], (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); task.files = files || []; } catch (error) { task.files = []; } return task; })); res.json(tasksWithFiles); }); }); }); // Обновление статуса документа app.put('/api/documents/:id/status', requireAuth, (req, res) => { const documentId = req.params.id; const { status, comment, refusalReason } = req.body; const userId = req.session.user.id; // Проверяем права пользователя db.get("SELECT login FROM users WHERE id = ?", [userId], (err, user) => { if (err || !user) { return res.status(403).json({ error: 'Пользователь не найден' }); } const userLogin = user.login; const doc1User = process.env.DOC1_USER; const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; const isDoc1 = userLogin === doc1User; const isDoc2 = doc2Users.includes(userLogin); if (!isDoc1 && !isDoc2) { return res.status(403).json({ error: 'Недостаточно прав' }); } // Проверяем текущий статус документа db.get(` SELECT d.*, ta.status as assignment_status, ta.user_id as assignee_id, t.title, t.created_by FROM documents d JOIN tasks t ON d.task_id = t.id JOIN task_assignments ta ON t.id = ta.task_id WHERE d.id = ? AND ta.user_id = ? `, [documentId, userId], (err, document) => { if (err || !document) { return res.status(404).json({ error: 'Документ не найден или у вас нет прав' }); } // Валидация переходов статусов if (isDoc1) { // DOC1 может только предварительно согласовать или отказать if (status !== 'pre_approved' && status !== 'refused') { return res.status(400).json({ error: 'DOC1 может только предварительно согласовать или отказать' }); } if (status === 'pre_approved') { // Если DOC1 предварительно согласовал, назначаем DOC2 updateDoc1Status(); } else { // Если отказал, просто обновляем статус updateStatus(); } } else if (isDoc2) { // DOC2 может согласовать или отказать только предварительно согласованные документы if (document.assignment_status !== 'pre_approved') { return res.status(400).json({ error: 'DOC2 может работать только с предварительно согласованными документами' }); } if (status === 'approved' || status === 'refused') { updateStatus(); } else { return res.status(400).json({ error: 'DOC2 может только согласовать или отказать' }); } } function updateDoc1Status() { // Обновляем статус DOC1 db.run("UPDATE task_assignments SET status = ? WHERE task_id = ? AND user_id = ?", [status, document.task_id, userId], function(err) { if (err) { return res.status(500).json({ error: err.message }); } // Назначаем DOC2 пользователям const doc2Logins = doc2Users; // Находим ID пользователей DOC2 const placeholders = doc2Logins.map(() => '?').join(','); db.all(`SELECT id FROM users WHERE login IN (${placeholders})`, doc2Logins, (err, doc2UsersList) => { if (err || doc2UsersList.length === 0) { console.error('DOC2 пользователи не найдены'); // Все равно считаем успехом return afterAssignments(); } // Создаем задания для каждого DOC2 const assignmentPromises = doc2UsersList.map(doc2User => { return new Promise((resolve, reject) => { db.run(` INSERT INTO task_assignments (task_id, user_id, status, created_at) VALUES (?, ?, 'assigned', datetime('now')) `, [document.task_id, doc2User.id], function(err) { if (err) reject(err); else resolve(); }); }); }); Promise.all(assignmentPromises) .then(() => { afterAssignments(); }) .catch(error => { console.error('Ошибка назначения DOC2:', error); afterAssignments(); // Все равно продолжаем }); }); function afterAssignments() { // Логируем действие const { logActivity } = require('./database'); logActivity(document.task_id, userId, 'STATUS_CHANGED', `Документ предварительно согласован. Назначен DOC2: ${doc2Logins.join(', ')}`); // Сохраняем комментарий if (comment) { db.run("UPDATE documents SET comment = COALESCE(comment, '') || '\n' || ? WHERE id = ?", [`DOC1 (${new Date().toLocaleString('ru-RU')}): ${comment}`, documentId]); } // Отправляем уведомление создателю sendDocumentNotification(document.created_by, document.task_id, 'pre_approved', document.title); // Отправляем уведомления всем DOC2 doc2Users.forEach(login => { db.get("SELECT id FROM users WHERE login = ?", [login], (err, doc2User) => { if (!err && doc2User) { sendDocumentNotification(doc2User.id, document.task_id, 'new_document_for_doc2', document.title); } }); }); res.json({ success: true }); } } ); } function updateStatus() { // Обновляем статус db.run("UPDATE task_assignments SET status = ? WHERE task_id = ? AND user_id = ?", [status, document.task_id, userId], function(err) { if (err) { return res.status(500).json({ error: err.message }); } // Сохраняем комментарий и причину отказа if (comment) { const role = isDoc1 ? 'DOC1' : 'DOC2'; const timestamp = new Date().toLocaleString('ru-RU'); db.run("UPDATE documents SET comment = COALESCE(comment, '') || '\n' || ? WHERE id = ?", [`${role} (${timestamp}): ${comment}`, documentId]); } if (status === 'refused' && refusalReason) { db.run("UPDATE documents SET refusal_reason = ? WHERE id = ?", [refusalReason, documentId]); } // Логируем действие const { logActivity } = require('./database'); const actionText = isDoc1 ? `DOC1: ${status === 'refused' ? 'Отказано' : 'Предварительно согласовано'}` : `DOC2: ${status === 'refused' ? 'Отказано' : 'Согласовано'}`; logActivity(document.task_id, userId, 'STATUS_CHANGED', actionText); // Отправляем уведомление создателю if (status === 'approved') { sendDocumentNotification(document.created_by, document.task_id, 'approved', document.title); } else if (status === 'refused') { sendDocumentNotification(document.created_by, document.task_id, 'refused', document.title); } res.json({ success: true }); } ); } }); }); }); // Отзыв документа app.post('/api/documents/:id/cancel', requireAuth, (req, res) => { const documentId = req.params.id; const userId = req.session.user.id; db.get("SELECT task_id FROM documents WHERE id = ?", [documentId], (err, document) => { if (err || !document) { return res.status(404).json({ error: 'Документ не найден' }); } const taskId = document.task_id; // Проверяем, что пользователь создатель задачи db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } if (parseInt(task.created_by) !== parseInt(userId)) { return res.status(403).json({ error: 'Вы не являетесь создателем этого документа' }); } // Получаем всех согласующих для уведомлений db.all("SELECT user_id FROM task_assignments WHERE task_id = ?", [taskId], (err, assignees) => { if (err) { console.error('Ошибка получения согласующих:', err); } // Обновляем статус задачи db.run("UPDATE tasks SET status = 'cancelled', closed_at = datetime('now') WHERE id = ?", [taskId], function(err) { if (err) { return res.status(500).json({ error: err.message }); } // Логируем действие const { logActivity } = require('./database'); logActivity(taskId, userId, 'STATUS_CHANGED', 'Документ отозван создателем'); // Отправляем уведомления согласующим if (assignees) { assignees.forEach(assignee => { if (assignee.user_id !== userId) { sendDocumentNotification(assignee.user_id, taskId, 'cancelled', 'Документ отозван'); } }); } res.json({ success: true }); }); }); }); }); }); // Получение пакета документов (ZIP архив) app.get('/api/documents/:id/package', requireAuth, async (req, res) => { const documentId = req.params.id; const userId = req.session.user.id; // Проверяем доступ к документу db.get(` SELECT t.id, t.created_by, t.title, d.document_number FROM documents d JOIN tasks t ON d.task_id = t.id WHERE d.id = ? `, [documentId], async (err, result) => { if (err || !result) { return res.status(404).json({ error: 'Документ не найден' }); } // Проверяем, что пользователь имеет доступ const isCreator = parseInt(result.created_by) === parseInt(userId); const isDoc1orDoc2 = await checkIfDoc1orDoc2(userId); if (!isCreator && !isDoc1orDoc2) { return res.status(403).json({ error: 'Недостаточно прав' }); } try { // Получаем все файлы документа db.all(` SELECT tf.* FROM task_files tf JOIN tasks t ON tf.task_id = t.id JOIN documents d ON t.id = d.task_id WHERE d.id = ? `, [documentId], async (err, files) => { if (err) { return res.status(500).json({ error: err.message }); } if (files.length === 0) { return res.json({ success: true, message: 'Нет файлов для скачивания' }); } // Создаем временный файл для архива const tempDir = path.join(__dirname, 'temp'); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } const zipFileName = `document_${documentId}_${Date.now()}.zip`; const zipFilePath = path.join(tempDir, zipFileName); const output = fs.createWriteStream(zipFilePath); const archive = archiver('zip', { zlib: { level: 9 } }); output.on('close', () => { // Отправляем файл res.download(zipFilePath, `Документ_${result.document_number || result.id}.zip`, (err) => { // Удаляем временный файл после отправки if (fs.existsSync(zipFilePath)) { fs.unlinkSync(zipFilePath); } }); }); archive.on('error', (err) => { console.error('Ошибка создания архива:', err); res.status(500).json({ error: 'Ошибка создания архива' }); }); archive.pipe(output); // Добавляем файлы в архив for (const file of files) { if (fs.existsSync(file.file_path)) { const fileName = path.basename(file.original_name); archive.file(file.file_path, { name: fileName }); } } // Добавляем информацию о документе как текстовый файл const docInfo = ` Документ: ${result.title} Номер документа: ${result.document_number || 'Не указан'} Дата создания: ${new Date().toLocaleString('ru-RU')} Файлы в архиве: ${files.map((f, i) => `${i + 1}. ${f.original_name} (${formatFileSize(f.file_size)})`).join('\n')} `; archive.append(docInfo, { name: 'Информация_о_документе.txt' }); await archive.finalize(); }); } catch (error) { console.error('Ошибка создания пакета:', error); res.status(500).json({ success: false, message: 'Ошибка создания пакета документов' }); } }); }); // Статистика по документам app.get('/api/documents/stats', requireAuth, (req, res) => { const userId = req.session.user.id; // Проверяем права db.get("SELECT login FROM users WHERE id = ?", [userId], (err, user) => { if (err || !user) { return res.status(403).json({ error: 'Пользователь не найден' }); } const userLogin = user.login; const doc1User = process.env.DOC1_USER; const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; const isDoc1 = userLogin === doc1User; const isDoc2 = doc2Users.includes(userLogin); const isAdmin = req.session.user.role === 'admin'; if (!isDoc1 && !isDoc2 && !isAdmin) { return res.status(403).json({ error: 'Недостаточно прав' }); } let statsQuery = ''; let params = []; if (isAdmin) { // Админ видит все документы statsQuery = ` SELECT COUNT(*) as total, COUNT(CASE WHEN t.status = 'active' AND t.closed_at IS NULL THEN 1 END) as active, COUNT(CASE WHEN ta.status = 'pre_approved' THEN 1 END) as pre_approved, COUNT(CASE WHEN ta.status = 'approved' THEN 1 END) as approved, COUNT(CASE WHEN ta.status = 'refused' THEN 1 END) as refused, COUNT(CASE WHEN t.status = 'cancelled' THEN 1 END) as cancelled FROM tasks t LEFT JOIN documents d ON t.id = d.task_id LEFT JOIN task_assignments ta ON t.id = ta.task_id WHERE t.title LIKE 'Документ:%' `; } else { // DOC1 и DOC2 видят только свои документы statsQuery = ` SELECT COUNT(*) as total, COUNT(CASE WHEN ta.status = 'assigned' THEN 1 END) as assigned, COUNT(CASE WHEN ta.status = 'pre_approved' THEN 1 END) as pre_approved, COUNT(CASE WHEN ta.status = 'approved' THEN 1 END) as approved, COUNT(CASE WHEN ta.status = 'refused' THEN 1 END) as refused FROM tasks t JOIN documents d ON t.id = d.task_id JOIN task_assignments ta ON t.id = ta.task_id WHERE t.title LIKE 'Документ:%' AND t.status = 'active' AND t.closed_at IS NULL AND ta.user_id = ? `; params = [userId]; } db.get(statsQuery, params, (err, stats) => { if (err) { return res.status(500).json({ error: err.message }); } res.json(stats || { total: 0, active: 0, pre_approved: 0, approved: 0, refused: 0, cancelled: 0 }); }); }); }); // Получение истории документа app.get('/api/documents/:id/history', requireAuth, (req, res) => { const documentId = req.params.id; const userId = req.session.user.id; // Проверяем доступ к документу db.get(` SELECT t.created_by FROM documents d JOIN tasks t ON d.task_id = t.id WHERE d.id = ? `, [documentId], (err, document) => { if (err || !document) { return res.status(404).json({ error: 'Документ не найден' }); } // Проверяем права const isCreator = parseInt(document.created_by) === parseInt(userId); const isDoc1orDoc2 = checkIfDoc1orDoc2Sync(userId); if (!isCreator && !isDoc1orDoc2) { return res.status(403).json({ error: 'Недостаточно прав' }); } const taskIdQuery = "SELECT task_id FROM documents WHERE id = ?"; db.get(taskIdQuery, [documentId], (err, result) => { if (err || !result) { return res.status(500).json({ error: 'Ошибка получения истории' }); } const taskId = result.task_id; // Получаем историю активности db.all(` SELECT al.*, u.name as user_name FROM activity_logs al LEFT JOIN users u ON al.user_id = u.id WHERE al.task_id = ? ORDER BY al.created_at DESC `, [taskId], (err, history) => { if (err) { return res.status(500).json({ error: err.message }); } // Получаем комментарии из документа db.get("SELECT comment FROM documents WHERE id = ?", [documentId], (err, doc) => { if (err) { doc = { comment: '' }; } const comments = doc.comment ? doc.comment.split('\n').filter(c => c.trim()).map(c => { return { text: c, type: 'comment' }; }) : []; res.json({ activity: history || [], comments: comments }); }); }); }); }); }); // Вспомогательные функции // Функция для проверки DOC1/DOC2 (асинхронная) function checkIfDoc1orDoc2(userId) { return new Promise((resolve, reject) => { db.get("SELECT login FROM users WHERE id = ?", [userId], (err, user) => { if (err || !user) { resolve(false); return; } const userLogin = user.login; const doc1User = process.env.DOC1_USER; const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; const isDoc1 = userLogin === doc1User; const isDoc2 = doc2Users.includes(userLogin); resolve(isDoc1 || isDoc2); }); }); } // Функция для проверки DOC1/DOC2 (синхронная версия) function checkIfDoc1orDoc2Sync(userId) { // Эта функция используется в синхронных контекстах // В реальном приложении нужно быть осторожным с синхронными вызовами try { const user = db.getSync("SELECT login FROM users WHERE id = ?", [userId]); if (!user) return false; const userLogin = user.login; const doc1User = process.env.DOC1_USER; const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; const isDoc1 = userLogin === doc1User; const isDoc2 = doc2Users.includes(userLogin); return isDoc1 || isDoc2; } catch (error) { return false; } } // Функция для отправки уведомлений о документах function sendDocumentNotification(userId, taskId, type, documentTitle) { try { const { sendTaskNotifications } = require('./notifications'); let message = ''; switch(type) { case 'new_document': message = `Новый документ на согласование: ${documentTitle}`; break; case 'new_document_for_doc2': message = `Новый документ для согласования (DOC2): ${documentTitle}`; break; case 'pre_approved': message = `Документ предварительно согласован: ${documentTitle}`; break; case 'approved': message = `Документ согласован: ${documentTitle}`; break; case 'refused': message = `В согласовании документа отказано: ${documentTitle}`; break; case 'cancelled': message = `Документ отозван создателем: ${documentTitle}`; break; default: message = `Обновление документа: ${documentTitle}`; } sendTaskNotifications(taskId, userId, message); } catch (error) { console.error('Ошибка отправки уведомления:', error); } } // Вспомогательная функция для форматирования размера файла function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // Middleware для аутентификации (можно импортировать из server.js) function requireAuth(req, res, next) { if (!req.session.user) { return res.status(401).json({ error: 'Требуется аутентификация' }); } next(); } }; //