const express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const session = require('express-session'); const fetch = require('node-fetch'); require('dotenv').config(); const { db, logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database'); const authService = require('./auth'); const adminRouter = require('./admin-server'); const postgresLogger = require('./postgres'); const app = express(); const PORT = process.env.PORT || 3000; app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static('public')); app.use('/uploads', express.static(path.join(__dirname, 'data', 'uploads'))); app.use(session({ secret: process.env.SESSION_SECRET || 'fallback_secret_change_in_production', resave: true, saveUninitialized: false, cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000, httpOnly: true } })); app.use(adminRouter); const requireAuth = (req, res, next) => { if (!req.session.user) { return res.status(401).json({ error: 'Требуется аутентификация' }); } next(); }; const storage = multer.diskStorage({ destination: (req, file, cb) => { const taskId = req.body.taskId || req.params.taskId; const userLogin = req.session.user.login; if (taskId) { const userFolder = createUserTaskFolder(taskId, userLogin); cb(null, userFolder); } else { const tempDir = path.join(__dirname, 'data', 'uploads', 'temp'); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } cb(null, tempDir); } }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage: storage, limits: { fileSize: 300 * 1024 * 1024, files: 15 } }); const createDirIfNotExists = (dirPath) => { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } }; function checkIfOverdue(dueDate, status) { if (!dueDate || status === 'completed') return false; const now = new Date(); const due = new Date(dueDate); return due < now; } function checkOverdueTasks() { const now = new Date().toISOString(); const query = ` SELECT ta.id, ta.task_id, ta.user_id, ta.status, ta.due_date FROM task_assignments ta JOIN tasks t ON ta.task_id = t.id WHERE ta.due_date IS NOT NULL AND ta.due_date < ? AND ta.status NOT IN ('completed', 'overdue') AND t.status = 'active' AND t.closed_at IS NULL `; db.all(query, [now], (err, assignments) => { if (err) { console.error('Ошибка при проверке просроченных задач:', err); return; } assignments.forEach(assignment => { db.run( "UPDATE task_assignments SET status = 'overdue' WHERE id = ?", [assignment.id] ); logActivity(assignment.task_id, assignment.user_id, 'STATUS_CHANGED', 'Задача просрочена'); }); }); } function checkUpcomingDeadlines() { const now = new Date(); const in48Hours = new Date(now.getTime() + 48 * 60 * 60 * 1000).toISOString(); const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(); const nowISO = now.toISOString(); const query = ` SELECT DISTINCT ta.*, t.title, t.description, t.created_by, u.name as user_name, u.email as user_email, creator.name as creator_name, creator.email as creator_email FROM task_assignments ta JOIN tasks t ON ta.task_id = t.id JOIN users u ON ta.user_id = u.id JOIN users creator ON t.created_by = creator.id WHERE ta.due_date IS NOT NULL AND ta.due_date > ? AND ta.due_date <= ? AND ta.status NOT IN ('completed', 'overdue') AND t.status = 'active' AND t.closed_at IS NULL `; db.all(query, [nowISO, in48Hours], async (err, assignments) => { if (err) { console.error('Ошибка при проверке сроков задач:', err); return; } for (const assignment of assignments) { const dueDate = new Date(assignment.due_date); const timeLeft = dueDate.getTime() - now.getTime(); const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000)); if (hoursLeft <= 48 && hoursLeft > 24) { await sendDeadlineNotification(assignment, 48); } else if (hoursLeft <= 24) { await sendDeadlineNotification(assignment, 24); } } }); } async function sendDeadlineNotification(assignment, hoursLeft) { try { if (!process.env.NOTIFICATION_SERVICE_URL || !process.env.NOTIFICATION_SERVICE_LOGIN || !process.env.NOTIFICATION_SERVICE_PASSWORD) { return; } const notificationKey = `deadline_${hoursLeft}h_task_${assignment.task_id}_user_${assignment.user_id}`; const lastSent = await getLastNotificationSent(notificationKey); const now = new Date(); if (lastSent) { const timeSinceLast = now.getTime() - new Date(lastSent).getTime(); if (timeSinceLast < 12 * 60 * 60 * 1000) { return; } } const subject = `⚠️ До окончания срока задачи осталось менее ${hoursLeft} часов`; const content = `Задача: ${assignment.title}\n\n` + `Описание: ${assignment.description || 'Без описания'}\n` + `Срок выполнения: ${new Date(assignment.due_date).toLocaleString('ru-RU')}\n` + `Осталось времени: ${hoursLeft} часов\n\n` + `Пожалуйста, завершите задачу в срок.`; const recipients = [ { id: assignment.user_id, name: assignment.user_name, email: assignment.user_email }, { id: assignment.created_by, name: assignment.creator_name, email: assignment.creator_email } ].filter((value, index, self) => self.findIndex(r => r.id === value.id) === index ); const recipientIds = recipients.map(r => r.id); const authHeader = Buffer.from( `${process.env.NOTIFICATION_SERVICE_LOGIN}:${process.env.NOTIFICATION_SERVICE_PASSWORD}` ).toString('base64'); const FormData = require('form-data'); const formData = new FormData(); formData.append('subject', subject); formData.append('content', content); formData.append('recipients', JSON.stringify(recipientIds)); formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk'])); const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, { method: 'POST', headers: { 'Authorization': `Basic ${authHeader}` }, body: formData }); if (response.ok) { await saveNotificationSent(notificationKey); console.log(`✅ Уведомление о сроке (${hoursLeft}ч) отправлено для задачи ${assignment.task_id}`); } } catch (error) { console.error('❌ Ошибка отправки уведомления о сроке:', error); } } function getLastNotificationSent(key) { return new Promise((resolve) => { db.get("SELECT created_at FROM notification_logs WHERE notification_key = ? ORDER BY created_at DESC LIMIT 1", [key], (err, row) => { resolve(row ? row.created_at : null); } ); }); } function saveNotificationSent(key) { db.run("INSERT INTO notification_logs (notification_key) VALUES (?)", [key]); } function encodeBasicAuth(login, password) { return Buffer.from(`${login}:${password}`).toString('base64'); } async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, authorId, comment = '', status = '', userName = '') { try { if (!process.env.NOTIFICATION_SERVICE_URL || !process.env.NOTIFICATION_SERVICE_LOGIN || !process.env.NOTIFICATION_SERVICE_PASSWORD) { console.log('⚠️ Настройки сервиса уведомлений не заданы'); // Логируем в PostgreSQL даже если уведомления не отправляются await logNotificationToPostgres({ type, taskId, taskTitle, taskDescription, authorId, comment, status, userName, error: 'Сервис уведомлений не настроен' }); return; } console.log(`📢 Начинаем отправку уведомлений для задачи ${taskId}:`); console.log(` Тип: ${type}, Автор действия: ${authorId}, Название: ${taskTitle}`); // Получаем заказчика (создателя задачи) ОТДЕЛЬНО const creator = await new Promise((resolve, reject) => { db.get(` SELECT t.created_by as user_id, u.name as user_name, u.login as user_login, u.email FROM tasks t LEFT JOIN users u ON t.created_by = u.id WHERE t.id = ? `, [taskId], (err, row) => { if (err) reject(err); else resolve(row); }); }); // Получаем исполнителей ОТДЕЛЬНО const assignees = await new Promise((resolve, reject) => { db.all(` SELECT ta.user_id, u.name as user_name, u.login as user_login, u.email FROM task_assignments ta LEFT JOIN users u ON ta.user_id = u.id WHERE ta.task_id = ? `, [taskId], (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); // Собираем всех участников const participants = []; if (creator) { participants.push({ ...creator, role: 'creator', is_creator: true }); } if (assignees && assignees.length > 0) { assignees.forEach(assignee => { participants.push({ ...assignee, role: 'assignee', is_creator: false }); }); } // Получаем информацию об авторе действия const author = await new Promise((resolve, reject) => { db.get("SELECT name, login FROM users WHERE id = ?", [authorId], (err, row) => { if (err) reject(err); else resolve(row); }); }); const authorName = author ? author.name : 'Система'; const authorLogin = author ? author.login : 'system'; // Логируем в PostgreSQL const postgresLogIds = await logNotificationToPostgres({ type, taskId, taskTitle, taskDescription, authorId, authorName, authorLogin, participants, comment, status, userName }); let subject, content; switch (type) { case 'created': subject = `Новая задача: ${taskTitle}`; content = `Создана новая задача:\n\n` + `📋 ${taskTitle}\n` + `📝 ${taskDescription || 'Без описания'}\n` + `👤 Автор: ${authorName}\n\n` + `Для просмотра перейдите в систему управления задачами.`; break; case 'updated': subject = `Обновлена задача: ${taskTitle}`; content = `Задача была обновлена:\n\n` + `📋 ${taskTitle}\n` + `📝 ${taskDescription || 'Без описания'}\n` + `👤 Изменено: ${authorName}\n\n` + `Для просмотра изменений перейдите в систему управления задачами.`; break; case 'rework': subject = `Задача возвращена на доработку: ${taskTitle}`; content = `Задача возвращена на доработку:\n\n` + `📋 ${taskTitle}\n` + `📝 Комментарий: ${comment}\n` + `👤 Автор замечания: ${authorName}\n\n` + `Пожалуйста, исправьте замечания и обновите статус задачи.`; break; case 'closed': subject = `Задача закрыта: ${taskTitle}`; content = `Задача была закрыта:\n\n` + `📋 ${taskTitle}\n` + `👤 Закрыта: ${authorName}\n\n` + `Задача завершена и перемещена в архив.`; break; case 'status_changed': const statusText = getStatusText(status); subject = `Изменен статус задачи: ${taskTitle}`; content = `Статус задачи изменен:\n\n` + `📋 ${taskTitle}\n` + `🔄 Новый статус: ${statusText}\n` + `👤 Изменил: ${userName || authorName}\n\n` + `Для просмотра перейдите в систему управления задачами.`; break; default: console.log(`⚠️ Неизвестный тип уведомления: ${type}`); return; } // Фильтруем получателей: исключаем автора действия const recipientIds = participants .filter(p => { const shouldExclude = p.user_id === authorId; if (shouldExclude) { console.log(` ✋ Исключаем автора действия: ${p.user_name} (ID: ${p.user_id})`); } return !shouldExclude; }) .map(p => p.user_id); if (recipientIds.length === 0) { console.log('❌ Нет получателей для уведомления (все участники - автор изменения)'); // Обновляем статус в PostgreSQL await updatePostgresLogStatus(postgresLogIds, 'no_recipients', 'Нет получателей после фильтрации'); return; } const authHeader = encodeBasicAuth( process.env.NOTIFICATION_SERVICE_LOGIN, process.env.NOTIFICATION_SERVICE_PASSWORD ); const FormData = require('form-data'); const formData = new FormData(); formData.append('subject', subject); formData.append('content', content); formData.append('recipients', JSON.stringify(recipientIds)); formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk'])); console.log(`🚀 Отправляем запрос на сервис уведомлений...`); try { const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, { method: 'POST', headers: { 'Authorization': `Basic ${authHeader}` }, body: formData }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); console.log(`✅ Уведомления успешно отправлены для задачи ${taskId}`); // Обновляем статус в PostgreSQL await updatePostgresLogStatus(postgresLogIds, 'sent', null, new Date().toISOString()); console.log(` Результат от сервиса:`, result); } catch (error) { console.error('❌ Ошибка отправки уведомлений:', error); // Обновляем статус в PostgreSQL await updatePostgresLogStatus(postgresLogIds, 'failed', error.message); console.error(' Детали ошибки:', { taskId, type, authorId, errorMessage: error.message, stack: error.stack }); } } catch (error) { console.error('❌ Общая ошибка при обработке уведомлений:', error); } } // Вспомогательные функции для работы с PostgreSQL async function logNotificationToPostgres(data) { try { const { type, taskId, taskTitle, taskDescription, authorId, authorName, authorLogin, participants = [], comment = '', status = '', userName = '', error = '' } = data; // Создаем сообщение let messageContent = ''; switch (type) { case 'created': messageContent = `Создана новая задача: ${taskTitle}`; break; case 'updated': messageContent = `Обновлена задача: ${taskTitle}`; break; case 'rework': messageContent = `Задача возвращена на доработку: ${taskTitle}. Комментарий: ${comment}`; break; case 'closed': messageContent = `Задача закрыта: ${taskTitle}`; break; case 'status_changed': messageContent = `Изменен статус задачи: ${taskTitle}. Новый статус: ${getStatusText(status)}`; break; } // Логируем для каждого получателя отдельно const recipientsToNotify = participants.filter(p => p.user_id !== authorId); const logIds = []; for (const recipient of recipientsToNotify) { const logId = await postgresLogger.logNotification({ taskId, taskTitle, taskDescription, notificationType: type, authorId, authorName, authorLogin, recipientId: recipient.user_id, recipientName: recipient.user_name, recipientLogin: recipient.user_login, messageContent: `${messageContent}\n\nЗадача: ${taskTitle}\nОписание: ${taskDescription || 'Без описания'}\nАвтор: ${authorName}`, messageSubject: getNotificationSubject(type, taskTitle), deliveryMethods: ['email', 'telegram', 'vk'], comments: error ? `Ошибка: ${error}` : comment }); if (logId) { logIds.push(logId); } } return logIds; } catch (error) { console.error('❌ Ошибка логирования в PostgreSQL:', error); return []; } } async function updatePostgresLogStatus(logIds, status, errorMessage = null, sentAt = null) { if (!logIds || logIds.length === 0) return; for (const logId of logIds) { await postgresLogger.updateNotificationStatus(logId, status, errorMessage, sentAt); } } function getNotificationSubject(type, taskTitle) { switch (type) { case 'created': return `Новая задача: ${taskTitle}`; case 'updated': return `Обновлена задача: ${taskTitle}`; case 'rework': return `Задача возвращена на доработку: ${taskTitle}`; case 'closed': return `Задача закрыта: ${taskTitle}`; case 'status_changed': return `Изменен статус задачи: ${taskTitle}`; default: return `Уведомление по задаче: ${taskTitle}`; } } function getStatusText(status) { const statusMap = { 'assigned': 'Назначена', 'in_progress': 'В работе', 'completed': 'Завершена', 'overdue': 'Просрочена', 'rework': 'На доработке' }; return statusMap[status] || status; } app.post('/api/login', async (req, res) => { const { login, password } = req.body; if (!login || !password) { return res.status(400).json({ error: 'Логин и пароль обязательны' }); } try { const user = await authService.authenticate(login, password); if (user) { const sessionUser = { id: user.id, login: user.login, name: user.name, email: user.email, role: user.role, auth_type: user.auth_type, groups: user.groups ? (typeof user.groups === 'string' ? JSON.parse(user.groups) : user.groups) : [] }; req.session.user = sessionUser; req.session.save((err) => { if (err) { console.error('Ошибка сохранения сессии:', err); return res.status(500).json({ error: 'Ошибка сохранения сессии' }); } console.log(`Успешная авторизация: ${user.name} (${user.login}) через ${user.auth_type}`); if (user.groups) { console.log(`Группы пользователя: ${user.groups}`); } res.json({ success: true, user: sessionUser }); }); } else { console.log(`Неудачная попытка входа: ${login}`); res.status(401).json({ error: 'Неверный логин или пароль' }); } } catch (error) { console.error('Ошибка аутентификации:', error); res.status(500).json({ error: 'Ошибка сервера при авторизации' }); } }); app.post('/api/logout', (req, res) => { req.session.destroy((err) => { if (err) { console.error('Ошибка при выходе:', err); return res.status(500).json({ error: 'Ошибка при выходе' }); } res.json({ success: true }); }); }); app.get('/api/user', (req, res) => { if (req.session.user) { if (req.session.user.auth_type === 'ldap') { db.get("SELECT groups FROM users WHERE id = ?", [req.session.user.id], (err, user) => { if (err || !user) { req.session.destroy(); return res.status(401).json({ error: 'Пользователь не найден' }); } let groups = []; try { groups = JSON.parse(user.groups || '[]'); } catch (e) { groups = []; } const allowedGroups = process.env.ALLOWED_GROUPS ? process.env.ALLOWED_GROUPS.split(',').map(g => g.trim()) : []; const isAdmin = groups.some(group => allowedGroups.includes(group)); const actualRole = isAdmin ? 'admin' : 'teacher'; if (req.session.user.role !== actualRole) { console.log(`Обновлена роль пользователя ${req.session.user.login} с ${req.session.user.role} на ${actualRole}`); db.run( "UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?", [actualRole, req.session.user.id] ); req.session.user.role = actualRole; } res.json({ user: req.session.user }); }); } else { res.json({ user: req.session.user }); } } else { res.status(401).json({ error: 'Не аутентифицирован' }); } }); app.get('/api/users', requireAuth, (req, res) => { const search = req.query.search || ''; let query = ` SELECT id, login, name, email, role, auth_type FROM users WHERE role IN ('admin', 'teacher') `; const params = []; if (search) { query += ` AND (login LIKE ? OR name LIKE ? OR email LIKE ?)`; const searchPattern = `%${search}%`; params.push(searchPattern, searchPattern, searchPattern); } query += " ORDER BY name"; db.all(query, params, (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); app.get('/api/tasks', requireAuth, (req, res) => { const userId = req.session.user.id; const showDeleted = req.session.user.role === 'admin' && req.query.showDeleted === 'true'; const search = req.query.search || ''; const statusFilter = req.query.status || 'active,in_progress,assigned,overdue,rework'; const creatorFilter = req.query.creator || ''; const assigneeFilter = req.query.assignee || ''; const deadlineFilter = req.query.deadline || ''; let query = ` SELECT DISTINCT t.*, u.name as creator_name, u.login as creator_login, ot.title as original_task_title, ou.name as original_creator_name, GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids, GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names FROM tasks t LEFT JOIN users u ON t.created_by = u.id LEFT JOIN tasks ot ON t.original_task_id = ot.id LEFT JOIN users ou ON ot.created_by = ou.id LEFT JOIN task_assignments ta ON t.id = ta.task_id LEFT JOIN users u2 ON ta.user_id = u2.id WHERE 1=1 `; const params = []; if (req.session.user.role !== 'admin') { query += ` AND (t.created_by = ? OR ta.user_id = ?)`; params.push(userId, userId); } if (!showDeleted) { query += " AND t.status = 'active'"; } if (statusFilter && statusFilter !== 'all') { const statuses = statusFilter.split(','); if (statuses.includes('closed')) { if (req.session.user.role !== 'admin') { query += ` AND (t.closed_at IS NOT NULL AND t.created_by = ?)`; params.push(userId); } else { query += ` AND t.closed_at IS NOT NULL`; } } else { query += ` AND t.closed_at IS NULL`; if (statuses.length > 0 && !statuses.includes('all')) { query += ` AND EXISTS ( SELECT 1 FROM task_assignments ta2 WHERE ta2.task_id = t.id AND ta2.status IN (${statuses.map(() => '?').join(',')}) )`; statuses.forEach(status => params.push(status)); } } } else { if (req.session.user.role !== 'admin') { query += ` AND (t.closed_at IS NULL OR t.created_by = ?)`; params.push(userId); } } if (creatorFilter) { query += ` AND t.created_by = ?`; params.push(creatorFilter); } if (assigneeFilter) { query += ` AND ta.user_id = ?`; params.push(assigneeFilter); } if (deadlineFilter) { const now = new Date(); let hours = 48; if (deadlineFilter === '24h') hours = 24; const deadlineTime = new Date(now.getTime() + hours * 60 * 60 * 1000); const deadlineISO = deadlineTime.toISOString(); const nowISO = now.toISOString(); query += ` AND ta.due_date IS NOT NULL AND ta.due_date > ? AND ta.due_date <= ? AND ta.status NOT IN ('completed', 'overdue')`; params.push(nowISO, deadlineISO); } if (search) { query += ` AND (t.title LIKE ? OR t.description LIKE ?)`; const searchPattern = `%${search}%`; params.push(searchPattern, searchPattern); } query += " GROUP BY t.id ORDER BY t.created_at DESC"; db.all(query, params, (err, tasks) => { if (err) { res.status(500).json({ error: err.message }); return; } const taskPromises = tasks.map(task => { return new Promise((resolve) => { db.all(` SELECT ta.*, u.name as user_name, u.login as user_login FROM task_assignments ta LEFT JOIN users u ON ta.user_id = u.id WHERE ta.task_id = ? `, [task.id], (err, assignments) => { if (err) { task.assignments = []; resolve(task); return; } assignments.forEach(assignment => { if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') { assignment.status = 'overdue'; } }); task.assignments = assignments || []; resolve(task); }); }); }); Promise.all(taskPromises).then(completedTasks => { res.json(completedTasks); }); }); }); app.get('/api/tasks/no-date', requireAuth, (req, res) => { const userId = req.session.user.id; const query = ` SELECT DISTINCT t.*, u.name as creator_name, u.login as creator_login, ot.title as original_task_title, ou.name as original_creator_name, GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids, GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names FROM tasks t LEFT JOIN users u ON t.created_by = u.id LEFT JOIN tasks ot ON t.original_task_id = ot.id LEFT JOIN users ou ON ot.created_by = ou.id LEFT JOIN task_assignments ta ON t.id = ta.task_id LEFT JOIN users u2 ON ta.user_id = u2.id WHERE t.status = 'active' AND t.closed_at IS NULL AND (t.due_date IS NULL OR t.due_date = '') AND (ta.due_date IS NULL OR ta.due_date = '') `; const params = []; if (req.session.user.role !== 'admin') { query += ` AND (t.created_by = ? OR ta.user_id = ?)`; params.push(userId, userId); } query += " GROUP BY t.id ORDER BY t.created_at DESC"; db.all(query, params, (err, tasks) => { if (err) { res.status(500).json({ error: err.message }); return; } const taskPromises = tasks.map(task => { return new Promise((resolve) => { db.all(` SELECT ta.*, u.name as user_name, u.login as user_login FROM task_assignments ta LEFT JOIN users u ON ta.user_id = u.id WHERE ta.task_id = ? `, [task.id], (err, assignments) => { if (err) { task.assignments = []; resolve(task); return; } task.assignments = assignments || []; resolve(task); }); }); }); Promise.all(taskPromises).then(completedTasks => { res.json(completedTasks); }); }); }); 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; if (!title) { return res.status(400).json({ error: 'Название задачи обязательно' }); } if (!dueDate) { return res.status(400).json({ error: 'Дата и время выполнения обязательны' }); } db.serialize(() => { const startDate = new Date().toISOString(); db.run( "INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date) VALUES (?, ?, ?, ?, ?, ?)", [title, description, createdBy, originalTaskId || null, startDate, dueDate || null], 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 ? 'Копия задачи создана' : 'Задача успешно создана' }); } ); }); }); 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) VALUES (?, ?, ?, ?, ?, ?)", [newTitle, originalTask.description, createdBy, taskId, startDate, dueDate || null], 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: 'Копия задачи успешно создана' }); } ); }); }); }); }); app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res) => { const { taskId } = req.params; const { title, description, assignedUsers, dueDate } = req.body; const userId = req.session.user.id; if (!title) { return res.status(400).json({ error: 'Название задачи обязательно' }); } if (!dueDate) { return res.status(400).json({ error: 'Дата и время выполнения обязательны' }); } db.get("SELECT created_by FROM tasks WHERE id = ? AND status = 'active'", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } if (req.session.user.role !== 'admin' && task.created_by !== userId) { return res.status(403).json({ error: 'У вас нет прав для редактирования этой задачи' }); } db.serialize(() => { db.run( "UPDATE tasks SET title = ?, description = ?, due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [title, description, dueDate || null, taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } updateTaskMetadata(taskId, { title, description, due_date: dueDate }); logActivity(taskId, userId, 'TASK_UPDATED', `Задача обновлена: ${title}`); 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) { db.run("DELETE FROM task_assignments WHERE task_id = ?", [taskId], (err) => { if (err) { console.error('Ошибка удаления старых назначений:', err); } const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers]; userIds.forEach(userId => { const startDate = new Date().toISOString(); db.run( "INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)", [taskId, userId, startDate, dueDate || null] ); }); logActivity(taskId, userId, 'TASK_ASSIGNMENTS_UPDATED', `Назначения обновлены`); sendTaskNotifications('updated', taskId, title, description, userId); }); } else { sendTaskNotifications('updated', taskId, title, description, userId); } res.json({ success: true, message: 'Задача обновлена' }); } ); }); }); }); app.post('/api/tasks/:taskId/rework', requireAuth, (req, res) => { const { taskId } = req.params; const { comment } = req.body; const userId = req.session.user.id; db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } if (req.session.user.role !== 'admin' && task.created_by !== userId) { return res.status(403).json({ error: 'У вас нет прав для возврата задачи на доработку' }); } db.serialize(() => { db.run( "UPDATE tasks SET rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [comment || 'Требуется доработка', taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } db.run( "UPDATE task_assignments SET status = 'rework', rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ?", [comment || 'Требуется доработка', taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } logActivity(taskId, userId, 'TASK_SENT_FOR_REWORK', `Задача возвращена на доработку: ${comment}`); db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => { if (!err && taskData) { sendTaskNotifications('rework', taskId, taskData.title, taskData.description, userId, comment); } }); res.json({ success: true, message: 'Задача возвращена на доработку' }); } ); } ); }); }); }); app.post('/api/tasks/:taskId/close', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } if (req.session.user.role !== 'admin' && task.created_by !== userId) { return res.status(403).json({ error: 'У вас нет прав для закрытия этой задачи' }); } db.run( "UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, closed_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [userId, taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } logActivity(taskId, userId, 'TASK_CLOSED', `Задача закрыта`); db.get("SELECT title FROM tasks WHERE id = ?", [taskId], (err, taskData) => { if (!err && taskData) { sendTaskNotifications('closed', taskId, taskData.title, '', userId); } }); res.json({ success: true, message: 'Задача закрыта' }); } ); }); }); app.post('/api/tasks/:taskId/reopen', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } if (req.session.user.role !== 'admin' && task.created_by !== userId) { return res.status(403).json({ error: 'У вас нет прав для открытия этой задачи' }); } db.run( "UPDATE tasks SET closed_at = NULL, closed_by = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } logActivity(taskId, userId, 'TASK_REOPENED', `Задача открыта`); res.json({ success: true, message: 'Задача открыта' }); } ); }); }); app.put('/api/tasks/:taskId/assignment/:userId', requireAuth, (req, res) => { const { taskId, userId } = req.params; const { dueDate } = req.body; const currentUserId = req.session.user.id; if (!dueDate) { return res.status(400).json({ error: 'Дата и время выполнения обязательны' }); } db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } if (req.session.user.role !== 'admin' && task.created_by !== currentUserId) { return res.status(403).json({ error: 'У вас нет прав для редактирования сроки' }); } db.run( "UPDATE task_assignments SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?", [dueDate || null, taskId, userId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { return res.status(404).json({ error: 'Назначение не найдено' }); } logActivity(taskId, currentUserId, 'ASSIGNMENT_UPDATED', `Обновлены сроки для пользователя ${userId}`); db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => { if (!err && taskData) { sendTaskNotifications('updated', taskId, taskData.title, taskData.description, currentUserId); } }); res.json({ success: true, message: 'Сроки обновлены' }); } ); }); }); app.delete('/api/tasks/:taskId', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; db.get("SELECT created_by FROM tasks WHERE id = ? AND status = 'active'", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } if (req.session.user.role !== 'admin' && task.created_by !== userId) { return res.status(403).json({ error: 'У вас нет прав для удаления этой задачи' }); } db.run( "UPDATE tasks SET status = 'deleted', deleted_at = CURRENT_TIMESTAMP, deleted_by = ? WHERE id = ?", [userId, taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } updateTaskMetadata(taskId, { status: 'deleted', deleted_at: new Date().toISOString(), deleted_by: userId }); logActivity(taskId, userId, 'TASK_DELETED', `Задача помечена как удаленная`); res.json({ success: true, message: 'Задача удалена' }); } ); }); }); app.post('/api/tasks/:taskId/restore', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } db.run( "UPDATE tasks SET status = 'active', deleted_at = NULL, deleted_by = NULL WHERE id = ?", [taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { return res.status(404).json({ error: 'Задача не найдена' }); } updateTaskMetadata(taskId, { status: 'active', deleted_at: null, deleted_by: null }); logActivity(taskId, userId, 'TASK_RESTORED', `Задача восстановлена`); res.json({ success: true, message: 'Задача восстановлена' }); } ); }); app.put('/api/tasks/:taskId/status', requireAuth, (req, res) => { const { taskId } = req.params; const { userId: targetUserId, status } = req.body; const currentUserId = req.session.user.id; if (parseInt(targetUserId) !== currentUserId) { return res.status(403).json({ error: 'Недостаточно прав' }); } if (!targetUserId || !status) { return res.status(400).json({ error: 'userId и status обязательны' }); } db.get("SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?", [taskId, currentUserId], (err, assignment) => { if (err || !assignment) { return res.status(403).json({ error: 'Вы не назначены на эту задачу' }); } db.get(` SELECT t.title, t.description, u.name as user_name FROM tasks t LEFT JOIN users u ON u.id = ? WHERE t.id = ? `, [currentUserId, taskId], (err, taskData) => { if (err) { console.error('Ошибка получения данных задачи:', err); } const finalStatus = status === 'completed' ? 'completed' : status; db.run( "UPDATE task_assignments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?", [finalStatus, taskId, targetUserId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Назначение не найдено' }); return; } logActivity(taskId, targetUserId, 'STATUS_CHANGED', `Статус изменен на: ${finalStatus}`); if (taskData) { sendTaskNotifications( 'status_changed', taskId, taskData.title, taskData.description, currentUserId, '', finalStatus, taskData.user_name || req.session.user.name ); } res.json({ success: true, message: 'Статус обновлен' }); } ); }); }); }); app.post('/api/tasks/:taskId/files', requireAuth, upload.array('files', 15), (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; if (!req.files || req.files.length === 0) { return res.status(400).json({ error: 'Нет файлов для загрузки' }); } checkTaskAccess(userId, taskId, (err, hasAccess) => { if (err || !hasAccess) { return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' }); } 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}`); }); res.json({ success: true, message: 'Файлы успешно загружены' }); }); }); app.get('/api/tasks/:taskId/files', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; checkTaskAccess(userId, taskId, (err, hasAccess) => { if (err || !hasAccess) { return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' }); } db.all(` SELECT tf.*, u.name as user_name, u.login as user_login FROM task_files tf LEFT JOIN users u ON tf.user_id = u.id WHERE tf.task_id = ? ORDER BY tf.uploaded_at DESC `, [taskId], (err, files) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(files); }); }); }); app.get('/api/files/:fileId/download', requireAuth, (req, res) => { const { fileId } = req.params; const userId = req.session.user.id; db.get("SELECT tf.*, t.id as task_id FROM task_files tf JOIN tasks t ON tf.task_id = t.id WHERE tf.id = ?", [fileId], (err, file) => { if (err || !file) { return res.status(404).json({ error: 'Файл не найдена' }); } checkTaskAccess(userId, file.task_id, (err, hasAccess) => { if (err || !hasAccess) { return res.status(404).json({ error: 'Файл не найден или у вас нет прав доступа' }); } if (!fs.existsSync(file.file_path)) { return res.status(404).json({ error: 'Файл не найден на сервере' }); } // Исправляем кодировку имени файла let decodedFileName = file.original_name; // Пробуем декодировать если это UTF-8 в Latin-1 (для старых записей) try { if (/^[A-Za-z0-9\.\-_]+$/.test(decodedFileName)) { // Если имя содержит только латинские символы, оставляем как есть } else if (decodedFileName.includes('Ð') || decodedFileName.includes('Ñ')) { // Исправляем неправильно декодированную кириллицу decodedFileName = Buffer.from(decodedFileName, 'binary').toString('utf8'); } } catch (e) { console.error('Ошибка декодирования имени файла:', e); } // Кодируем имя файла для безопасной передачи const encodedFileName = encodeURIComponent(decodedFileName); // Устанавливаем заголовки для скачивания res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`); res.setHeader('Content-Type', 'application/octet-stream'); // Отправляем файл res.sendFile(file.file_path); }); }); }); app.get('/api/activity-logs', requireAuth, (req, res) => { const userId = req.session.user.id; let query = ` SELECT al.*, u.name as user_name, t.title as task_title FROM activity_logs al LEFT JOIN users u ON al.user_id = u.id LEFT JOIN tasks t ON al.task_id = t.id WHERE 1=1 `; if (req.session.user.role !== 'admin') { query += ` AND (t.created_by = ${userId} OR al.task_id IN ( SELECT task_id FROM task_assignments WHERE user_id = ${userId} ))`; } query += " ORDER BY al.created_at DESC LIMIT 100"; db.all(query, (err, logs) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(logs); }); }); app.get('/admin', (req, res) => { if (!req.session.user || req.session.user.role !== 'admin') { return res.status(403).send('Доступ запрещен'); } res.sendFile(path.join(__dirname, 'public/admin.html')); }); // API для получения логов уведомлений app.get('/api/notification-logs', requireAuth, async (req, res) => { try { const { taskId, status, startDate, endDate, limit = 50, offset = 0 } = req.query; const logs = await postgresLogger.getNotifications({ taskId: taskId ? parseInt(taskId) : null, userId: req.session.user.id, status, startDate, endDate, limit: parseInt(limit), offset: parseInt(offset) }); res.json(logs); } catch (error) { console.error('Ошибка получения логов уведомлений:', error); res.status(500).json({ error: 'Ошибка получения логов' }); } }); // API для получения статистики app.get('/api/notification-stats', requireAuth, async (req, res) => { try { const { period = 'day' } = req.query; if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } const stats = await postgresLogger.getStatistics(period); res.json(stats); } catch (error) { console.error('Ошибка получения статистики:', error); res.status(500).json({ error: 'Ошибка получения статистики' }); } }); // API для проверки состояния PostgreSQL app.get('/api/postgres-health', requireAuth, async (req, res) => { try { if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } const health = await postgresLogger.healthCheck(); res.json(health); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(PORT, () => { console.log(`CRM сервер запущен на порту ${PORT}`); console.log(`Откройте http://localhost:${PORT} в браузере`); console.log('Данные хранятся в папке:', path.join(__dirname, 'data')); console.log('Тестовые пользователи:'); console.log('- Логин: director, Пароль: director123 (Администратор)'); console.log('- Логин: zavuch, Пароль: zavuch123'); console.log('- Логин: teacher, Пароль: teacher123'); console.log('LDAP авторизация доступна для пользователей школы'); console.log(`Разрешенные группы: ${process.env.ALLOWED_GROUPS}`); console.log('Система уведомлений активна'); setInterval(checkOverdueTasks, 60000); setInterval(checkUpcomingDeadlines, 60000); });