fdnjhbpfwbz
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ server.log
|
||||
*.txt
|
||||
package-lock.json
|
||||
*.zip
|
||||
.env
|
||||
61
auth.js
Normal file
61
auth.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
class AuthService {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
setDatabase(database) {
|
||||
this.db = database;
|
||||
this.initUsers();
|
||||
}
|
||||
|
||||
initUsers() {
|
||||
if (!this.db) return;
|
||||
const users = [
|
||||
{
|
||||
login: process.env.USER_1_LOGIN,
|
||||
password: process.env.USER_1_PASSWORD,
|
||||
name: process.env.USER_1_NAME,
|
||||
email: process.env.USER_1_EMAIL,
|
||||
role: process.env.USER_1_ROLE || 'admin'
|
||||
}
|
||||
];
|
||||
for (const u of users) {
|
||||
if (u.login && u.password) {
|
||||
this.createUserIfNotExists(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createUserIfNotExists(userData) {
|
||||
const row = this.db.prepare('SELECT id FROM users WHERE login = ?').get(userData.login);
|
||||
if (!row) {
|
||||
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
||||
this.db.prepare(
|
||||
'INSERT INTO users (login, password, name, email, role, auth_type, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime(\'now\'))'
|
||||
).run(userData.login, hashedPassword, userData.name, userData.email || null, userData.role, 'local');
|
||||
console.log('Создан пользователь: ' + userData.name);
|
||||
}
|
||||
}
|
||||
|
||||
authenticate(login, password) {
|
||||
const user = this.db.prepare("SELECT * FROM users WHERE login = ? AND auth_type = 'local'").get(login);
|
||||
if (!user) return null;
|
||||
const isValid = bcrypt.compareSync(password, user.password);
|
||||
if (isValid) {
|
||||
this.db.prepare("UPDATE users SET last_login = datetime('now') WHERE id = ?").run(user.id);
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getUserById(id) {
|
||||
const user = this.db.prepare('SELECT id, login, name, email, role, auth_type, created_at, last_login FROM users WHERE id = ?').get(id);
|
||||
return user || null;
|
||||
}
|
||||
}
|
||||
|
||||
const authService = new AuthService();
|
||||
module.exports = authService;
|
||||
@@ -13,8 +13,12 @@
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.17",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"connect-sqlite3": "^0.9.16",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.22.2",
|
||||
"express-session": "^1.18.2",
|
||||
"multer": "^2.2.0",
|
||||
"pdf-parse": "^2.4.5"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
if (typeof window.fetch === 'function' && !window.__fetchPatched) {
|
||||
window.__fetchPatched = true;
|
||||
const _origFetch = window.fetch.bind(window);
|
||||
window.fetch = function(input, init) {
|
||||
init = init || {};
|
||||
init.credentials = 'include';
|
||||
init.cache = 'no-store';
|
||||
const method = (init.method || 'GET').toUpperCase();
|
||||
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
||||
const token = window.__CSRF_TOKEN;
|
||||
if (token) {
|
||||
if (!init.headers) init.headers = {};
|
||||
if (typeof init.headers === 'object' && !(init.headers instanceof Headers)) {
|
||||
init.headers['X-CSRF-Token'] = token;
|
||||
} else if (init.headers instanceof Headers) {
|
||||
init.headers.set('X-CSRF-Token', token);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _origFetch(input, init);
|
||||
};
|
||||
}
|
||||
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
@@ -804,5 +827,7 @@ function renderDvoikiCards(rows) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
loadFilters();
|
||||
loadAll();
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('login-form').addEventListener('submit', login);
|
||||
checkAuth();
|
||||
});
|
||||
|
||||
136
public/auth.js
Normal file
136
public/auth.js
Normal file
@@ -0,0 +1,136 @@
|
||||
let currentUser = null;
|
||||
|
||||
function getCookie(name) {
|
||||
const match = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
|
||||
return match ? match[2] : null;
|
||||
}
|
||||
|
||||
function setCookie(name, value, maxAgeSec) {
|
||||
document.cookie = name + '=' + value + ';path=/;max-age=' + maxAgeSec;
|
||||
}
|
||||
|
||||
function getLockedUntil() { return parseInt(getCookie('lockout_until') || '0', 10); }
|
||||
function setLockedUntil(ts) { setCookie('lockout_until', ts, 300); }
|
||||
function getLoginAttempts() { return parseInt(getCookie('login_attempts') || '0', 10); }
|
||||
function setLoginAttempts(count) { setCookie('login_attempts', count, 300); }
|
||||
function clearLoginCookies() {
|
||||
setCookie('login_attempts', 0, 0);
|
||||
setCookie('lockout_until', 0, 0);
|
||||
}
|
||||
|
||||
function isLocked() {
|
||||
return Date.now() < getLockedUntil();
|
||||
}
|
||||
|
||||
function getRemainingLockoutSeconds() {
|
||||
const rem = Math.floor((getLockedUntil() - Date.now()) / 1000);
|
||||
return Math.max(0, rem);
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/user', { credentials: 'include' });
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
currentUser = data.user;
|
||||
if (data.csrfToken) {
|
||||
window.__CSRF_TOKEN = data.csrfToken;
|
||||
}
|
||||
showMainInterface();
|
||||
} else {
|
||||
showLoginInterface();
|
||||
}
|
||||
} catch (error) {
|
||||
showLoginInterface();
|
||||
}
|
||||
}
|
||||
|
||||
function showLoginInterface() {
|
||||
document.getElementById('app').style.display = 'none';
|
||||
document.getElementById('login-screen').style.display = 'flex';
|
||||
}
|
||||
|
||||
function showMainInterface() {
|
||||
document.getElementById('login-screen').style.display = 'none';
|
||||
document.getElementById('app').style.display = 'block';
|
||||
const userEl = document.getElementById('currentUser');
|
||||
if (userEl && currentUser) {
|
||||
userEl.textContent = (currentUser.role === 'admin' ? '👑 ' : '') + currentUser.name;
|
||||
}
|
||||
if (typeof loadFilters === 'function') loadFilters();
|
||||
if (typeof loadAll === 'function') loadAll();
|
||||
}
|
||||
|
||||
async function login(event) {
|
||||
event.preventDefault();
|
||||
if (isLocked()) {
|
||||
const rem = getRemainingLockoutSeconds();
|
||||
document.getElementById('login-error').textContent = 'Слишком много попыток. Подождите ' + rem + ' секунд.';
|
||||
document.getElementById('login-error').style.display = 'block';
|
||||
document.getElementById('login-attempts').textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const login = document.getElementById('login').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const errorEl = document.getElementById('login-error');
|
||||
const attemptsEl = document.getElementById('login-attempts');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ login, password })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
clearLoginCookies();
|
||||
const data = await response.json();
|
||||
currentUser = data.user;
|
||||
if (data.csrfToken) {
|
||||
window.__CSRF_TOKEN = data.csrfToken;
|
||||
}
|
||||
showMainInterface();
|
||||
} else if (response.status === 429) {
|
||||
const data = await response.json();
|
||||
errorEl.textContent = data.error;
|
||||
errorEl.style.display = 'block';
|
||||
} else {
|
||||
let attempts = getLoginAttempts() + 1;
|
||||
setLoginAttempts(attempts);
|
||||
if (attempts >= 10) {
|
||||
setLockedUntil(Date.now() + 300000);
|
||||
errorEl.textContent = 'Слишком много попыток. Подождите 5 минут.';
|
||||
attemptsEl.textContent = '';
|
||||
} else {
|
||||
errorEl.textContent = 'Неверный логин или пароль';
|
||||
attemptsEl.textContent = 'Попыток: ' + attempts + ' из 10';
|
||||
}
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('login-error').textContent = 'Ошибка соединения';
|
||||
document.getElementById('login-error').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/api/logout', { method: 'POST' });
|
||||
} catch (e) {}
|
||||
currentUser = null;
|
||||
window.__CSRF_TOKEN = null;
|
||||
showLoginInterface();
|
||||
}
|
||||
|
||||
function togglePasswordVisibility() {
|
||||
const pwd = document.getElementById('password');
|
||||
const icon = document.querySelector('.password-toggle i');
|
||||
if (pwd.type === 'password') {
|
||||
pwd.type = 'text';
|
||||
if (icon) icon.className = 'fas fa-eye-slash';
|
||||
} else {
|
||||
pwd.type = 'password';
|
||||
if (icon) icon.className = 'fas fa-eye';
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,47 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Школьный журнал — статистика</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="login-screen">
|
||||
<div class="login-card">
|
||||
<h1 class="login-title">Школьный журнал</h1>
|
||||
<p class="login-subtitle">Войдите в свою учётную запись</p>
|
||||
<form id="login-form" autocomplete="off">
|
||||
<div class="input-group">
|
||||
<div class="input-underline">
|
||||
<i class="fas fa-user input-icon"></i>
|
||||
<input type="text" id="login" placeholder="Логин" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-underline">
|
||||
<i class="fas fa-lock input-icon"></i>
|
||||
<input type="password" id="password" placeholder="Пароль" required autocomplete="off">
|
||||
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()" tabindex="-1">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">
|
||||
<span>Войти</span>
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
<div id="login-error" class="login-error" style="display:none"></div>
|
||||
<div id="login-attempts" class="login-attempts"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app" style="display:none">
|
||||
<header>
|
||||
<h1>📊 Школьный журнал</h1>
|
||||
<span id="status" class="status">Нет данных</span>
|
||||
<div class="header-right">
|
||||
<span id="currentUser" class="user-info"></span>
|
||||
<button id="logoutBtn" class="btn-logout" onclick="logout()">Выйти</button>
|
||||
<span id="status" class="status">Нет данных</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
@@ -177,6 +213,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="auth.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
112
public/style.css
112
public/style.css
@@ -681,6 +681,118 @@ h2 { font-size: 18px; margin-bottom: 16px; color: #1a1a2e; }
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#login-screen {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
}
|
||||
.login-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
width: 400px;
|
||||
max-width: 95vw;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
}
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
color: #1a1a2e;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.login-subtitle {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.input-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.input-underline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 8px;
|
||||
padding: 0 12px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.input-underline:focus-within {
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
.input-icon {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.input-underline input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 12px 0;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
}
|
||||
.password-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
width: auto;
|
||||
}
|
||||
.password-toggle:hover {
|
||||
color: #555;
|
||||
}
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.login-error {
|
||||
margin-top: 14px;
|
||||
padding: 10px;
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.login-attempts {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #d97706;
|
||||
}
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.user-info {
|
||||
font-size: 14px;
|
||||
color: #c0c0d0;
|
||||
}
|
||||
.btn-logout {
|
||||
background: transparent;
|
||||
color: #c0c0d0;
|
||||
border: 1px solid #555;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.btn-logout:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #fff;
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.upload-section form { flex-direction: column; }
|
||||
button { width: 100%; }
|
||||
|
||||
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: 'Все данные удалены' });
|
||||
|
||||
12
src/db.js
12
src/db.js
@@ -12,6 +12,18 @@ db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
login TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT,
|
||||
role TEXT DEFAULT 'admin',
|
||||
auth_type TEXT DEFAULT 'local',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS classes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL
|
||||
|
||||
Reference in New Issue
Block a user