feat(webui): смена пароля и кнопка выхода

- POST /api/auth/password — смена пароля админа с проверкой текущего,
  атомарно переписывает /etc/kaskad/webui.env и hot-reload WEB_PASS
  в памяти (рестарт не нужен)
- GET  /api/auth/whoami   — текущий логин (показываем в шапке)
- GET  /api/logout        — 401 с уникальным realm → браузер сбрасывает
  кеш Basic Auth
- UI: кнопки 🔑 Пароль и ↪ Выход в шапке; модалка смены пароля
  с двойным вводом и автологаутом после успеха

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
andrey271192
2026-05-12 06:37:52 +03:00
parent f6514e9bba
commit 035534e4f2
3 changed files with 153 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ from pathlib import Path
from flask import Flask, jsonify, render_template, request, Response
SERVERS_JSON = Path(os.environ.get("KASKAD_SERVERS_JSON", "/etc/wireguard/ru-servers.json"))
WEBUI_ENV = Path(os.environ.get("KASKAD_WEBUI_ENV", "/etc/kaskad/webui.env"))
LOCAL_HOST = os.environ.get("LOCAL_HOST", "ams1")
LOCAL_IP = os.environ.get("LOCAL_IP", "127.0.0.1")
BOT_KEY = os.environ.get("KASKAD_SSH_KEY", "/root/.ssh/id_ed25519")
@@ -545,6 +546,61 @@ def api_ips_clear():
return jsonify(results=results)
@app.route("/api/auth/whoami")
@require_auth
def api_whoami():
return jsonify(user=WEB_USER)
@app.route("/api/auth/password", methods=["POST"])
@require_auth
def api_change_password():
"""Сменить пароль администратора.
Перезаписывает строку KASKAD_WEB_PASS=... в /etc/kaskad/webui.env и
обновляет переменную в памяти процесса, чтобы НЕ требовался рестарт.
"""
global WEB_PASS
body = request.json or {}
current = (body.get("current") or "").strip()
new = (body.get("new") or "").strip()
if not new or len(new) < 8:
return jsonify(error="новый пароль должен быть не короче 8 символов"), 400
if current != WEB_PASS:
return jsonify(error="текущий пароль неверный"), 403
if new == current:
return jsonify(error="новый пароль совпадает со старым"), 400
if not WEBUI_ENV.exists():
return jsonify(error=f"нет файла {WEBUI_ENV} — сменить пароль вручную невозможно"), 500
try:
lines = WEBUI_ENV.read_text().splitlines()
found = False
for i, line in enumerate(lines):
if line.startswith("KASKAD_WEB_PASS="):
lines[i] = f"KASKAD_WEB_PASS={new}"
found = True
break
if not found:
lines.append(f"KASKAD_WEB_PASS={new}")
WEBUI_ENV.write_text("\n".join(lines) + "\n")
WEBUI_ENV.chmod(0o600)
except Exception as e:
return jsonify(error=f"запись {WEBUI_ENV}: {e}"), 500
WEB_PASS = new
return jsonify(ok=True)
@app.route("/api/logout")
def api_logout():
"""Возвращаем 401 с новым realm — браузер сбрасывает кеш Basic Auth."""
return Response(
"logged out", 401,
{"WWW-Authenticate": f'Basic realm="kaskad-logout-{os.urandom(4).hex()}"'},
)
@app.route("/")
@require_auth
def index():