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

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 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
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">
<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>

View File

@@ -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%; }