// 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; // 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() }); }); // Вспомогательные функции 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() { 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')); }); // Инициализация сервера async function initializeServer() { console.log('🚀 Инициализация сервера...'); try { // 1. Инициализируем базу данных console.log('🔧 Инициализация базы данных...'); await initializeDatabase(); // 2. Получаем объект БД db = getDb(); console.log('✅ База данных готова'); // 3. Настраиваем authService с БД authService.setDatabase(db); console.log('✅ Сервис аутентификации готов'); // 4. Настраиваем загрузку файлов upload = setupUploadMiddleware(); console.log('✅ Middleware загрузки файлов настроен'); // 5. Настраиваем endpoint'ы для задач setupTaskEndpoints(app, db, upload); console.log('✅ Endpoint\'ы задач настроены'); // 6. Загружаем админ роутер динамически 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('⚠️ Создана заглушка для админ роутера из-за ошибки'); } // 7. Помечаем сервер как готовый 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;