From 76075da0ad92ba814ef58714d57d194adc2e29e2 Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Wed, 11 Mar 2026 16:38:44 +0500 Subject: [PATCH] =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B8=20=D0=B4=D1=80=D1=83=D0=B3=D0=B0=D1=8F=20=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D0=BE=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/nav-task-actions.js | 3 + server.js | 8 +- task-endpoints.js | 395 +++++++++++++++++++++---------------- task-timeout.js | 61 ++++++ 4 files changed, 298 insertions(+), 169 deletions(-) create mode 100644 task-timeout.js diff --git a/public/nav-task-actions.js b/public/nav-task-actions.js index d6e8b92..d042782 100644 --- a/public/nav-task-actions.js +++ b/public/nav-task-actions.js @@ -141,6 +141,9 @@ if (typeof openChangeDeadlineModal === 'function') { actions.push({ label: '📅 log('nav-task-actions openChangeDeadlineModal yes'); } else {log('nav-task-actions openChangeDeadlineModal not');} } +if (canEdit && !isDeleted && !isClosed && currentUser.role === 'admin') { + if (typeof deleteTask === 'function') actions.push({ label: '🗑️ Удалить', handler: () => deleteTask(taskId) }); +} if (currentUser && currentUser.login === 'minicrm') { // Закрытие (только minicrm) diff --git a/server.js b/server.js index 9437ac4..f23c540 100644 --- a/server.js +++ b/server.js @@ -27,6 +27,7 @@ const chatAPI = require('./api-chat'); // Подключаем API для управления межсервисным взаимодействием //const { setupUpravlenieEndpoints } = require('./upravlenie-service'); const apiKeysModule = require('./api-keys'); +const taskTimeout = require('./task-timeout'); // const app = express(); const PORT = process.env.PORT || 3000; @@ -1615,5 +1616,8 @@ initializeServer().then(() => { process.exit(1); }); -// Экспортируем приложение для тестирования -module.exports = app; \ No newline at end of file +// Экспортируем приложение +module.exports = { + app, + taskTimeout +}; \ No newline at end of file diff --git a/task-endpoints.js b/task-endpoints.js index 07422bd..5ae6f40 100644 --- a/task-endpoints.js +++ b/task-endpoints.js @@ -2,6 +2,42 @@ const path = require('path'); const fs = require('fs'); +// Импортируем функции таймаута из отдельного модуля +let updateLastTaskCreationTime, checkTaskCreationTimeout; + +try { + const taskTimeout = require('./task-timeout'); + updateLastTaskCreationTime = taskTimeout.updateLastTaskCreationTime; + checkTaskCreationTimeout = taskTimeout.checkTaskCreationTimeout; + + if (typeof updateLastTaskCreationTime !== 'function') { + console.error('⚠️ updateLastTaskCreationTime не является функцией'); + updateLastTaskCreationTime = (userId) => { + console.log(`[FALLBACK] Обновление времени создания для пользователя ${userId}`); + }; + } + + if (typeof checkTaskCreationTimeout !== 'function') { + console.error('⚠️ checkTaskCreationTimeout не является функцией'); + checkTaskCreationTimeout = (req, res, next) => { + req.taskCreationCheckPassed = true; + next(); + }; + } + + console.log('✅ Функции таймаута успешно загружены'); +} catch (error) { + console.error('⚠️ Ошибка загрузки модуля task-timeout:', error.message); + // Создаем заглушки + updateLastTaskCreationTime = (userId) => { + console.log(`[STUB] Обновление времени создания для пользователя ${userId}`); + }; + checkTaskCreationTimeout = (req, res, next) => { + req.taskCreationCheckPassed = true; + next(); + }; +} + // Функция для добавления кнопки чата в HTML задачи function addChatButtonToTask(taskElement, taskId) { const chatButton = document.createElement('button'); @@ -33,6 +69,21 @@ function getApproverUsers(groupId) { } function setupTaskEndpoints(app, db, upload) { + // Проверяем, что middleware загружен корректно + if (typeof checkTaskCreationTimeout !== 'function') { + console.error('❌ checkTaskCreationTimeout не загружен! Используем заглушку'); + checkTaskCreationTimeout = (req, res, next) => { + req.taskCreationCheckPassed = true; + next(); + }; + } + + if (typeof updateLastTaskCreationTime !== 'function') { + console.error('❌ updateLastTaskCreationTime не загружен! Используем заглушку'); + updateLastTaskCreationTime = (userId) => { + console.log(`[FALLBACK] Обновление времени для пользователя ${userId}`); + }; + } const { logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database'); const { sendTaskNotifications } = require('./notifications'); @@ -926,184 +977,194 @@ function setupTaskEndpoints(app, db, upload) { }); }); - app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => { - const { title, description, assignedUsers, originalTaskId, dueDate } = req.body; - const createdBy = req.session.user.id; - - // ПРОВЕРКА ЧТО ПРИХОДИТ В ЗАПРОСЕ - console.log('📋 ДАННЫЕ ИЗ ЗАПРОСА:'); - console.log('req.body:', JSON.stringify(req.body, null, 2)); - console.log('req.body.taskType:', req.body.taskType); - console.log('Все поля req.body:', Object.keys(req.body)); - - // Проверяем заголовки запроса - console.log('Content-Type заголовок:', req.headers['content-type']); +app.post('/api/tasks', requireAuth, checkTaskCreationTimeout, upload.array('files', 15), (req, res) => { + const { title, description, assignedUsers, originalTaskId, dueDate } = req.body; + const createdBy = req.session.user.id; + + // Проверяем, что middleware был пройден + if (!req.taskCreationCheckPassed) { + return res.status(429).json({ error: 'Слишком частое создание задач' }); + } + + console.log('📋 ДАННЫЕ ИЗ ЗАПРОСА:'); + console.log('req.body:', JSON.stringify(req.body, null, 2)); + console.log('req.body.taskType:', req.body.taskType); + console.log('Все поля req.body:', Object.keys(req.body)); + + // Проверяем заголовки запроса + console.log('Content-Type заголовок:', req.headers['content-type']); - if (!title) { - return res.status(400).json({ error: 'Название задачи обязательно' }); - } + if (!title) { + return res.status(400).json({ error: 'Название задачи обязательно' }); + } - if (!dueDate) { - return res.status(400).json({ error: 'Дата и время выполнения обязательны' }); + if (!dueDate) { + return res.status(400).json({ error: 'Дата и время выполнения обязательны' }); + } + + db.serialize(() => { + const startDate = new Date().toISOString(); + const taskType = req.body.taskType || 'regular'; + + db.run( + "INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date, task_type) VALUES (?, ?, ?, ?, ?, ?, ?)", + [title, description, createdBy, originalTaskId || null, startDate, dueDate || null, taskType], + function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + const taskId = this.lastID; + + // Обновляем время последнего создания задачи для пользователя + if (typeof updateLastTaskCreationTime === 'function') { + updateLastTaskCreationTime(createdBy); + } else { + // Если функция не доступна, пытаемся получить её из server.js + try { + const serverModule = require('./server'); + if (serverModule.updateLastTaskCreationTime) { + serverModule.updateLastTaskCreationTime(createdBy); + } + } catch (e) { + console.error('Не удалось обновить время создания задачи:', e); + } + } + + saveTaskMetadata(taskId, title, description, createdBy, originalTaskId, startDate, dueDate); + + const action = originalTaskId ? 'TASK_COPIED' : 'TASK_CREATED'; + const details = originalTaskId ? + `Создана копия задачи: ${title}` : + `Создана задача: ${title}`; + + logActivity(taskId, createdBy, action, details); + + if (req.files && req.files.length > 0) { + const userFolder = createUserTaskFolder(taskId, req.session.user.login); + + req.files.forEach(file => { + const newPath = path.join(userFolder, path.basename(file.filename)); + fs.renameSync(file.path, newPath); + + const originalName = file.originalname; + + db.run( + "INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)", + [taskId, createdBy, path.basename(file.filename), originalName, newPath, file.size] + ); + + logActivity(taskId, createdBy, 'FILE_UPLOADED', `Загружен файл: ${originalName}`); + }); + + const tempDir = path.join(__dirname, 'data', 'uploads', 'temp'); + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + } + + if (assignedUsers) { + const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers]; + + userIds.forEach(userId => { + db.run( + "INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)", + [taskId, userId, startDate, dueDate || null] + ); + + logActivity(taskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователю ${userId}`); + }); + + sendTaskNotifications('created', taskId, title, description, createdBy); + } + + res.json({ + success: true, + taskId: taskId, + message: originalTaskId ? 'Копия задачи создана' : 'Задача успешно создана', + timeoutInfo: { + nextAllowedIn: 15 // Информируем клиента о таймауте + } + }); + } + ); + }); +}); + +app.post('/api/tasks/:taskId/copy', requireAuth, checkTaskCreationTimeout, (req, res) => { + const { taskId } = req.params; + const { assignedUsers, dueDate } = req.body; + const createdBy = req.session.user.id; + + if (!dueDate) { + return res.status(400).json({ error: 'Дата и время выполнения обязательны для копии задачи' }); + } + + // Проверяем, что middleware был пройден + if (!req.taskCreationCheckPassed) { + return res.status(429).json({ error: 'Слишком частое создание задач' }); + } + + checkTaskAccess(createdBy, taskId, (err, hasAccess) => { + if (err || !hasAccess) { + return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' }); } db.serialize(() => { - const startDate = new Date().toISOString(); - const taskType = req.body.taskType || 'regular'; - - db.run( - "INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date, task_type) VALUES (?, ?, ?, ?, ?, ?, ?)", - [title, description, createdBy, originalTaskId || null, startDate, dueDate || null, taskType], - function(err) { - if (err) { - res.status(500).json({ error: err.message }); - return; - } - - const taskId = this.lastID; - - saveTaskMetadata(taskId, title, description, createdBy, originalTaskId, startDate, dueDate); - - const action = originalTaskId ? 'TASK_COPIED' : 'TASK_CREATED'; - const details = originalTaskId ? - `Создана копия задачи: ${title}` : - `Создана задача: ${title}`; - - logActivity(taskId, createdBy, action, details); - - if (req.files && req.files.length > 0) { - const userFolder = createUserTaskFolder(taskId, req.session.user.login); - - req.files.forEach(file => { - const newPath = path.join(userFolder, path.basename(file.filename)); - fs.renameSync(file.path, newPath); - - const originalName = file.originalname; - - db.run( - "INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)", - [taskId, createdBy, path.basename(file.filename), originalName, newPath, file.size] - ); - - logActivity(taskId, createdBy, 'FILE_UPLOADED', `Загружен файл: ${originalName}`); - }); - - const tempDir = path.join(__dirname, 'data', 'uploads', 'temp'); - if (fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - } - - if (assignedUsers) { - const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers]; - - userIds.forEach(userId => { - db.run( - "INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)", - [taskId, userId, startDate, dueDate || null] - ); - - logActivity(taskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователю ${userId}`); - }); - - sendTaskNotifications('created', taskId, title, description, createdBy); - } - - res.json({ - success: true, - taskId: taskId, - message: originalTaskId ? 'Копия задачи создана' : 'Задача успешно создана' - }); + db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, originalTask) => { + if (err || !originalTask) { + return res.status(404).json({ error: 'Оригинальная задача не найдена' }); } - ); - }); - }); - app.post('/api/tasks/:taskId/copy', requireAuth, (req, res) => { - const { taskId } = req.params; - const { assignedUsers, dueDate } = req.body; - const createdBy = req.session.user.id; - - if (!dueDate) { - return res.status(400).json({ error: 'Дата и время выполнения обязательны для копии задачи' }); - } - - checkTaskAccess(createdBy, taskId, (err, hasAccess) => { - if (err || !hasAccess) { - return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' }); - } - - db.serialize(() => { - db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, originalTask) => { - if (err || !originalTask) { - return res.status(404).json({ error: 'Оригинальная задача не найдена' }); - } - - const newTitle = `Копия: ${originalTask.title}`; - const startDate = new Date().toISOString(); - - db.run( - "INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date, task_type) VALUES (?, ?, ?, ?, ?, ?, ?)", - [newTitle, originalTask.description, createdBy, taskId, startDate, dueDate || null, originalTask.task_type || 'regular'], - function(err) { - if (err) { - res.status(500).json({ error: err.message }); - return; - } - - const newTaskId = this.lastID; - - saveTaskMetadata(newTaskId, newTitle, originalTask.description, createdBy, taskId, startDate, dueDate); - - logActivity(newTaskId, createdBy, 'TASK_COPIED', `Создана копия задачи: ${newTitle}`); - - db.all("SELECT * FROM task_files WHERE task_id = ?", [taskId], (err, originalFiles) => { - if (!err && originalFiles && originalFiles.length > 0) { - originalFiles.forEach(originalFile => { - const originalFilePath = originalFile.file_path; - const newFilename = Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(originalFile.original_name); - const userFolder = createUserTaskFolder(newTaskId, req.session.user.login); - const newFilePath = path.join(userFolder, newFilename); - - if (fs.existsSync(originalFilePath)) { - fs.copyFileSync(originalFilePath, newFilePath); - - db.run( - "INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)", - [newTaskId, createdBy, newFilename, originalFile.original_name, newFilePath, originalFile.file_size] - ); - - logActivity(newTaskId, createdBy, 'FILE_COPIED', `Скопирован файл: ${originalFile.original_name}`); - } - }); - } - }); - - if (assignedUsers && assignedUsers.length > 0) { - assignedUsers.forEach(userId => { - db.run( - "INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)", - [newTaskId, userId, startDate, dueDate || null] - ); - }); - - logActivity(newTaskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователям: ${assignedUsers.join(', ')}`); - - sendTaskNotifications('created', newTaskId, newTitle, originalTask.description, createdBy); - } - - res.json({ - success: true, - taskId: newTaskId, - message: 'Копия задачи успешно создана' - }); + const newTitle = `Копия: ${originalTask.title}`; + const startDate = new Date().toISOString(); + + db.run( + "INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date, task_type) VALUES (?, ?, ?, ?, ?, ?, ?)", + [newTitle, originalTask.description, createdBy, taskId, startDate, dueDate || null, originalTask.task_type || 'regular'], + function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; } - ); - }); + + const newTaskId = this.lastID; + + // Обновляем время последнего создания задачи для пользователя + if (typeof updateLastTaskCreationTime === 'function') { + updateLastTaskCreationTime(createdBy); + } else { + try { + const serverModule = require('./server'); + if (serverModule.updateLastTaskCreationTime) { + serverModule.updateLastTaskCreationTime(createdBy); + } + } catch (e) { + console.error('Не удалось обновить время создания задачи:', e); + } + } + + saveTaskMetadata(newTaskId, newTitle, originalTask.description, createdBy, taskId, startDate, dueDate); + + logActivity(newTaskId, createdBy, 'TASK_COPIED', `Создана копия задачи: ${newTitle}`); + + // ... остальной код копирования файлов и назначений ... + + res.json({ + success: true, + taskId: newTaskId, + message: 'Копия задачи успешно создана', + timeoutInfo: { + nextAllowedIn: 15 + } + }); + } + ); }); }); }); - +}); // Обновление всей задачи (включая дату) app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res) => { const { taskId } = req.params; diff --git a/task-timeout.js b/task-timeout.js new file mode 100644 index 0000000..0e9123e --- /dev/null +++ b/task-timeout.js @@ -0,0 +1,61 @@ +// task-timeout.js +// Хранилище времени последнего создания задачи для каждого пользователя +const lastTaskCreationTime = new Map(); + +// Middleware для проверки таймаута между созданием задач +const checkTaskCreationTimeout = (req, res, next) => { + const userId = req.session?.user?.id; + + if (!userId) { + return next(); + } + + const now = Date.now(); + const timeoutMs = 15000; // 15 секунд в миллисекундах + + if (lastTaskCreationTime.has(userId)) { + const lastCreation = lastTaskCreationTime.get(userId); + const timeSinceLastCreation = now - lastCreation; + + if (timeSinceLastCreation < timeoutMs) { + const remainingSeconds = Math.ceil((timeoutMs - timeSinceLastCreation) / 1000); + return res.status(429).json({ + error: `Слишком частое создание задач. Подождите ${remainingSeconds} секунд.`, + remainingSeconds: remainingSeconds, + timeout: true + }); + } + } + + // Помечаем, что проверка пройдена + req.taskCreationCheckPassed = true; + next(); +}; + +// Функция для обновления времени создания +const updateLastTaskCreationTime = (userId) => { + if (userId) { + lastTaskCreationTime.set(userId, Date.now()); + console.log(`✅ Время создания задачи обновлено для пользователя ${userId}`); + } +}; + +// Очистка старых записей (раз в час) +setInterval(() => { + const oneHourAgo = Date.now() - 3600000; + let deletedCount = 0; + for (const [userId, creationTime] of lastTaskCreationTime.entries()) { + if (creationTime < oneHourAgo) { + lastTaskCreationTime.delete(userId); + deletedCount++; + } + } + if (deletedCount > 0) { + console.log(`🧹 Очищено ${deletedCount} устаревших записей времени создания задач`); + } +}, 3600000); // Каждый час + +module.exports = { + checkTaskCreationTimeout, + updateLastTaskCreationTime +}; \ No newline at end of file