主站 - 新增 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>
77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from typing import Any
|
|
|
|
from datahub.crypto import SecretVault, mask_secret
|
|
from datahub.db import HubDB
|
|
from datahub.timeutil import isoformat
|
|
|
|
|
|
|
|
def token_hash(token: str) -> str:
|
|
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
|
|
|
|
|
class AuthService:
|
|
"""Machine credentials only: the `/v1` API token and the provider secrets.
|
|
|
|
Operator accounts live on the review site (HEL-560) — the console verifies
|
|
them through `SiteAuth`, so nothing here authenticates a person.
|
|
"""
|
|
|
|
def __init__(self, db: HubDB, vault: SecretVault, api_token: str) -> None:
|
|
self.db = db
|
|
self.vault = vault
|
|
self._bootstrap(api_token)
|
|
|
|
def _bootstrap(self, api_token: str) -> None:
|
|
if api_token:
|
|
existing = self.db.fetchone("SELECT token_hash FROM api_tokens WHERE name = ?", ("review",))
|
|
hashed = token_hash(api_token)
|
|
last4 = mask_secret(api_token)
|
|
if existing is None:
|
|
self.db.execute(
|
|
"INSERT INTO api_tokens(token_hash, name, last4, created_at) VALUES (?,?,?,?)",
|
|
(hashed, "review", last4, isoformat()),
|
|
)
|
|
elif existing["token_hash"] != hashed:
|
|
self.db.execute(
|
|
"UPDATE api_tokens SET token_hash = ?, last4 = ? WHERE name = ?",
|
|
(hashed, last4, "review"),
|
|
)
|
|
|
|
def check_api_token(self, supplied: str) -> bool:
|
|
if not supplied:
|
|
return False
|
|
row = self.db.fetchone(
|
|
"SELECT token_hash FROM api_tokens WHERE token_hash = ? AND revoked_at IS NULL",
|
|
(token_hash(supplied),),
|
|
)
|
|
return row is not None
|
|
|
|
def credential_status(self, name: str) -> dict[str, Any]:
|
|
row = self.db.fetchone("SELECT last4, updated_at FROM credentials WHERE name = ?", (name,))
|
|
if not row:
|
|
return {"configured": False, "last4": "", "updated_at": ""}
|
|
return {"configured": True, "last4": row["last4"], "updated_at": row["updated_at"]}
|
|
|
|
def store_credential(self, name: str, secret: str) -> None:
|
|
payload = self.vault.encrypt_json({name: secret})
|
|
self.db.execute(
|
|
"""
|
|
INSERT INTO credentials(name, encrypted_payload, last4, updated_at)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
encrypted_payload=excluded.encrypted_payload, last4=excluded.last4, updated_at=excluded.updated_at
|
|
""",
|
|
(name, payload, mask_secret(secret), isoformat()),
|
|
)
|
|
|
|
def load_credential(self, name: str) -> str:
|
|
row = self.db.fetchone("SELECT encrypted_payload FROM credentials WHERE name = ?", (name,))
|
|
if not row:
|
|
return ""
|
|
data = self.vault.decrypt_json(str(row["encrypted_payload"]))
|
|
return str(data.get(name) or "")
|