Files
OpenLesson/server.js
2026-04-13 10:43:17 +05:00

381 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 / XLSX) ---------------------
function parseLessonFromJsonRecord(record) {
let fields = {};
if (Array.isArray(record)) {
// Массив пар [["key","value"], ...]
for (const [key, value] of record) {
fields[key.trim()] = (value || '').toString().trim();
}
} else if (typeof record === 'object' && record !== null) {
// Обычный объект
fields = { ...record };
Object.keys(fields).forEach(k => {
fields[k.trim()] = (fields[k] || '').toString().trim();
});
} else {
throw new Error('Неверный формат записи');
}
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) {
let lesson;
try {
lesson = parseLessonFromJsonRecord(record);
} catch(e) {
continue; // пропускаем битые записи
}
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/Excel' });
}
});
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}`);
});