require('dotenv').config(); const express = require('express'); const session = require('express-session'); const path = require('path'); const db = require('./sqllite'); const auth = require('./auth'); const app = express(); const PORT = process.env.PORT || 3000; app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, 'public'))); const SQLiteStore = require('connect-sqlite3')(session); app.use(session({ store: new SQLiteStore({ db: 'sessions.db', dir: './data' }), secret: process.env.SESSION_SECRET || 'default_secret', resave: false, saveUninitialized: false, cookie: { secure: false, httpOnly: true, maxAge: 1000 * 60 * 60 * 24 } })); function isAuthenticated(req, res, next) { if (req.session.user) { next(); } else { res.status(401).json({ error: 'Не авторизован' }); } } function isAdmin(req, res, next) { if (req.session.user && req.session.user.role === 'admin') { next(); } else { res.status(403).json({ error: 'Требуются права администратора' }); } } function isAllowed(req, res, next) { if (req.session.user && (req.session.user.role === 'admin' || req.session.user.role === 'user')) { next(); } else { res.status(403).json({ error: 'Доступ запрещён' }); } } // --------------------- API авторизации --------------------- app.post('/api/auth', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ success: false, message: 'Логин и пароль обязательны' }); } if (username === process.env.USER_1_LOGIN && password === process.env.USER_1_PASSWORD) { req.session.user = { username: process.env.USER_1_LOGIN, full_name: process.env.USER_1_NAME, role: process.env.USER_1_ROLE || 'admin', groups: ['local'] }; return res.json({ success: true, full_name: process.env.USER_1_NAME, role: req.session.user.role }); } const ldapResult = await auth.authenticateWithLDAP(username, password); if (!ldapResult.success) { return res.status(401).json({ success: false, message: ldapResult.message }); } const access = auth.checkUserAccess(ldapResult.groups); if (!access.allowed) { return res.status(403).json({ success: false, message: access.message }); } req.session.user = { username: ldapResult.username, full_name: ldapResult.full_name, role: access.role, groups: ldapResult.groups }; res.json({ success: true, full_name: ldapResult.full_name, role: access.role }); }); app.post('/api/logout', (req, res) => { req.session.destroy(); res.json({ success: true }); }); app.get('/api/me', (req, res) => { if (req.session.user) { res.json({ authenticated: true, user: req.session.user }); } else { res.json({ authenticated: false }); } }); // --------------------- API для родителей --------------------- app.get('/api/lessons', async (req, res) => { try { const filter = { class_name: req.query.class_name, parallel: req.query.parallel, teacher: req.query.teacher, topic: req.query.topic }; const lessons = await db.getLessons(filter); const enriched = lessons.map(l => ({ ...l, available: l.current_slots < l.max_slots })); res.json(enriched); } catch (err) { console.error(err); res.status(500).json({ error: 'Ошибка получения уроков' }); } }); app.post('/api/register', async (req, res) => { const { lesson_id, parent_name, parent_phone } = req.body; if (!lesson_id || !parent_name || !parent_phone) { return res.status(400).json({ error: 'Все поля обязательны' }); } try { const lesson = await db.getLessonById(lesson_id); if (!lesson) return res.status(404).json({ error: 'Урок не найден' }); if (lesson.current_slots >= lesson.max_slots) { return res.status(400).json({ error: 'Нет свободных мест' }); } const result = await db.incrementCurrentSlots(lesson_id); if (result.changes === 0) { return res.status(400).json({ error: 'Места закончились, попробуйте позже' }); } await db.addRegistration(lesson_id, parent_name, parent_phone); res.json({ success: true, message: 'Вы успешно записаны!' }); } catch (err) { console.error(err); res.status(500).json({ error: 'Ошибка при записи' }); } }); // --------------------- Административные API --------------------- app.get('/api/admin/lessons', isAuthenticated, isAdmin, async (req, res) => { try { const filter = { class_name: req.query.class_name, parallel: req.query.parallel, teacher: req.query.teacher, topic: req.query.topic }; const lessons = await db.getLessons(filter); res.json(lessons); } catch (err) { console.error(err); res.status(500).json({ error: 'Ошибка получения уроков' }); } }); app.post('/api/admin/lessons', isAuthenticated, isAdmin, async (req, res) => { const { id, class_name, parallel, subject, teacher, topic, max_slots, date, time } = req.body; if (!class_name || !parallel || !subject || !teacher || !max_slots || !date || !time) { return res.status(400).json({ error: 'Все поля обязательны' }); } try { if (id) { const existing = await db.getLessonById(id); if (!existing) return res.status(404).json({ error: 'Урок не найден' }); await db.updateLesson(id, { class_name, parallel, subject, teacher, topic, max_slots, date, time }); res.json({ success: true, message: 'Урок обновлён' }); } else { const newId = await db.createLesson({ class_name, parallel, subject, teacher, topic, max_slots, date, time }); res.json({ success: true, message: 'Урок создан', id: newId }); } } catch (err) { console.error(err); res.status(500).json({ error: 'Ошибка сохранения урока' }); } }); app.delete('/api/admin/lessons/:id', isAuthenticated, isAdmin, async (req, res) => { try { await db.deleteLesson(req.params.id); res.json({ success: true }); } catch (err) { console.error(err); res.status(500).json({ error: 'Ошибка удаления' }); } }); app.get('/api/admin/registrations', isAuthenticated, isAdmin, async (req, res) => { const lessonId = req.query.lesson_id; try { if (lessonId) { const regs = await db.getRegistrationsByLesson(lessonId); res.json(regs); } else { const allRegs = await db.getAllRegistrations(); res.json(allRegs); } } catch (err) { console.error(err); res.status(500).json({ error: 'Ошибка получения записей' }); } }); // --------------------- API для страницы /info --------------------- app.get('/api/info/registrations', isAllowed, async (req, res) => { try { const filters = { parent_name: req.query.parent_name, class_name: req.query.class_name, subject: req.query.subject, teacher: req.query.teacher }; const registrations = await db.getRegistrationsWithFilters(filters); res.json(registrations); } catch (err) { console.error(err); res.status(500).json({ error: 'Ошибка получения данных' }); } }); app.get('/api/filter-options/subjects', async (req, res) => { try { const rows = await db.allQuery('SELECT DISTINCT subject FROM lessons ORDER BY subject'); res.json(rows.map(r => r.subject)); } catch (err) { res.status(500).json([]); } }); // --------------------- Импорт уроков из JSON (без даты/времени) --------------------- function parseLessonFromJsonRecord(record) { const fields = {}; for (const [key, value] of record) { fields[key.trim()] = (value || '').trim(); } const lastName = fields["Фамилия Учителя"] || ""; const firstName = fields["Имя Учителя"] || ""; const patronymic = fields["Отчество Учителя"] || ""; const teacher = `${lastName} ${firstName} ${patronymic}`.trim().replace(/\s+/g, ' '); const subject = fields["Предмет"] || ""; const topic = fields["Тема Урока"] || ""; const parallelRaw = fields["Класс (параллель)"] || ""; const classLetter = fields["Класс (буква)"] || ""; const parallel = parseInt(parallelRaw, 10); const className = `${parallelRaw}${classLetter}`.trim(); return { teacher, subject, topic, parallel, className }; } app.post('/api/admin/import/preview', isAuthenticated, isAdmin, async (req, res) => { try { let records = req.body.jsonData; if (typeof records === 'string') records = JSON.parse(records); if (!Array.isArray(records)) return res.status(400).json({ error: 'Данные должны быть массивом' }); const preview = []; for (const record of records) { if (!Array.isArray(record)) continue; const lesson = parseLessonFromJsonRecord(record); if (lesson.teacher && lesson.subject && !isNaN(lesson.parallel)) { preview.push(lesson); } } res.json({ success: true, preview }); } catch (err) { console.error(err); res.status(500).json({ error: 'Ошибка разбора JSON' }); } }); app.post('/api/admin/import', isAuthenticated, isAdmin, async (req, res) => { try { const { lessons, defaultMaxSlots } = req.body; if (!Array.isArray(lessons) || !defaultMaxSlots) { return res.status(400).json({ error: 'Недостаточно параметров' }); } const maxSlots = parseInt(defaultMaxSlots, 10); const defaultDate = new Date().toISOString().slice(0,10); const defaultTime = "12:00"; const results = []; for (const lesson of lessons) { const { className, parallel, subject, teacher, topic } = lesson; const existing = await db.getQuery( `SELECT id FROM lessons WHERE class_name=? AND subject=? AND teacher=? AND date=? AND time=?`, [className, subject, teacher, defaultDate, defaultTime] ); if (existing) { results.push({ className, subject, status: 'skipped', reason: 'уже существует' }); continue; } const newId = await db.createLesson({ class_name: className, parallel: parallel, subject: subject, teacher: teacher, topic: topic, max_slots: maxSlots, date: defaultDate, time: defaultTime }); results.push({ className, subject, id: newId, status: 'created' }); } res.json({ success: true, results }); } catch (err) { console.error(err); res.status(500).json({ error: 'Ошибка импорта: ' + err.message }); } }); // --------------------- Статические страницы --------------------- app.get('/admin', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin.html')); }); app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); app.get('/info', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'info.html')); }); app.get('/help', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'help.html')); }); // API для выпадающих списков (общие) app.get('/api/filter-options/class-names', async (req, res) => { try { const rows = await db.allQuery('SELECT DISTINCT class_name FROM lessons ORDER BY class_name'); res.json(rows.map(r => r.class_name)); } catch (err) { res.status(500).json([]); } }); app.get('/api/filter-options/teachers', async (req, res) => { try { const rows = await db.allQuery('SELECT DISTINCT teacher FROM lessons ORDER BY teacher'); res.json(rows.map(r => r.teacher)); } catch (err) { res.status(500).json([]); } }); app.get('/api/filter-options/topics', async (req, res) => { try { const rows = await db.allQuery('SELECT DISTINCT topic FROM lessons WHERE topic IS NOT NULL AND topic != "" ORDER BY topic'); res.json(rows.map(r => r.topic)); } catch (err) { res.status(500).json([]); } }); app.listen(PORT, () => { console.log(`Сервер запущен на http://localhost:${PORT}`); });