189 lines
5.6 KiB
HTML
189 lines
5.6 KiB
HTML
|
|
<!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>
|