Files
xiaobai-review/backend/features/accounts/http.py
T
施工员andmultica-agent 3eaa36a8d5 feat(HEL-560): 数据中枢接管数据源/模型池/会员,注册改一次性邀请码
主站
- 新增 m0006 invite_codes 迁移;注册强制邀请码(首个管理员除外),消码与建号
  同一事务,并发提交只有一个能成功
- 新增 /api/hub-admin/* 服务端点(共享 HUB_ADMIN_TOKEN,先于鉴权校验),供数据
  中枢桥接读写会话/密码/模型池/会员/邀请码,并提供供应商模型列表拉取
- 前端:注册表单加邀请码(桌面 login、index.html、移动端);「系统管理」改为
  「数据中枢」入口指向 8766,原模型池与会员管理分区移除,仅留「行情管理」;
  随之清理陈旧 CSS

数据中枢
- 取消独立账号:删除 hub_admin/hub_sessions 与登录、改密、锁定逻辑,改为校验
  主站 xiaobai_session,仅管理员可进,CSRF 由会话派生,危险操作二次确认走主站
- 控制台新增数据源凭证可编辑区(原有内容一项不删)、供应商制模型池(自动拉取
  /models,失败退回卡内手动录入)、会员管理与邀请码页
- 日夜双主题:颜色收敛为同名 token 换值,SVG 改用 inline style 以吃到变量

自测
- 主站 verify_baseline 通过(498 项);数据中枢 235 项通过
- tools/verify_datahub_console.py 端到端跑通两服务真实对话;
  tools/verify_datahub_console_ui.py 浏览器跑通门禁/凭证/模型池/会员/主题/1030 窄屏

Co-authored-by: multica-agent <github@multica.ai>
2026-09-16 11:44:09 +08:00

184 lines
6.9 KiB
Python

from __future__ import annotations
import json
import secrets
from http import HTTPStatus
from backend.features.accounts.security import token_hash
class AccountHttpMixin:
def _device_hash(self) -> str:
raw = self.device_token()
return token_hash(raw) if raw else ""
def _ensure_device_token(self) -> str:
return self.device_token() or secrets.token_urlsafe(32)
def _auth_success_headers(self, session_token: str, device_raw: str) -> list[tuple[str, str]]:
return [
("Set-Cookie", self.session_cookie(session_token)),
("Set-Cookie", self.device_cookie(device_raw)),
]
def _send_authenticated_session(self, result: dict, status: HTTPStatus, device_raw: str) -> None:
self.send_json(
{
"ok": True,
"authenticated": True,
"user": result["user"],
"csrf_token": result["csrf_token"],
},
status,
self._auth_success_headers(result["session_token"], device_raw),
)
def auth_register(self) -> None:
try:
body = self.read_json_body()
device_raw = self._ensure_device_token()
result = self.application_service.register_account(
str(body.get("username") or ""),
str(body.get("password") or ""),
token_hash(device_raw),
str(body.get("invite_code") or ""),
)
self._send_authenticated_session(result, HTTPStatus.CREATED, device_raw)
except (ValueError, json.JSONDecodeError) as exc:
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
def auth_login(self) -> None:
try:
body = self.read_json_body()
device_raw = self._ensure_device_token()
result = self.application_service.login_account(
str(body.get("username") or ""),
str(body.get("password") or ""),
token_hash(device_raw),
)
self._send_authenticated_session(result, HTTPStatus.OK, device_raw)
except (ValueError, json.JSONDecodeError) as exc:
self.send_json({"error": str(exc)}, HTTPStatus.UNAUTHORIZED)
def auth_accounts(self) -> None:
current_user_id = None
if self.require_auth(send_error=False):
current_user_id = int(self.auth_user["id"])
payload = self.application_service.accounts.list_device_accounts(
self._device_hash(),
current_user_id,
)
self.send_json({"ok": True, **payload})
def auth_switch(self) -> None:
try:
body = self.read_json_body()
try:
user_id = int(body.get("user_id"))
except (TypeError, ValueError):
user_id = 0
device_raw = self.device_token()
result = self.application_service.accounts.switch_account(
token_hash(device_raw) if device_raw else "",
user_id,
)
self._send_authenticated_session(
result,
HTTPStatus.OK,
device_raw or self._ensure_device_token(),
)
except PermissionError as exc:
self.send_json({"error": str(exc)}, HTTPStatus.UNAUTHORIZED)
except (ValueError, json.JSONDecodeError) as exc:
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
def auth_forget(self) -> None:
try:
body = self.read_json_body()
try:
user_id = int(body.get("user_id"))
except (TypeError, ValueError):
user_id = 0
self.application_service.accounts.forget_account(self._device_hash(), user_id)
except (ValueError, json.JSONDecodeError):
pass
self.send_json({"ok": True})
def auth_me(self) -> None:
service = self.application_service
if not self.require_auth(send_error=False):
self.send_json(
{
"ok": True,
"authenticated": False,
"registration_required": service.database.count_users() == 0,
}
)
return
headers = None
if not self.device_token():
device_raw = secrets.token_urlsafe(32)
service.accounts.remember_account(
token_hash(device_raw),
int(self.auth_user["id"]),
fresh=True,
)
headers = [("Set-Cookie", self.device_cookie(device_raw))]
self.send_json(
{
"ok": True,
"authenticated": True,
"user": {
"id": int(self.auth_user["id"]),
"username": str(self.auth_user["username"]),
"role": str(self.auth_user.get("role") or "user"),
"membership": service.membership(),
},
"csrf_token": str(self.auth_user["csrf_token"]),
},
headers=headers,
)
def auth_logout(self) -> None:
raw_token = self.session_token()
user_id = int(getattr(self, "auth_user", {}).get("id") or 0)
if raw_token:
self.application_service.database.delete_session(token_hash(raw_token))
self.application_service.accounts.revoke_current_device_grant(
self._device_hash(),
user_id,
)
self.send_json(
{"ok": True},
headers={"Set-Cookie": self.session_cookie("", clear=True)},
)
def save_birth_profile(self) -> None:
try:
body = self.read_json_body()
personal = self.application_service.save_birth_profile(body)
self.send_json({"ok": True, "personal": personal})
except (ValueError, json.JSONDecodeError) as exc:
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
def change_password(self) -> None:
try:
body = self.read_json_body()
current = str(body.get("current_password") or "")
new = str(body.get("new_password") or "")
confirmation = str(body.get("confirm_password") or "")
if new != confirmation:
raise ValueError("两次输入的新密码不一致。")
self.application_service.change_password(current, new)
self.send_json({"ok": True})
except (ValueError, json.JSONDecodeError) as exc:
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
def save_membership(self) -> None:
try:
service = self.application_service
service.update_membership(self.read_json_body())
self.send_json({"ok": True, "users": service.admin_users()})
except (ValueError, json.JSONDecodeError) as exc:
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)