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

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ server.log
*.txt *.txt
package-lock.json package-lock.json
*.zip *.zip
.env

61
auth.js Normal file
View 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;

View File

@@ -13,8 +13,12 @@
"type": "commonjs", "type": "commonjs",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.17", "adm-zip": "^0.5.17",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^12.11.1", "better-sqlite3": "^12.11.1",
"connect-sqlite3": "^0.9.16",
"dotenv": "^16.3.1",
"express": "^4.22.2", "express": "^4.22.2",
"express-session": "^1.18.2",
"multer": "^2.2.0", "multer": "^2.2.0",
"pdf-parse": "^2.4.5" "pdf-parse": "^2.4.5"
} }

View File

@@ -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 dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn'); const uploadBtn = document.getElementById('uploadBtn');
@@ -804,5 +827,7 @@ function renderDvoikiCards(rows) {
}).join(''); }).join('');
} }
loadFilters(); document.addEventListener('DOMContentLoaded', function() {
loadAll(); document.getElementById('login-form').addEventListener('submit', login);
checkAuth();
});

136
public/auth.js Normal file
View 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';
}
}

View File

@@ -5,11 +5,47 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Школьный журнал — статистика</title> <title>Школьный журнал — статистика</title>
<link rel="stylesheet" href="style.css"> <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> </head>
<body> <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> <header>
<h1>📊 Школьный журнал</h1> <h1>📊 Школьный журнал</h1>
<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> <span id="status" class="status">Нет данных</span>
</div>
</header> </header>
<main> <main>
@@ -177,6 +213,8 @@
</div> </div>
</div> </div>
<script src="auth.js"></script>
<script src="app.js"></script> <script src="app.js"></script>
</div>
</body> </body>
</html> </html>

View File

@@ -681,6 +681,118 @@ h2 { font-size: 18px; margin-bottom: 16px; color: #1a1a2e; }
font-size: 13px; 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) { @media (max-width: 640px) {
.upload-section form { flex-direction: column; } .upload-section form { flex-direction: column; }
button { width: 100%; } button { width: 100%; }

152
server.js
View File

@@ -1,12 +1,19 @@
require('dotenv').config();
const express = require('express'); const express = require('express');
const multer = require('multer'); const multer = require('multer');
const path = require('path'); const path = require('path');
const fs = require('fs'); 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 { parsePdfBuffer } = require('./src/parser');
const { upsertClass, upsertStudent, upsertSubject, upsertTeacher, upsertGradeValue, insertDailyGradeValue, insertLessonPlanValue, db } = require('./src/db'); const { upsertClass, upsertStudent, upsertSubject, upsertTeacher, upsertGradeValue, insertDailyGradeValue, insertLessonPlanValue, db } = require('./src/db');
const { getFullStats, getSubjectDetails, getTeacherDetails, getClassDetails } = require('./src/stats'); const { getFullStats, getSubjectDetails, getTeacherDetails, getClassDetails } = require('./src/stats');
const AdmZip = require('adm-zip'); const AdmZip = require('adm-zip');
const authService = require('./auth');
authService.setDatabase(db);
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@@ -54,6 +61,123 @@ const tasks = new Map();
app.use(express.json()); app.use(express.json());
app.use(express.static(path.join(__dirname, 'public'))); 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) { function processParsedPdf(parsed) {
if (!parsed.className) throw new Error('Не удалось определить класс'); if (!parsed.className) throw new Error('Не удалось определить класс');
if (!parsed.subjects || parsed.subjects.length === 0) 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 { try {
if (!req.file) return res.status(400).json({ error: 'Файл не загружен' }); 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); const task = tasks.get(req.params.taskId);
if (!task) return res.status(404).json({ error: 'Задача не найдена' }); 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 { try {
res.json(getFullStats(req.query)); res.json(getFullStats(req.query));
} catch (e) { } 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 { try {
res.json(getSubjectDetails(req.params.name)); res.json(getSubjectDetails(req.params.name));
} catch (e) { } 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 { try {
res.json(getTeacherDetails(req.params.name)); res.json(getTeacherDetails(req.params.name));
} catch (e) { } 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 { try {
res.json(getClassDetails(req.params.name)); res.json(getClassDetails(req.params.name));
} catch (e) { } 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 { 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 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); 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 { try {
const { student, subject, teacher, date_from, date_to, limit, offset } = req.query; const { student, subject, teacher, date_from, date_to, limit, offset } = req.query;
const cls = buildClassWhere(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 { try {
const { period, student, subject, teacher, date_from, date_to } = req.query; const { period, student, subject, teacher, date_from, date_to } = req.query;
const cls = buildClassWhere(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 { try {
const { subject, teacher, date_from, date_to, no_topic, no_homework, low_grades, min_grades } = req.query; const { subject, teacher, date_from, date_to, no_topic, no_homework, low_grades, min_grades } = req.query;
const cls = buildClassWhere(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 { try {
const { student, subject, teacher, date_from, date_to } = req.query; const { student, subject, teacher, date_from, date_to } = req.query;
const cls = buildClassWhere(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 { try {
const cls = db.prepare(`SELECT id FROM classes WHERE name = ?`).get(req.params.name); const cls = db.prepare(`SELECT id FROM classes WHERE name = ?`).get(req.params.name);
if (!cls) return res.status(404).json({ error: 'Класс не найден' }); 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 { try {
const classes = db.prepare(`SELECT id, name FROM classes WHERE name LIKE ?`).all(req.params.grade + '%'); 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: 'Классы не найдены' }); 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 { 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;'); 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: 'Все данные удалены' }); res.json({ ok: true, message: 'Все данные удалены' });

View File

@@ -12,6 +12,18 @@ db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON'); db.pragma('foreign_keys = ON');
db.exec(` 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 ( CREATE TABLE IF NOT EXISTS classes (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL name TEXT UNIQUE NOT NULL