// server.js const express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const session = require('express-session'); require('dotenv').config(); // Импортируем модули const { initializeDatabase, getDb, isInitialized } = require('./database'); const authService = require('./auth'); const postgresLogger = require('./postgres'); const { sendTaskNotifications, checkUpcomingDeadlines, getStatusText } = require('./notifications'); const { setupUploadMiddleware } = require('./upload-middleware'); const { setupTaskEndpoints } = require('./task-endpoints'); const app = express(); const PORT = process.env.PORT || 3000; // Глобальные переменные let db = null; let serverReady = false; let adminRouter = null; let upload = null; // Инициализируем multer сразу с настройками по умолчанию const uploadsDir = path.join(__dirname, 'data', 'uploads'); const createDirIfNotExists = (dirPath) => { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } }; createDirIfNotExists(uploadsDir); // Создаем базовую конфигурацию multer const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, uploadsDir); }, filename: function (req, file, cb) { // Используем оригинальное имя файла с timestamp для уникальности const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const ext = path.extname(file.originalname); const name = path.basename(file.originalname, ext); cb(null, name + '-' + uniqueSuffix + ext); } }); // Создаем экземпляр multer сразу upload = multer({ storage: storage, limits: { fileSize: 50 * 1024 * 1024 // 50MB } }); // Middleware 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 } })); // Middleware для проверки готовности сервера app.use((req, res, next) => { if (!serverReady && req.path !== '/health' && req.path !== '/api/health') { return res.status(503).json({ error: 'Сервер запускается...', status: 'initializing' }); } next(); }); // Health check endpoints app.get('/health', (req, res) => { res.json({ status: serverReady ? 'ready' : 'initializing', database: isInitialized() ? 'connected' : 'connecting', auth: authService.isReady() ? 'ready' : 'waiting', timestamp: new Date().toISOString() }); }); app.get('/api/health', (req, res) => { res.json({ status: serverReady ? 'ready' : 'initializing', database: isInitialized() ? 'connected' : 'connecting', auth: authService.isReady() ? 'ready' : 'waiting', timestamp: new Date().toISOString() }); }); // Вспомогательные функции function checkIfOverdue(dueDate, status) { if (!dueDate || status === 'completed') return false; const now = new Date(); const due = new Date(dueDate); return due < now; } function checkOverdueTasks() { if (!db) { console.error('❌ База данных не доступна для проверки просроченных задач'); return; } 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] ); const { logActivity } = require('./database'); if (logActivity) { logActivity(assignment.task_id, assignment.user_id, 'STATUS_CHANGED', 'Задача просрочена'); } }); }); } // Middleware для аутентификации const requireAuth = (req, res, next) => { if (!req.session.user) { return res.status(401).json({ error: 'Требуется аутентификация' }); } next(); }; // API для аутентификации 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.message); res.status(500).json({ error: 'Ошибка сервера при авторизации', details: error.message }); } }); 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') { if (!db) { return res.status(503).json({ error: 'База данных не готова' }); } 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: 'Не аутентифицирован' }); } }); // Middleware для проверки наличия БД в API endpoints app.use((req, res, next) => { if (!db && req.path.startsWith('/api/') && req.path !== '/api/health' && req.path !== '/api/login') { return res.status(503).json({ error: 'База данных не готова' }); } next(); }); // API для пользователей 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); }); }); // API для файлов app.get('/api/tasks/:taskId/files', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; const { checkTaskAccess } = require('./database'); 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: 'Файл не найдена' }); } const { checkTaskAccess } = require('./database'); 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); }); }); }); // API для логов активности 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); }); }); // API для логов уведомлений из PostgreSQL app.get('/api/notification-logs', requireAuth, async (req, res) => { try { const { taskId, status, startDate, endDate, limit = 50, offset = 0 } = req.query; // Получаем все логи для текущего пользователя const query = ` SELECT * FROM sms_logs WHERE creator_id = ? OR assignee_id = ? ${taskId ? 'AND task_id = ?' : ''} ${status ? 'AND status = ?' : ''} ${startDate ? 'AND created_at >= ?' : ''} ${endDate ? 'AND created_at <= ?' : ''} ORDER BY created_at DESC LIMIT ? OFFSET ? `; const params = [req.session.user.id, req.session.user.id]; if (taskId) params.push(taskId); if (status) params.push(status); if (startDate) params.push(startDate); if (endDate) params.push(endDate); params.push(parseInt(limit), parseInt(offset)); const logs = await new Promise((resolve, reject) => { db.all(query, params, (err, rows) => { if (err) reject(err); else resolve(rows); }); }); const countQuery = ` SELECT COUNT(*) as total FROM sms_logs WHERE creator_id = ? OR assignee_id = ? ${taskId ? 'AND task_id = ?' : ''} ${status ? 'AND status = ?' : ''} ${startDate ? 'AND created_at >= ?' : ''} ${endDate ? 'AND created_at <= ?' : ''} `; const countParams = [req.session.user.id, req.session.user.id]; if (taskId) countParams.push(taskId); if (status) countParams.push(status); if (startDate) countParams.push(startDate); if (endDate) countParams.push(endDate); const countResult = await new Promise((resolve, reject) => { db.get(countQuery, countParams, (err, row) => { if (err) reject(err); else resolve(row); }); }); res.json({ logs: logs || [], total: countResult?.total || 0, limit: parseInt(limit), offset: parseInt(offset) }); } 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: 'Недостаточно прав' }); } let dateFilter = ''; switch (period) { case 'day': dateFilter = "created_at >= CURRENT_DATE"; break; case 'week': dateFilter = "created_at >= DATE('now', '-7 days')"; break; case 'month': dateFilter = "created_at >= DATE('now', '-30 days')"; break; case 'year': dateFilter = "created_at >= DATE('now', '-365 days')"; break; default: dateFilter = "created_at >= CURRENT_DATE"; } const statsQuery = ` SELECT status, COUNT(*) as count, COUNT(CASE WHEN sent_at IS NOT NULL THEN 1 END) as sent_count, COUNT(CASE WHEN error_message IS NOT NULL THEN 1 END) as error_count FROM sms_logs WHERE ${dateFilter} GROUP BY status `; const totalQuery = ` SELECT COUNT(*) as total, COUNT(CASE WHEN sent_at IS NOT NULL THEN 1 END) as total_sent, COUNT(CASE WHEN error_message IS NOT NULL THEN 1 END) as total_errors FROM sms_logs WHERE ${dateFilter} `; const [stats, total] = await Promise.all([ new Promise((resolve, reject) => { db.all(statsQuery, [], (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }), new Promise((resolve, reject) => { db.get(totalQuery, [], (err, row) => { if (err) reject(err); else resolve(row || { total: 0, total_sent: 0, total_errors: 0 }); }); }) ]); res.json({ period: period, stats: stats, total: total.total || 0, totalSent: total.total_sent || 0, totalErrors: total.total_errors || 0, timestamp: new Date().toISOString() }); } 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.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')); }); // Страница профилей пользователей (только для админов) app.get('/admin/profiles', (req, res) => { if (!req.session.user || req.session.user.role !== 'admin') { return res.status(403).send('Доступ запрещен'); } res.sendFile(path.join(__dirname, 'public/admin-profiles.html')); }); // Админ панель для документов app.get('/admin-doc', (req, res) => { if (!req.session.user || req.session.user.role !== 'admin') { return res.status(403).send('Доступ запрещен'); } res.sendFile(path.join(__dirname, 'public/admin-doc.html')); }); // Страница согласования документов app.get('/doc', (req, res) => { if (!req.session.user) { return res.redirect('/'); } res.sendFile(path.join(__dirname, 'public/doc.html')); }); // API для типов документов app.get('/api/document-types', requireAuth, (req, res) => { db.all("SELECT * FROM simple_document_types ORDER BY name", [], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); }); }); // API для документов (ИСПРАВЛЕНО: upload определен в начале файла) app.post('/api/documents', requireAuth, upload.array('files', 15), async (req, res) => { try { console.log('📝 Начало создания документа...'); const userId = req.session.user.id; const { title, description, dueDate, documentTypeId, documentNumber, documentDate, pagesCount, urgencyLevel, comment } = req.body; console.log('📋 Данные документа:', { title, userId, documentTypeId, documentNumber }); // Валидация обязательных полей - только название if (!title || title.trim() === '') { return res.status(400).json({ error: 'Название документа обязательно' }); } // Находим группу "Секретарь" db.get(` SELECT u.id FROM users u JOIN user_group_memberships ugm ON u.id = ugm.user_id JOIN user_groups g ON ugm.group_id = g.id WHERE g.name = 'Секретарь' LIMIT 1 `, async (err, secretary) => { if (err) { console.error('❌ Ошибка поиска секретаря:', err); return res.status(500).json({ error: 'Ошибка поиска секретаря' }); } if (!secretary) { console.warn('⚠️ Секретарь не найден в группе "Секретарь"'); return res.status(400).json({ error: 'Не найден секретарь для согласования документов. Пожалуйста, добавьте пользователя в группу "Секретарь".' }); } console.log('✅ Найден секретарь из группы:', secretary.id); // Создаем задачу 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) { console.error('❌ Ошибка создания задачи:', err); return res.status(500).json({ error: 'Ошибка создания задачи' }); } const taskId = this.lastID; console.log('✅ Задача создана, ID:', taskId); // Создаем запись документа в таблице simple_documents // Тип документа не обязателен - может быть NULL db.run(` INSERT INTO simple_documents ( task_id, document_type_id, document_number, document_date, pages_count, urgency_level, comment ) VALUES (?, ?, ?, ?, ?, ?, ?) `, [ taskId, documentTypeId || null, // Может быть 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; console.log('✅ Запись документа создана, ID:', documentId); // Назначаем задачу секретарю db.run(` INSERT INTO task_assignments (task_id, user_id, status, created_at) VALUES (?, ?, 'assigned', datetime('now')) `, [taskId, secretary.id], function(err) { if (err) { console.error('❌ Ошибка назначения задачи секретарю:', err); // Удаляем задачу и документ db.run("DELETE FROM simple_documents WHERE task_id = ?", [taskId]); db.run("DELETE FROM tasks WHERE id = ?", [taskId]); return res.status(500).json({ error: 'Ошибка назначения задачи секретарю' }); } console.log('✅ Задача назначена секретарю'); // Загружаем файлы если есть if (req.files && req.files.length > 0) { console.log('📁 Файлов для загрузки:', req.files.length); 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 { console.log('✅ Файл сохранен:', originalName); resolve(); } }); }); }); Promise.all(uploadPromises) .then(() => { console.log('✅ Все файлы загружены'); // Логируем действие const { logActivity } = require('./database'); logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ для согласования: ${title}`); res.json({ success: true, taskId: taskId, documentId: documentId, message: 'Документ успешно создан и отправлен на согласование' }); }) .catch(error => { console.error('❌ Ошибка загрузки файлов:', error); // Все равно возвращаем успех, так как задача и документ созданы const { logActivity } = require('./database'); logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ для согласования: ${title}`); res.json({ success: true, taskId: taskId, documentId: documentId, message: 'Документ создан, но были проблемы с загрузкой файлов' }); }); } else { console.log('📁 Файлы не прикреплены'); // Логируем действие const { logActivity } = require('./database'); logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ для согласования: ${title}`); res.json({ success: true, taskId: taskId, documentId: documentId, 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; console.log('📄 Запрос документов пользователя ID:', userId); db.all(` SELECT t.id, t.title, t.description, t.due_date, t.created_at, t.status, t.closed_at, sd.id as document_id, sd.document_type_id, sdt.name as document_type_name, sd.document_number, sd.document_date, sd.pages_count, sd.urgency_level, sd.comment, sd.refusal_reason, u.name as creator_name, ta.status as assignment_status, ta.user_id as assignee_id, au.name as assignee_name FROM tasks t LEFT JOIN simple_documents sd ON t.id = sd.task_id LEFT JOIN simple_document_types sdt ON sd.document_type_id = sdt.id LEFT JOIN users u ON t.created_by = u.id LEFT JOIN task_assignments ta ON t.id = ta.task_id LEFT JOIN users au ON ta.user_id = au.id WHERE t.created_by = ? AND t.title LIKE 'Документ:%' ORDER BY t.created_at DESC `, [userId], async (err, tasks) => { if (err) { console.error('❌ Ошибка получения документов:', err); return res.status(500).json({ error: 'Ошибка получения документов', details: err.message }); } console.log('✅ Найдено задач:', tasks.length); // Загружаем файлы для каждой задачи 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) { console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error); task.files = []; } return task; })); res.json(tasksWithFiles); }); }); // Получение документов для секретаря app.get('/api/documents/secretary', requireAuth, (req, res) => { const userId = req.session.user.id; console.log('📄 Запрос документов для секретаря ID:', userId); // Проверяем, что пользователь секретарь db.get(` SELECT 1 FROM users u JOIN user_group_memberships ugm ON u.id = ugm.user_id JOIN user_groups g ON ugm.group_id = g.id WHERE u.id = ? AND g.name = 'Секретарь' `, [userId], (err, isSecretary) => { if (err || !isSecretary) { // Пробуем альтернативный способ проверки db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, user) => { if (err || !user || !user.groups || !user.groups.includes('Секретарь')) { console.log('⚠️ Пользователь не является секретарем:', userId); return res.status(403).json({ error: 'Недостаточно прав. Требуется роль секретаря.' }); } fetchDocuments(); }); } else { fetchDocuments(); } }); function fetchDocuments() { db.all(` SELECT t.id, t.title, t.description, t.due_date, t.created_at, ta.status as assignment_status, sd.id as document_id, sd.document_type_id, sdt.name as document_type_name, sd.document_number, sd.document_date, sd.pages_count, sd.urgency_level, sd.comment, sd.refusal_reason, u.name as creator_name FROM tasks t JOIN task_assignments ta ON t.id = ta.task_id LEFT JOIN simple_documents sd ON t.id = sd.task_id LEFT JOIN simple_document_types sdt ON sd.document_type_id = sdt.id LEFT 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 ORDER BY CASE sd.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) { console.error('❌ Ошибка получения документов для секретаря:', err); return res.status(500).json({ error: 'Ошибка получения документов', details: err.message }); } console.log('✅ Найдено задач для секретаря:', tasks.length); // Загружаем файлы для каждой задачи 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) { console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, 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 1 FROM users u JOIN user_group_memberships ugm ON u.id = ugm.user_id JOIN user_groups g ON ugm.group_id = g.id WHERE u.id = ? AND g.name = 'Секретарь' `, [userId], (err, isSecretary) => { if (err || !isSecretary) { db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, user) => { if (err || !user || !user.groups || !user.groups.includes('Секретарь')) { if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } } updateDocumentStatus(); }); } else { updateDocumentStatus(); } }); function updateDocumentStatus() { db.get("SELECT task_id FROM simple_documents WHERE id = ?", [documentId], (err, document) => { if (err || !document) { return res.status(404).json({ error: 'Документ не найден' }); } const taskId = document.task_id; // Обновляем статус в задании db.run("UPDATE task_assignments SET status = ? WHERE task_id = ? AND user_id = ?", [status, taskId, userId], function(err) { if (err) { return res.status(500).json({ error: err.message }); } // Обновляем причину отказа если есть if (refusalReason) { db.run("UPDATE simple_documents SET refusal_reason = ? WHERE id = ?", [refusalReason, documentId]); } // Логируем действие const { logActivity } = require('./database'); const actionMap = { 'approved': 'Документ согласован', 'completed': 'Документ согласован', 'received': 'Оригинал документа получен', 'signed': 'Документ подписан', 'refused': 'В согласовании отказано' }; const actionText = actionMap[status] || `Статус изменен на: ${status}`; logActivity(taskId, userId, 'STATUS_CHANGED', actionText); 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 simple_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.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', 'Документ отозван создателем'); res.json({ success: true }); }); }); }); }); // Получение пакета документов 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 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 isSecretary = req.session.user.groups && req.session.user.groups.includes('Секретарь'); if (!isCreator && !isSecretary) { return res.status(403).json({ error: 'Недостаточно прав' }); } // Здесь будет логика создания ZIP архива с документами // Пока возвращаем заглушку res.json({ success: true, message: 'Функция создания пакета документов будет реализована в следующей версии' }); }); }); // API для получения настроек уведомлений пользователя app.get('/api/user/settings', requireAuth, async (req, res) => { try { if (!req.session.user || !req.session.user.id) { return res.status(401).json({ error: 'Не аутентифицирован' }); } const userId = req.session.user.id; const { getDb } = require('./database'); const db = getDb(); db.get("SELECT email_notifications, notification_email, telegram_notifications, telegram_chat_id, vk_notifications, vk_user_id FROM user_settings WHERE user_id = ?", [userId], (err, settings) => { if (err) { console.error('❌ Ошибка получения настроек:', err); return res.status(500).json({ error: 'Ошибка получения настроек' }); } if (!settings) { // Возвращаем настройки по умолчанию res.json({ email_notifications: true, notification_email: req.session.user.email || '', telegram_notifications: false, telegram_chat_id: '', vk_notifications: false, vk_user_id: '' }); } else { // Преобразуем boolean из SQLite (0/1) в true/false const result = { email_notifications: !!settings.email_notifications, notification_email: settings.notification_email || '', telegram_notifications: !!settings.telegram_notifications, telegram_chat_id: settings.telegram_chat_id || '', vk_notifications: !!settings.vk_notifications, vk_user_id: settings.vk_user_id || '' }; res.json(result); } } ); } catch (error) { console.error('❌ Ошибка получения настроек:', error); res.status(500).json({ error: 'Ошибка получения настроек' }); } }); // API для сохранения настроек уведомлений app.post('/api/user/settings', requireAuth, async (req, res) => { try { if (!req.session.user || !req.session.user.id) { return res.status(401).json({ error: 'Не аутентифицирован' }); } const userId = req.session.user.id; const { email_notifications, notification_email, telegram_notifications, telegram_chat_id, vk_notifications, vk_user_id } = req.body; // Валидация if (email_notifications === undefined || telegram_notifications === undefined || vk_notifications === undefined) { return res.status(400).json({ error: 'Не все обязательные поля заполнены' }); } const { getDb } = require('./database'); const db = getDb(); // Проверяем, есть ли уже настройки для пользователя db.get("SELECT id FROM user_settings WHERE user_id = ?", [userId], (err, existing) => { if (err) { console.error('❌ Ошибка проверки настроек:', err); return res.status(500).json({ error: 'Ошибка сохранения настроек' }); } if (existing) { // Обновляем существующие настройки db.run( `UPDATE user_settings SET email_notifications = ?, notification_email = ?, telegram_notifications = ?, telegram_chat_id = ?, vk_notifications = ?, vk_user_id = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?`, [ email_notifications ? 1 : 0, notification_email || '', telegram_notifications ? 1 : 0, telegram_chat_id || '', vk_notifications ? 1 : 0, vk_user_id || '', userId ], function(updateErr) { if (updateErr) { console.error('❌ Ошибка обновления настроек:', updateErr); return res.status(500).json({ error: 'Ошибка сохранения настроек' }); } console.log(`✅ Настройки пользователя ${userId} обновлены`); res.json({ success: true }); } ); } else { // Создаем новые настройки db.run( `INSERT INTO user_settings (user_id, email_notifications, notification_email, telegram_notifications, telegram_chat_id, vk_notifications, vk_user_id) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ userId, email_notifications ? 1 : 0, notification_email || '', telegram_notifications ? 1 : 0, telegram_chat_id || '', vk_notifications ? 1 : 0, vk_user_id || '' ], function(insertErr) { if (insertErr) { console.error('❌ Ошибка создания настроек:', insertErr); return res.status(500).json({ error: 'Ошибка сохранения настроек' }); } console.log(`✅ Настройки пользователя ${userId} созданы`); res.json({ success: true }); } ); } }); } catch (error) { console.error('❌ Ошибка сохранения настроек:', error); res.status(500).json({ error: 'Ошибка сохранения настроек' }); } }); // API для проверки настроек app.get('/api/user/settings/check', requireAuth, (req, res) => { try { if (!req.session.user || !req.session.user.id) { return res.status(401).json({ error: 'Не аутентифицирован' }); } const userId = req.session.user.id; const { getDb } = require('./database'); const db = getDb(); db.get("SELECT COUNT(*) as count FROM user_settings WHERE user_id = ?", [userId], (err, result) => { if (err) { console.error('❌ Ошибка проверки таблицы:', err); return res.json({ table_exists: false, user_has_settings: false, error: err.message }); } res.json({ table_exists: true, user_has_settings: result.count > 0, user_id: userId }); }); } catch (error) { console.error('❌ Ошибка проверки настроек:', error); res.status(500).json({ error: 'Ошибка проверки настроек' }); } }); // API для проверки email уведомлений app.get('/api/email-health', requireAuth, async (req, res) => { try { if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } const emailNotifications = require('./email-notifications'); const health = { ready: emailNotifications.isReady(), email: process.env.YANDEX_EMAIL, host: process.env.YANDEX_SMTP_HOST, timestamp: new Date().toISOString() }; res.json(health); } catch (error) { res.status(500).json({ error: error.message }); } }); // API для групп пользователей app.get('/api/groups', requireAuth, (req, res) => { if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } db.all(` SELECT g.*, COUNT(ugm.user_id) as member_count FROM user_groups g LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id GROUP BY g.id ORDER BY g.name `, [], (err, groups) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(groups); }); }); // API для всех пользователей с группами app.get('/api/users/all', requireAuth, (req, res) => { if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } db.all(` SELECT u.id, u.login, u.name, u.email, u.role, u.auth_type, u.created_at, json_group_array( json_object( 'group_id', g.id, 'group_name', g.name, 'group_color', g.color ) ) as groups FROM users u LEFT JOIN user_group_memberships ugm ON u.id = ugm.user_id LEFT JOIN user_groups g ON ugm.group_id = g.id WHERE u.role IN ('admin', 'teacher') GROUP BY u.id ORDER BY u.name `, [], (err, users) => { if (err) { console.error('Ошибка получения пользователей:', err); res.status(500).json({ error: err.message }); return; } // Парсим JSON строку с группами const usersWithGroups = users.map(user => { try { user.groups = JSON.parse(user.groups).filter(g => g.group_id !== null); } catch (e) { user.groups = []; } return user; }); res.json(usersWithGroups); }); }); // API для добавления пользователя в группу app.post('/api/groups/:groupId/users/:userId', requireAuth, (req, res) => { if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } const { groupId, userId } = req.params; // Проверяем существование группы и пользователя db.serialize(() => { db.get("SELECT id FROM user_groups WHERE id = ?", [groupId], (err, group) => { if (err || !group) { return res.status(404).json({ error: 'Группа не найдена' }); } db.get("SELECT id FROM users WHERE id = ?", [userId], (err, user) => { if (err || !user) { return res.status(404).json({ error: 'Пользователь не найден' }); } // Добавляем пользователя в группу db.run(` INSERT OR IGNORE INTO user_group_memberships (user_id, group_id) VALUES (?, ?) `, [userId, groupId], function(err) { if (err) { console.error('Ошибка добавления в группу:', err); return res.status(500).json({ error: err.message }); } if (this.changes > 0) { // Обновляем группы пользователя в таблице users db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, userData) => { if (err) { return res.json({ success: true }); } let groups = []; try { groups = JSON.parse(userData.groups || '[]'); } catch (e) { groups = []; } // Получаем имя группы db.get("SELECT name FROM user_groups WHERE id = ?", [groupId], (err, groupData) => { if (groupData && !groups.includes(groupData.name)) { groups.push(groupData.name); db.run( "UPDATE users SET groups = ?, updated_at = datetime('now') WHERE id = ?", [JSON.stringify(groups), userId], (updateErr) => { if (updateErr) { console.error('Ошибка обновления групп пользователя:', updateErr); } res.json({ success: true }); } ); } else { res.json({ success: true }); } }); }); } else { res.json({ success: true, message: 'Пользователь уже в группе' }); } }); }); }); }); }); // API для удаления пользователя из группы app.delete('/api/groups/:groupId/users/:userId', requireAuth, (req, res) => { if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } const { groupId, userId } = req.params; // Удаляем пользователя из группы db.run(` DELETE FROM user_group_memberships WHERE user_id = ? AND group_id = ? `, [userId, groupId], function(err) { if (err) { console.error('Ошибка удаления из группы:', err); return res.status(500).json({ error: err.message }); } if (this.changes > 0) { // Обновляем группы пользователя в таблице users db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, userData) => { if (err) { return res.json({ success: true }); } let groups = []; try { groups = JSON.parse(userData.groups || '[]'); } catch (e) { groups = []; } // Получаем имя группы db.get("SELECT name FROM user_groups WHERE id = ?", [groupId], (err, groupData) => { if (groupData) { groups = groups.filter(group => group !== groupData.name); db.run( "UPDATE users SET groups = ?, updated_at = datetime('now') WHERE id = ?", [JSON.stringify(groups), userId], (updateErr) => { if (updateErr) { console.error('Ошибка обновления групп пользователя:', updateErr); } res.json({ success: true }); } ); } else { res.json({ success: true }); } }); }); } else { res.json({ success: true, message: 'Пользователь не был в группе' }); } }); }); // API для создания группы app.post('/api/groups', requireAuth, (req, res) => { if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } const { name, description, color, can_approve_documents } = req.body; if (!name) { return res.status(400).json({ error: 'Название группы обязательно' }); } db.run(` INSERT INTO user_groups (name, description, color, can_approve_documents) VALUES (?, ?, ?, ?) `, [name, description || '', color || '#3498db', can_approve_documents || false], function(err) { if (err) { console.error('Ошибка создания группы:', err); res.status(500).json({ error: err.message }); return; } res.json({ success: true, id: this.lastID }); }); }); // Инициализация сервера async function initializeServer() { console.log('🚀 Инициализация сервера...'); try { // 1. Инициализируем базу данных console.log('🔧 Инициализация базы данных...'); await initializeDatabase(); // 2. Получаем объект БД db = getDb(); console.log('✅ База данных готова'); const { initializeDocumentTypes } = require('./init-document-types'); initializeDocumentTypes(db); console.log('✅ Сервис document готов'); // 3. Настраиваем authService с БД authService.setDatabase(db); console.log('✅ Сервис аутентификации готов'); // 4. Настраиваем endpoint'ы для задач (upload уже настроен в начале файла) setupTaskEndpoints(app, db, upload); console.log('✅ Endpoint\'ы задач настроены'); // 5. Загружаем админ роутер динамически try { adminRouter = require('./admin-server'); console.log('Admin router loaded:', adminRouter); console.log('Type:', typeof adminRouter); if (adminRouter && typeof adminRouter === 'function') { app.use(adminRouter); console.log('✅ Админ роутер подключен'); } else { console.error('❌ Admin router is not a valid middleware function'); // Создаем заглушку, чтобы сервер работал const express = require('express'); const stubRouter = express.Router(); stubRouter.get('*', (req, res) => { res.status(501).json({ error: 'Admin router not available' }); }); app.use(stubRouter); console.log('⚠️ Используется заглушка для админ роутера'); } } catch (error) { console.error('❌ Ошибка загрузки админ роутера:', error.message); console.error('Stack:', error.stack); // Создаем заглушку, чтобы сервер не падал const express = require('express'); const stubRouter = express.Router(); stubRouter.get('*', (req, res) => { res.status(503).json({ error: 'Admin panel temporarily unavailable', message: error.message }); }); app.use(stubRouter); console.log('⚠️ Создана заглушка для админ роутера из-за ошибки'); } // 6. Помечаем сервер как готовый serverReady = true; console.log('✅ Сервер полностью инициализирован'); } catch (error) { console.error('❌ Ошибка инициализации сервера:', error.message); console.error(error.stack); process.exit(1); } } // Запускаем инициализацию и сервер initializeServer().then(() => { // Запускаем сервер 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); }); }).catch(error => { console.error('❌ Не удалось запустить сервер:', error); process.exit(1); }); // Экспортируем приложение для тестирования module.exports = app;