feat: revamp web hub — global connection status, array config editor, log streaming
- Global connection badge in app header (persistent dashboard SSE via ConnectionProvider) - LogProvider keeps log data and SSE stream alive across page switches (no reload spinner) - Config drawer: password reveal toggle, array-type row editor for list fields - Plugin management: optimistic updates, race-free loading, reset all group overrides - Logs: load-earlier pagination; backend tail-read optimization (before/end offsets) - web_auth: Web password changes persist (sync_admin seeding only when .env has explicit key) - Dashboard: collect in thread pool + 1s result cache - ErrorBoundary + local Suspense: plugin chunk loading stays inside content area - UI polish: table row hover, mobile header/breadcrumb fixes, wider config drawer - config_standard: array type inference for list fields; array env skip - random_jm_code / steam_info: array schema migration; README web frontend note Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+75
-2
@@ -9,6 +9,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import sqlite3
|
||||
@@ -104,11 +105,78 @@ def init_auth_db() -> None:
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _env_has_password_key() -> bool:
|
||||
""".env 中是否显式配置了 hexi_web_password(区别于代码默认值 admin)。"""
|
||||
env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
if not env_path.exists():
|
||||
return False
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("hexi_web_password=") or line.startswith(
|
||||
"hexi_web_password ="
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _write_env_password(password: str) -> None:
|
||||
"""把 Web 端修改后的密码回写 .env,保持 .env 永远是权威源(重启后不还原)。"""
|
||||
env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
try:
|
||||
lines = (
|
||||
env_path.read_text(encoding="utf-8").splitlines()
|
||||
if env_path.exists()
|
||||
else []
|
||||
)
|
||||
replaced = False
|
||||
out: list[str] = []
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("hexi_web_password=") or stripped.startswith(
|
||||
"hexi_web_password ="
|
||||
):
|
||||
if not replaced:
|
||||
# JSON 字符串写法可安全转义 # 等特殊字符,dotenv 解析后与裸值一致
|
||||
out.append("hexi_web_password=" + json.dumps(password))
|
||||
replaced = True
|
||||
else:
|
||||
out.append(line)
|
||||
if not replaced:
|
||||
out.append("hexi_web_password=" + json.dumps(password))
|
||||
env_path.write_text(
|
||||
"\n".join(out).rstrip("\n") + "\n", encoding="utf-8"
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _seed_admin_if_missing(username: str, password: str) -> None:
|
||||
with _conn() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id FROM web_users WHERE username=?", (username,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
conn.execute(
|
||||
"INSERT INTO web_users(username,password_hash,is_active,created_at) VALUES(?,?,1,?)",
|
||||
(username, _hash_password(password), _now()),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def sync_admin() -> None:
|
||||
"""启动时调用:以 .env 为准创建/同步超级管理员(含改密码场景)。"""
|
||||
"""启动时调用:以 .env 为准创建/同步超级管理员。
|
||||
|
||||
仅当 .env 显式配置了 hexi_web_password 时才会在每次启动同步密码
|
||||
(此时 .env 是权威源,重启始终以 .env 覆盖);若 .env 未显式配置,
|
||||
则只在用户不存在时播种默认账号,保证 Web 端「修改密码」能持久生效、
|
||||
不被每次启动静默还原。
|
||||
"""
|
||||
init_auth_db()
|
||||
username = _read_env("hexi_web_username", "admin")
|
||||
password = _read_env("hexi_web_password", "admin")
|
||||
if not _env_has_password_key():
|
||||
_seed_admin_if_missing(username, password)
|
||||
return
|
||||
with _conn() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id FROM web_users WHERE username=?", (username,)
|
||||
@@ -155,7 +223,11 @@ def revoke_user_tokens(user_id: int, except_token: Optional[str] = None) -> None
|
||||
def change_password(
|
||||
user_id: int, old_password: str, new_password: str, keep_token: Optional[str] = None
|
||||
) -> bool:
|
||||
"""校验旧密码后改新密码,并吊销旧令牌(保留当前令牌可选)。成功返回 True。"""
|
||||
"""校验旧密码后改新密码,并吊销旧令牌(保留当前令牌可选)。成功返回 True。
|
||||
|
||||
成功后会回写 .env(hexi_web_password),保证重启后仍是新密码:
|
||||
否则 sync_admin 会按 .env 默认值把密码还原。
|
||||
"""
|
||||
if not new_password:
|
||||
return False
|
||||
init_auth_db()
|
||||
@@ -170,6 +242,7 @@ def change_password(
|
||||
(_hash_password(new_password), user_id),
|
||||
)
|
||||
conn.commit()
|
||||
_write_env_password(new_password)
|
||||
revoke_user_tokens(user_id, except_token=keep_token)
|
||||
return True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user