fdnjhbpfwbz

This commit is contained in:
2026-07-07 10:37:51 +05:00
parent c9e757a981
commit 026d346d31
9 changed files with 531 additions and 18 deletions

152
server.js
View File

@@ -1,12 +1,19 @@
require('dotenv').config();
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
const { parsePdfBuffer } = require('./src/parser');
const { upsertClass, upsertStudent, upsertSubject, upsertTeacher, upsertGradeValue, insertDailyGradeValue, insertLessonPlanValue, db } = require('./src/db');
const { getFullStats, getSubjectDetails, getTeacherDetails, getClassDetails } = require('./src/stats');
const AdmZip = require('adm-zip');
const authService = require('./auth');
authService.setDatabase(db);
const app = express();
const PORT = process.env.PORT || 3000;
@@ -54,6 +61,123 @@ const tasks = new Map();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({
store: new SQLiteStore({
db: 'sessions.db',
dir: path.join(__dirname, 'data'),
table: 'sessions'
}),
secret: process.env.SESSION_SECRET || 'fallback_secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: false,
maxAge: 7 * 24 * 60 * 60 * 1000,
httpOnly: true,
sameSite: 'lax'
}
}));
app.use((req, res, next) => {
const mutating = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
const isLoginPath = req.path === '/api/login';
const isStatic = req.path.startsWith('/');
if (!isLoginPath && req.session && req.session.user && !req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
if (mutating) {
if (isLoginPath) return next();
if (isStatic && !req.path.startsWith('/api/')) return next();
if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
const clientToken = req.headers['x-csrf-token'];
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
if (!clientToken || clientToken !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF token missing or invalid' });
}
}
next();
});
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
next();
};
const loginAttempts = new Map();
app.post('/api/login', (req, res) => {
const ip = req.ip || req.connection.remoteAddress;
const now = Date.now();
if (loginAttempts.has(ip)) {
const data = loginAttempts.get(ip);
if (now < data.resetTime && data.count > 10) {
return res.status(429).json({ error: 'Слишком много попыток входа. Попробуйте через минуту.' });
}
if (now > data.resetTime) {
loginAttempts.set(ip, { count: 1, resetTime: now + 60000 });
} else {
data.count++;
}
} else {
loginAttempts.set(ip, { count: 1, resetTime: now + 60000 });
}
const { login, password } = req.body;
if (!login || !password) {
return res.status(400).json({ error: 'Логин и пароль обязательны' });
}
try {
const user = 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
};
if (sessionUser.login === process.env.USER_1_LOGIN) {
sessionUser.role = 'admin';
}
req.session.user = sessionUser;
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
req.session.save((err) => {
res.json({ success: true, user: sessionUser, csrfToken: req.session.csrfToken });
});
} else {
res.status(401).json({ error: 'Неверный логин или пароль' });
}
} catch (error) {
res.status(500).json({ error: 'Ошибка сервера при авторизации' });
}
});
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'Ошибка при выходе' });
res.json({ success: true });
});
});
app.get('/api/user', (req, res) => {
if (req.session && req.session.user) {
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
res.json({ user: req.session.user, csrfToken: req.session.csrfToken });
} else {
res.status(401).json({ error: 'Не аутентифицирован' });
}
});
function processParsedPdf(parsed) {
if (!parsed.className) throw new Error('Не удалось определить класс');
if (!parsed.subjects || parsed.subjects.length === 0) throw new Error('Не найдены предметы');
@@ -149,7 +273,7 @@ function processParsedPdf(parsed) {
};
}
app.post('/api/upload', upload.single('pdf'), async (req, res) => {
app.post('/api/upload', requireAuth, upload.single('pdf'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'Файл не загружен' });
@@ -243,7 +367,7 @@ app.post('/api/upload', upload.single('pdf'), async (req, res) => {
}
});
app.get('/api/task/:taskId', (req, res) => {
app.get('/api/task/:taskId', requireAuth, (req, res) => {
const task = tasks.get(req.params.taskId);
if (!task) return res.status(404).json({ error: 'Задача не найдена' });
@@ -267,7 +391,7 @@ app.get('/api/task/:taskId', (req, res) => {
}
});
app.get('/api/stats', (req, res) => {
app.get('/api/stats', requireAuth, (req, res) => {
try {
res.json(getFullStats(req.query));
} catch (e) {
@@ -275,7 +399,7 @@ app.get('/api/stats', (req, res) => {
}
});
app.get('/api/subjects/:name', (req, res) => {
app.get('/api/subjects/:name', requireAuth, (req, res) => {
try {
res.json(getSubjectDetails(req.params.name));
} catch (e) {
@@ -283,7 +407,7 @@ app.get('/api/subjects/:name', (req, res) => {
}
});
app.get('/api/teachers/:name', (req, res) => {
app.get('/api/teachers/:name', requireAuth, (req, res) => {
try {
res.json(getTeacherDetails(req.params.name));
} catch (e) {
@@ -291,7 +415,7 @@ app.get('/api/teachers/:name', (req, res) => {
}
});
app.get('/api/classes/:name', (req, res) => {
app.get('/api/classes/:name', requireAuth, (req, res) => {
try {
res.json(getClassDetails(req.params.name));
} catch (e) {
@@ -299,7 +423,7 @@ app.get('/api/classes/:name', (req, res) => {
}
});
app.get('/api/filters', (req, res) => {
app.get('/api/filters', requireAuth, (req, res) => {
try {
const students = db.prepare(`SELECT DISTINCT s.full_name FROM daily_grades d JOIN students s ON d.student_id = s.id ORDER BY s.full_name`).all().map(r => r.full_name);
const subjects = db.prepare(`SELECT DISTINCT sj.name FROM daily_grades d JOIN subjects sj ON d.subject_id = sj.id ORDER BY sj.name`).all().map(r => r.name);
@@ -312,7 +436,7 @@ app.get('/api/filters', (req, res) => {
}
});
app.get('/api/daily', (req, res) => {
app.get('/api/daily', requireAuth, (req, res) => {
try {
const { student, subject, teacher, date_from, date_to, limit, offset } = req.query;
const cls = buildClassWhere(req.query);
@@ -391,7 +515,7 @@ app.get('/api/daily', (req, res) => {
}
});
app.get('/api/pivot', (req, res) => {
app.get('/api/pivot', requireAuth, (req, res) => {
try {
const { period, student, subject, teacher, date_from, date_to } = req.query;
const cls = buildClassWhere(req.query);
@@ -548,7 +672,7 @@ app.get('/api/pivot', (req, res) => {
}
});
app.get('/api/lessons', (req, res) => {
app.get('/api/lessons', requireAuth, (req, res) => {
try {
const { subject, teacher, date_from, date_to, no_topic, no_homework, low_grades, min_grades } = req.query;
const cls = buildClassWhere(req.query);
@@ -634,7 +758,7 @@ app.get('/api/lessons', (req, res) => {
}
});
app.get('/api/dvoiki', (req, res) => {
app.get('/api/dvoiki', requireAuth, (req, res) => {
try {
const { student, subject, teacher, date_from, date_to } = req.query;
const cls = buildClassWhere(req.query);
@@ -737,7 +861,7 @@ app.get('/api/dvoiki', (req, res) => {
}
});
app.delete('/api/classes/:name', (req, res) => {
app.delete('/api/classes/:name', requireAuth, (req, res) => {
try {
const cls = db.prepare(`SELECT id FROM classes WHERE name = ?`).get(req.params.name);
if (!cls) return res.status(404).json({ error: 'Класс не найден' });
@@ -754,7 +878,7 @@ app.delete('/api/classes/:name', (req, res) => {
}
});
app.delete('/api/parallel/:grade', (req, res) => {
app.delete('/api/parallel/:grade', requireAuth, (req, res) => {
try {
const classes = db.prepare(`SELECT id, name FROM classes WHERE name LIKE ?`).all(req.params.grade + '%');
if (!classes.length) return res.status(404).json({ error: 'Классы не найдены' });
@@ -774,7 +898,7 @@ app.delete('/api/parallel/:grade', (req, res) => {
}
});
app.delete('/api/data', (req, res) => {
app.delete('/api/data', requireAuth, (req, res) => {
try {
db.exec('DELETE FROM lesson_plans; DELETE FROM daily_grades; DELETE FROM grades; DELETE FROM teachers; DELETE FROM students; DELETE FROM subjects; DELETE FROM classes;');
res.json({ ok: true, message: 'Все данные удалены' });