Files
steam-chat/docs/backend-redesign.md

12 KiB
Raw Permalink Blame History

Steam Chat 后台改造设计

目标

本次改造要把当前的 Steam Chat 从“启动时读取 config.js 并立即登录 Steam”改成“应用先启动后台用户登录后再由管理员在 Web 页面完成 Steam 登录”。

核心目标:

  • 不再依赖 config.js 保存 Steam 账号密码。
  • 服务无 Steam token 时也能启动并打开 Web 后台。
  • 增加 SQLite 多用户后台,管理员负责用户管理和 Steam 登录,普通用户只使用已连接的 Steam 会话聊天。
  • 前端完全重写为后台应用,不再在现有聊天页面上追加登录和管理功能。
  • 首次 Steam 登录成功后只持久化 refresh.token,不保存 Steam 明文密码或 Steam Guard 验证码。
  • 保留现有聊天、好友、群组、历史、图片和 WebSocket 能力。

当前问题

当前入口在启动时调用 Steam 登录流程。如果项目根目录没有 config.js,会退回 config.example.js;示例配置没有真实 Steam 凭据时,登录流程会报错,应用也无法作为一个可配置的后台服务正常交付。

当前持久化路径也分散在项目根目录下:

  • refresh.token
  • logs/chat.jsonl
  • logs/images/
  • logs/stickers/

容器部署后这些状态必须进入统一数据目录,否则镜像重建或容器重建会丢失登录 token、聊天记录和媒体缓存。

运行模型

改造后的启动顺序:

  1. 读取环境变量和数据目录配置。
  2. 初始化 SQLite 用户库和应用密钥。
  3. 启动 HTTP/WebSocket 服务。
  4. 如果数据目录存在 refresh.token,后台自动尝试 Steam token 登录。
  5. 如果没有 tokenWeb 后台保持可访问,管理员在页面发起 Steam 登录。

Steam 登录状态由后端统一维护:

  • logged_out:未登录 Steam聊天发送、好友、群组、表情等依赖 Steam 的接口返回 503。
  • logging_in:正在提交 Steam 登录。
  • waiting_guardSteam 要求邮箱码或手机 2FA等待管理员提交验证码。
  • onlineSteam 登录成功,聊天功能可用。
  • error:最近一次登录失败,状态接口返回错误信息。
  • reconnecting:已登录后断线,后台自动重连或等待管理员处理。

Web 页面在应用登录后始终可打开。未连接 Steam 时,聊天区域显示当前 Steam 状态;管理员可看到 Steam 登录表单,普通用户只能看到等待连接状态。

用户系统

使用 Node 26 内置 node:sqlite,不引入额外数据库容器或 native npm 依赖。

SQLite 文件位置:

${STEAM_CHAT_DATA_DIR}/auth.sqlite

建议表结构:

CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT NOT NULL UNIQUE,
  password_hash TEXT NOT NULL,
  role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
  disabled INTEGER NOT NULL DEFAULT 0,
  session_version INTEGER NOT NULL DEFAULT 1,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  last_login_at TEXT
);

CREATE TABLE app_meta (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

密码规则:

  • 用户名长度 3 到 64只允许字母、数字、下划线、短横线和点。
  • 密码最少 8 位。
  • 使用 crypto.scryptSync 加随机盐保存密码哈希。
  • 哈希字段保存为自描述格式,例如 scrypt:v1:<salt>:<hash>
  • 登录、重置密码、禁用、启用、角色变更时不记录明文密码。

首次访问时,如果 users 表为空,前端显示初始化管理员页面。初始化接口创建第一个启用的 admin 用户。之后该接口必须拒绝再次调用。

权限规则:

  • admin:可管理后台用户,可执行 Steam 登录、Steam Guard 提交、Steam 退出。
  • user:可使用已登录 Steam 会话聊天,可读取聊天所需的好友、群组、历史和媒体接口。
  • 不能删除当前登录用户自己。
  • 不能删除、禁用或降级最后一个启用的管理员。
  • 禁用用户后,该用户已有 Cookie 必须失效。

应用会话

使用签名 Cookie不引入服务端 session 表。

Cookie

  • 名称:steam_chat_session
  • 属性:HttpOnlySameSite=LaxPath=/
  • 过期时间:默认 7 天
  • HTTPS 反代下设置 Secure

签名密钥:

  • 首次启动时生成 32 字节随机密钥。
  • 保存在 app_meta.session_secret
  • 数据目录持久化后,容器重建不会导致所有用户退出。

Cookie payload 包含:

{
  "uid": 1,
  "role": "admin",
  "sv": 1,
  "iat": 1710000000000,
  "exp": 1710604800000
}

校验规则:

  • HMAC 使用 sha256
  • 每个受保护请求都根据 uid 查询数据库。
  • 用户不存在、被禁用、session_version 不匹配、Cookie 过期或签名不正确时返回 401。
  • 重置密码、禁用用户、启用用户、变更角色时递增 session_version,使旧 Cookie 失效。

HTTP API

所有响应使用 JSON。未认证返回 401权限不足返回 403。

公开接口:

  • GET /healthz
    • 仅表示应用进程和 HTTP 服务可用,不要求 Steam 已登录。
    • 返回:{ "ok": true }
  • GET /api/auth/me
    • 未登录也可访问。
    • 返回是否需要初始化、当前登录用户和 Steam 状态摘要。
  • POST /api/auth/setup
    • 仅在没有任何用户时可用。
    • 请求:{ "username": "admin", "password": "..." }
    • 成功后直接写入登录 Cookie。
  • POST /api/auth/login
    • 请求:{ "username": "...", "password": "..." }
    • 成功后写入登录 Cookie。
  • POST /api/auth/logout
    • 清除当前 Cookie。

登录用户接口:

  • POST /api/auth/password
    • 当前用户修改自己的后台密码。
    • 请求:{ "oldPassword": "...", "newPassword": "..." }

管理员用户管理接口:

  • GET /api/users
    • 返回用户列表,不返回密码哈希。
  • POST /api/users
    • 请求:{ "username": "...", "password": "...", "role": "user" }
  • PATCH /api/users/:id
    • 支持修改 roledisabled
  • POST /api/users/:id/password
    • 管理员重置指定用户密码。
  • DELETE /api/users/:id
    • 删除非自身用户,并执行最后管理员保护。

Steam 管理接口:

  • GET /api/steam/status
    • 返回当前 Steam 状态、是否需要 Guard、最近错误、当前 SteamID。
  • POST /api/steam/login
    • 仅管理员。
    • 请求:{ "accountName": "...", "password": "...", "logonID": 123 }
    • logonID 可选;缺省时后端生成稳定随机值并保存在数据目录配置中。
    • 如果需要 Steam Guard返回 waiting_guard
  • POST /api/steam/guard
    • 仅管理员。
    • 请求:{ "code": "ABCDE" }
    • 后端把验证码交给当前待处理的 Steam Guard callback。
  • POST /api/steam/logout
    • 仅管理员。
    • 调用 Steam logOff(),删除数据目录里的 refresh.token,状态变为 logged_out

聊天相关的既有接口和 WebSocket 协议保留,但必须先通过应用登录认证。未登录 Steam 时,依赖 Steam 的接口返回:

{
  "error": "Steam is not logged in",
  "steamStatus": "logged_out"
}

Steam 登录实现

将 Steam 生命周期从“构造时绑定固定 config”调整为“运行期按请求传入登录参数”。

后端新增 SteamLoginService,负责:

  • 启动时读取 ${STEAM_CHAT_DATA_DIR}/refresh.token 并尝试 token 登录。
  • 管理 Steam 状态机。
  • 监听 steamGuard(domain, callback, lastCodeWrong)
  • 保存待提交的 Guard callback并通过状态接口暴露 guardTypedomainlastCodeWrong
  • 登录成功后监听 refreshToken 事件并写入数据目录。
  • Steam 退出时删除 token。

Guard 类型规则:

  • domain 为字符串时表示邮箱码,前端展示邮箱域名。
  • domainnull 时表示手机 2FA。
  • lastCodeWrongtrue 时,前端提示等待新验证码后再提交,避免 2FA 循环。

安全要求:

  • 请求日志、错误日志、前端状态都不能输出 Steam 密码、后台密码、Steam Guard code、refresh token。
  • 同一时间只允许一个 Steam 登录流程。已有 logging_inwaiting_guard 时,再次登录请求返回 409。
  • 管理员手动重新登录前,应先调用 Steam 退出或等待当前登录流程结束。

前端重写

前端必须按新的后台业务模型完全重写。旧的 web/index.htmlweb/app.tsweb/style.css 只作为功能参考,不作为增量改造基础;实现时允许删除旧 DOM 结构、旧全局状态机和旧样式组织。

重写目标:

  • 首屏是后台应用入口,不是默认聊天界面。
  • 先解决后台初始化和后台登录,再进入业务工作台。
  • Steam 连接状态是后台工作台的一部分,而不是启动前置条件。
  • 聊天能力作为已登录后台用户的一个业务模块存在。
  • 管理员和普通用户看到的导航、按钮和操作必须按权限区分。

页面结构:

  1. 初始化页
    • 仅在没有任何后台用户时展示。
    • 创建第一个管理员,成功后直接进入后台。
  2. 后台登录页
    • 已初始化但未登录时展示。
    • 只处理后台用户登录,不处理 Steam 登录。
  3. 应用外壳
    • 登录后展示统一后台布局。
    • 包含侧边导航、顶部状态区、当前后台用户入口、退出登录入口。
  4. Steam 连接页
    • 管理员可提交 Steam 账号密码。
    • 需要 Steam Guard 时切换到验证码提交状态。
    • 普通用户只能查看当前 Steam 状态。
  5. 用户管理页
    • 仅管理员可见。
    • 支持新增用户、重置密码、启用/禁用、删除。
  6. 聊天页
    • 重新实现会话列表、好友/群组列表、消息区、输入区、图片发送、表情/贴纸选择。
    • Steam 未在线时展示不可用状态,并禁用发送、图片、表情等操作。

前端启动顺序:

  1. 请求 GET /api/auth/me
  2. 如果需要初始化,渲染初始化页。
  3. 如果未登录,渲染后台登录页。
  4. 如果已登录,进入应用外壳。
  5. 加载 GET /api/steam/statusGET /api/config
  6. 只有后台已登录后才建立 WebSocket。
  7. Steam 未在线时保留页面可访问,但禁用依赖 Steam 的交互。

实现约束:

  • v1 继续使用当前 TypeScript 静态构建链路,不强制引入前端框架或 bundler。
  • 可以把 web/app.ts 拆成多个 TypeScript 模块,但输出仍由 tsc 生成到 dist/web
  • 所有前端请求默认携带同源 Cookie401 时关闭 WebSocket 并回到后台登录页。
  • 前端状态中不能保存或打印 Steam 密码、后台密码、Steam Guard code、refresh token。
  • 移动端必须能完成初始化、后台登录、Steam Guard 提交和基础聊天。

测试要求

新增或调整以下测试:

  • 用户初始化:无用户时允许 setup有用户后拒绝 setup。
  • 登录认证:密码正确写 Cookie密码错误拒绝禁用用户拒绝。
  • Cookie 校验:签名错误、过期、session_version 不匹配都返回 401。
  • 用户管理:管理员可新增、重置密码、禁用、启用、删除;普通用户返回 403。
  • 最后管理员保护:不能删除、禁用或降级最后一个启用管理员。
  • Steam 状态:无 token 时服务启动且状态为 logged_out
  • Steam token 登录:存在 refresh.token 时启动后自动尝试登录。
  • Steam Guard触发 waiting_guard,提交验证码后继续登录,错误验证码保留状态。
  • 聊天接口:应用未登录返回 401应用已登录但 Steam 未登录返回 503Steam 在线后保持现有行为。
  • WebSocket未认证握手拒绝认证后可连接。

验收命令:

npm run typecheck
npm test