fix(webui): cookie session login/logout (replace Basic Auth)

Browsers cannot clear HTTP Basic credentials; logout never worked reliably.
Use Flask signed session + /login form + GET /logout + POST /api/logout.
Optional KASKAD_SECRET_KEY or auto /etc/kaskad/.session_secret.
Fetch API uses credentials: same-origin; 401 redirects to /login.

Docs + README updated.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Андрей Бобырев
2026-05-12 20:04:03 +03:00
parent b8757548a2
commit a2182d0b91
7 changed files with 182 additions and 57 deletions

View File

@@ -246,9 +246,15 @@
this.loading = true; this.error = '';
try {
const r = await fetch(url, {
method, headers: {'Content-Type':'application/json'},
method,
credentials: 'same-origin',
headers: {'Content-Type':'application/json'},
body: body ? JSON.stringify(body) : undefined
});
if (r.status === 401) {
window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname || '/');
throw new Error('требуется вход');
}
const j = await r.json().catch(()=>({}));
if (!r.ok) throw new Error(j.error || r.statusText);
return j;
@@ -278,33 +284,15 @@
});
} catch { return; }
this.settingsOpen = false;
this.flash('Пароль изменён. Сейчас браузер попросит новый.');
setTimeout(() => this.logout(), 1200);
this.flash('Пароль изменён');
this.forms.pwd = {current:'', new:'', confirm:''};
},
logout() {
// HTTP Basic Auth живёт в браузере отдельно от cookies. fetch() с ручным
// Authorization часто ИГНОРИРУЕТСЯ — Chrome подставляет сохранённый пароль.
// XMLHttpRequest.open(..., user, password) заставляет уйти именно эта пара
// (заведомо ложная), сервер отвечает 401 + новый realm — после этого
// location.replace('/') обычно снова показывает окно входа.
const go = () => {
const u = new URL(window.location.href);
u.pathname = '/';
u.search = '_bye=' + Date.now();
u.hash = '';
window.location.replace(u.href);
};
const noise = () => Math.random().toString(36).slice(2) + Date.now();
async logout() {
try {
const xhr = new XMLHttpRequest();
xhr.timeout = 12000;
xhr.open('GET', '/api/logout?_=' + noise(), true, '__logout', noise());
xhr.onload = xhr.onerror = xhr.ontimeout = go;
xhr.send();
} catch (e) {
go();
}
await fetch('/api/logout', { method: 'POST', credentials: 'same-origin' });
} catch {}
window.location.href = '/login';
},
fmtAge(s) {

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вход — Kaskad</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body class="login-page">
<div class="login-box">
<h1>Каскад</h1>
<p class="muted">Вход в панель управления</p>
{% if error %}
<p class="status err" style="margin:12px 0">{{ error }}</p>
{% endif %}
<form method="post" action="{{ url_for('login') }}">
<input type="hidden" name="next" value="{{ next_url }}">
<label>Логин
<input name="username" autocomplete="username" required autofocus>
</label>
<label>Пароль
<input type="password" name="password" autocomplete="current-password" required>
</label>
<button type="submit" class="danger solid">Войти</button>
</form>
</div>
</body>
</html>