Files
SchoolVue/public/auth.js
2026-07-07 10:37:51 +05:00

137 lines
4.7 KiB
JavaScript

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';
}
}