fdnjhbpfwbz
This commit is contained in:
152
server.js
152
server.js
@@ -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: 'Все данные удалены' });
|
||||
|
||||
Reference in New Issue
Block a user