14.04.2026 Update

This commit is contained in:
2026-04-15 11:38:26 +03:00
parent 6aa0349f5d
commit f50d79fab3
45 changed files with 5645 additions and 751 deletions

189
public/login.html Normal file
View File

@@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NanoRouter — Вход</title>
<link rel="stylesheet" href="style.css">
</head>
<body class="login-body">
<div class="login-wrapper">
<div class="login-card">
<div class="login-logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="48" height="48">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5"/>
<path d="M2 12l10 5 10-5"/>
</svg>
</div>
<h1 class="login-title">NanoRouter</h1>
<p class="login-subtitle">Войдите в панель управления</p>
<form id="loginForm" class="login-form" autocomplete="off">
<div class="form-row">
<label for="username">Логин</label>
<input type="text" id="username" name="username" placeholder="admin" required autocomplete="username">
</div>
<div class="form-row">
<label for="password">Пароль</label>
<input type="password" id="password" name="password" placeholder="Пароль" required autocomplete="current-password">
</div>
<input type="hidden" id="nonce" name="nonce">
<input type="hidden" id="response" name="response">
<button type="submit" class="btn btn-primary login-btn" id="submitBtn">Войти</button>
</form>
<div id="loginError" class="login-error hidden"></div>
</div>
</div>
<script>
(function() {
async function sha256hex(str) {
const enc = new TextEncoder();
const data = enc.encode(str);
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function hashPassword(password) {
let salt = 'nano-router-salt-v1';
for (let i = 0; i < 10000; i++) {
const data = salt + password + i.toString();
const hash = await sha256hex(data);
salt = hash.substring(0, 32);
}
const finalHash = await sha256hex(salt + password);
return finalHash;
}
async function computeResponse(nonce, passwordHash) {
const str = nonce + ':' + passwordHash;
return await sha256hex(str);
}
async function login(e) {
e.preventDefault();
const errEl = document.getElementById('loginError');
const btn = document.getElementById('submitBtn');
errEl.classList.add('hidden');
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (!username || !password) {
errEl.textContent = 'Введите логин и пароль';
errEl.classList.remove('hidden');
return;
}
btn.disabled = true;
btn.textContent = 'Вход...';
try {
const chRes = await fetch('/api/auth/challenge');
const chJson = await chRes.json();
if (!chJson.success) {
throw new Error(chJson.error || 'Failed to get challenge');
}
const nonce = chJson.data.nonce;
const passwordHash = await hashPassword(password);
const response = await computeResponse(nonce, passwordHash);
const loginRes = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nonce, response })
});
const loginJson = await loginRes.json();
if (!loginJson.success) {
throw new Error(loginJson.error || 'Authentication failed');
}
const redirect = new URLSearchParams(window.location.search).get('redirect') || '/home.html';
window.location.href = redirect;
} catch (err) {
errEl.textContent = err.message;
errEl.classList.remove('hidden');
btn.disabled = false;
btn.textContent = 'Войти';
}
}
document.getElementById('loginForm').addEventListener('submit', login);
})();
</script>
<style>
.login-body {
background: var(--bg);
background-image:
radial-gradient(ellipse at 20% 0%, rgba(0, 212, 255, 0.06) 0%, transparent 60%),
radial-gradient(ellipse at 80% 100%, rgba(0, 255, 136, 0.04) 0%, transparent 50%);
}
.login-wrapper {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-card {
width: 100%;
max-width: 380px;
background: var(--surface);
border: 1px solid var(--border-hi);
border-radius: var(--radius);
padding: 40px 32px;
backdrop-filter: blur(20px);
box-shadow: var(--shadow), 0 0 40px rgba(0, 212, 255, 0.06);
text-align: center;
}
.login-logo { margin-bottom: 16px; }
.login-logo svg { stroke: var(--accent); filter: drop-shadow(0 0 12px var(--accent-glow)); }
.login-title {
font-size: 1.4rem;
font-weight: 700;
color: var(--text);
text-shadow: 0 0 20px rgba(0, 212, 255, 0.2);
margin-bottom: 6px;
}
.login-subtitle {
font-size: .85rem;
color: var(--muted);
margin-bottom: 28px;
}
.login-form {
display: flex;
flex-direction: column;
gap: 16px;
text-align: left;
}
.login-form .form-row label {
font-size: .76rem;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: .06em;
}
.login-btn {
width: 100%;
padding: 12px;
font-size: 1rem;
margin-top: 8px;
}
.login-error {
margin-top: 16px;
padding: 10px 14px;
border-radius: var(--radius-sm);
background: rgba(255, 51, 102, 0.1);
border: 1px solid rgba(255, 51, 102, 0.3);
color: var(--danger);
font-size: .85rem;
text-align: center;
}
</style>
</body>
</html>