391 lines
15 KiB
JavaScript
391 lines
15 KiB
JavaScript
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 === undefined || parallel === null || !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: 'Ошибка сохранения урока: ' + err.message });
|
||
}
|
||
});
|
||
|
||
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)) {
|
||
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.post('/api/admin/clear-db', isAuthenticated, isAdmin, async (req, res) => {
|
||
try {
|
||
await db.clearAllData();
|
||
res.json({ success: true, message: 'База данных очищена' });
|
||
} catch (err) {
|
||
console.error('Ошибка очистки БД:', err);
|
||
res.status(500).json({ error: 'Не удалось очистить базу данных' });
|
||
}
|
||
});
|
||
|
||
// --------------------- Статические страницы ---------------------
|
||
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}`);
|
||
}); |