17 KiB
用户管理增强设计
背景
当前后台已经具备基础用户系统:
- 首次初始化管理员。
- 后台账号密码登录。
admin/user两级固定角色。- 用户启用、禁用、删除、重置密码。
- 签名 Cookie 会话和
session_version失效机制。
本设计是在 docs/backend-redesign.md 的用户系统基础上继续增强,不重做认证入口,不引入外部身份服务,也不把 Steam 密码、Steam Guard code 或 refresh token 写入配置文件。
目标
- 用户资料从“只有账号名”扩展为可运营的后台用户档案。
- 管理员可以看到用户状态、最近登录和最近活跃信息。
- 管理员可以审计关键管理动作。
- 管理员可以踢下线指定用户或使用户所有会话失效。
- 固定角色继续保留,但后端按权限点执行校验,避免业务逻辑散落判断角色字符串。
- 普通用户只能访问被授权的 Steam 账户,不能查看或操作未授权 Steam 账户的聊天、历史、好友、群组和素材。
- 为后续多 Steam 账户切换预留数据结构,但 v1 不实现多个 Steam 账户同时在线。
非目标
- 不支持自定义角色。
- 不支持头像、邮箱、手机号等用户资料。
- 不做审计日志导出。
- 不接入 LDAP、OAuth、OIDC 或外部 SSO。
- 不实现多个 Steam 会话并发在线;v1 仍保持一个运行时活动 Steam 会话。
- 不在数据库中保存 Steam 明文密码或 Steam Guard code。
角色和权限
角色保持固定:
admin:后台管理员。user:普通聊天用户。
后端引入权限点映射,业务代码调用权限点而不是直接散落判断角色:
| 权限点 | admin | user | 说明 |
|---|---|---|---|
user.manage |
是 | 否 | 新增、编辑、禁用、删除用户 |
session.manage |
是 | 否 | 查看和踢下线用户会话 |
audit.view |
是 | 否 | 查看审计日志 |
steam.manage |
是 | 否 | 登录、退出、切换 Steam 账户 |
steam.account.manage |
是 | 否 | 维护 Steam 账户资料和授权关系 |
chat.use |
是 | 是 | 使用聊天能力;普通用户还必须通过 Steam 账户授权 |
self.password.change |
是 | 是 | 修改自己的后台密码 |
admin 对所有 Steam 账户默认有访问权。user 只允许访问授权表中的 Steam 账户。
数据模型
继续使用 ${STEAM_CHAT_DATA_DIR}/auth.sqlite。启动时执行幂等迁移,旧库自动补齐字段和表。
users
在现有 users 表上增加字段:
ALTER TABLE users ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN note TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN created_by INTEGER;
ALTER TABLE users ADD COLUMN last_login_ip TEXT;
ALTER TABLE users ADD COLUMN last_seen_at TEXT;
ALTER TABLE users ADD COLUMN password_changed_at TEXT;
ALTER TABLE users ADD COLUMN force_password_change INTEGER NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN failed_login_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN locked_until TEXT;
字段说明:
display_name:后台展示名,可为空;为空时前端展示username。note:管理员备注,仅管理员可见。created_by:创建该用户的管理员 ID;初始化管理员为空。last_login_ip:最近一次登录成功的客户端 IP,按现有trustProxy规则解析。last_seen_at:最近一次通过认证请求的时间。password_changed_at:最近一次密码变更时间。force_password_change:管理员重置密码后可要求用户下次登录后先改密。failed_login_count/locked_until:登录失败和临时锁定状态。
user_sessions
当前签名 Cookie 是无状态会话,无法列出或单独踢下线。增强后改成“签名 Cookie + 会话表”:
CREATE TABLE IF NOT EXISTS user_sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
created_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
revoked_at TEXT,
revoked_by INTEGER,
ip TEXT,
user_agent TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_user_sessions_expires_at ON user_sessions(expires_at);
Cookie payload 增加 sid:
{
"sid": "base64url-random-id",
"uid": 1,
"role": "admin",
"sv": 1,
"iat": 1710000000000,
"exp": 1710604800000
}
校验规则:
- Cookie 签名、过期时间、
uid、role、session_version仍然校验。 - 额外查询
user_sessions.id = sid。 - 会话不存在、已撤销、已过期时返回 401。
- 每次认证成功节流更新
user_sessions.last_seen_at和users.last_seen_at。 - 管理员踢下线时设置
revoked_at和revoked_by。
steam_accounts
新增 Steam 账户资源表:
CREATE TABLE IF NOT EXISTS steam_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
steam_id TEXT NOT NULL UNIQUE,
label TEXT NOT NULL DEFAULT '',
account_name_hint TEXT NOT NULL DEFAULT '',
refresh_token TEXT,
refresh_token_updated_at TEXT,
enabled INTEGER NOT NULL DEFAULT 1,
created_by INTEGER,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_login_at TEXT,
last_active_at TEXT,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
);
字段说明:
steam_id:SteamID64,是授权和日志隔离的主键。label:管理员维护的显示名称,例如“客服一号”。account_name_hint:可选账号提示,只保存脱敏后的账号名或管理员输入的备注,不保存密码。refresh_token:该 Steam 账户的 refresh token,用于后续免密码连接;接口响应、审计日志和普通错误日志都不能返回该字段。refresh_token_updated_at:最近一次写入 refresh token 的时间。enabled:禁用后不能被连接,普通用户也不能访问。last_login_at/last_active_at:最近连接和活动时间。
Steam refresh token 写入 steam_accounts.refresh_token。本项目仍不保存 Steam 明文密码和 Steam Guard code;refresh token 只作为 Steam 账户资源的敏感字段保存在本地 SQLite 中。
user_steam_accounts
用户和 Steam 账户的授权关系:
CREATE TABLE IF NOT EXISTS user_steam_accounts (
user_id INTEGER NOT NULL,
steam_account_id INTEGER NOT NULL,
granted_by INTEGER,
granted_at TEXT NOT NULL,
PRIMARY KEY (user_id, steam_account_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (steam_account_id) REFERENCES steam_accounts(id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES users(id) ON DELETE SET NULL
);
规则:
- 新建普通用户默认没有 Steam 账户访问权。
- 管理员默认可访问所有 Steam 账户,不需要写授权行。
- 禁用 Steam 账户后,所有普通用户对该账户的访问立即失效。
- 删除 Steam 账户时同时删除授权关系和该账户记录中的 refresh token。
audit_logs
新增本地审计日志表:
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_user_id INTEGER,
action TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id TEXT NOT NULL,
detail_json TEXT NOT NULL DEFAULT '{}',
ip TEXT,
user_agent TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (actor_user_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
记录动作:
- 用户:创建、更新资料、启用、禁用、删除、角色变更、重置密码、强制改密状态变更。
- 会话:踢下线单个会话、踢下线用户全部会话、用户主动退出全部会话。
- Steam 账户:新增、更新、禁用、启用、删除、授权、取消授权、登录、退出、切换。
审计日志只记录必要元数据。密码、Steam Guard code、refresh token 必须脱敏或不进入 detail_json。
Steam 账户访问控制
当前运行模型
v1 仍只有一个活动 Steam 会话。后端维护当前活动账户:
app_meta.active_steam_account_id
所有依赖 Steam 账户上下文的接口都必须先解析当前活动账户:
- 校验后台用户会话。
- 读取当前 Steam 状态和
active_steam_account_id。 - 如果接口依赖 Steam 在线,继续要求 Steam 状态为
online。 - 校验当前用户是否可访问该 Steam 账户。
- 执行业务逻辑。
普通用户未被授权访问当前活动账户时返回 403:
{
"error": "Steam account access denied"
}
如果当前没有活动 Steam 账户,依赖 Steam 的接口返回 503:
{
"error": "Steam account is not connected",
"steamStatus": "logged_out"
}
需要保护的接口
以下接口必须校验 Steam 账户访问权:
GET /api/steam/statusGET /api/friendsGET /api/groupsGET /api/emoticonsGET /historyGET /conversationsGET /proxy/sticker/:typeGET /proxy/imagePOST /messagePOST /image- WebSocket 握手后的所有聊天、历史、好友、群组、素材消息类型
管理员管理接口仍只校验管理员权限:
- Steam 登录、退出、切换账户。
- Steam 账户维护。
- 用户 Steam 账户授权维护。
聊天历史隔离
现有聊天历史是 JSONL 文件。为了避免用户读取未授权账户历史,新增日志字段:
{
"steamAccountId": "7656119...",
"id": "target-steam-id",
"message": "..."
}
规则:
- 新写入的历史必须带
steamAccountId。 - 查询历史和会话列表时必须传入当前活动账户,并只返回该账户记录。
- 旧历史没有
steamAccountId,迁移期只允许管理员查看。 - 当前活动账户首次成功识别后,可以由管理员触发一次“认领旧历史到该 Steam 账户”的维护动作;v1 可以先不做自动认领。
后端 API
当前用户
GET /api/auth/me 增加字段:
{
"needsSetup": false,
"user": {
"id": 1,
"username": "admin",
"displayName": "管理员",
"role": "admin",
"disabled": false,
"forcePasswordChange": false
},
"permissions": ["user.manage", "steam.manage"],
"steam": {
"status": "online",
"steamId": "7656119...",
"activeAccount": {
"id": 1,
"steamId": "7656119...",
"label": "客服一号"
},
"accessAllowed": true
}
}
用户管理
新增或调整接口:
GET /api/users?query=&role=&status=- 管理员可用。
- 支持按账号、昵称、备注搜索。
status支持enabled、disabled、locked。
POST /api/users- 新增
displayName、note、steamAccountIds。
- 新增
PATCH /api/users/:id- 支持
displayName、note、role、disabled、forcePasswordChange。
- 支持
GET /api/users/:id/sessions- 返回该用户未过期且未撤销的会话。
DELETE /api/users/:id/sessions/:sessionId- 管理员踢下线单个会话。
DELETE /api/users/:id/sessions- 管理员踢下线用户全部会话。
GET /api/users/:id/steam-accounts- 查看普通用户已授权 Steam 账户。
PUT /api/users/:id/steam-accounts- 用完整
steamAccountIds覆盖授权关系。
- 用完整
保留现有接口:
POST /api/users/:id/passwordDELETE /api/users/:idPOST /api/auth/passwordPOST /api/auth/logout
新增当前用户退出全部会话:
POST /api/auth/logout-all- 撤销当前用户除当前请求外的所有会话,随后也可选择清除当前 Cookie。
Steam 账户管理
新增接口:
GET /api/steam/accounts- 管理员返回全部账户。
- 普通用户返回自己被授权且启用的账户。
POST /api/steam/accounts/login- 管理员用账号密码登录 Steam。
- 请求包含
accountName、password、可选logonID、可选label。 - 登录成功后用 SteamID64 upsert
steam_accounts,写入refresh_token,并设为当前活动账户。
POST /api/steam/accounts/:id/connect- 管理员用该账户
refresh_token连接 Steam。 - 成功后设为当前活动账户。
- 管理员用该账户
PATCH /api/steam/accounts/:id- 管理员更新
label、accountNameHint、enabled。
- 管理员更新
DELETE /api/steam/accounts/:id- 管理员删除账户资料、授权关系和表内 refresh token。
- 如果删除的是当前活动账户,必须先退出 Steam 或由后端自动执行退出。
POST /api/steam/accounts/:id/logout- 管理员退出当前活动账户;只允许操作当前活动账户。
兼容现有接口:
POST /api/steam/login可以保留为POST /api/steam/accounts/login的兼容入口。POST /api/steam/logout可以保留为退出当前活动账户的兼容入口。GET /api/steam/status返回当前活动账户信息和当前用户访问结果。
前端设计
导航
管理员导航增加:
用户管理Steam 账户审计日志
普通用户导航保持:
Steam 连接或状态页聊天账号
普通用户只看到自己可访问的 Steam 账户状态。未授权当前活动账户时,聊天页展示无权限状态,不加载好友、群组、历史和 WebSocket。
用户管理页
用户列表展示:
- 账号。
- 昵称。
- 角色。
- 启用、禁用、锁定状态。
- 已授权 Steam 账户数量。
- 最近登录时间和 IP。
- 最近活跃时间。
用户详情区支持:
- 修改昵称和备注。
- 修改角色。
- 启用、禁用。
- 重置密码。
- 要求下次登录改密。
- 管理 Steam 账户授权。
- 查看和踢下线会话。
Steam 账户页
管理员可查看:
- SteamID64。
- 显示名称。
- 当前是否活动。
- 是否启用。
- 最近登录时间。
- 被授权用户数量。
操作:
- 登录新的 Steam 账户。
- 用已保存 token 连接已有账户。
- 编辑显示名称和账号提示。
- 启用、禁用。
- 删除账户。
- 查看已授权用户。
审计日志页
v1 只提供后台查看,不提供导出:
- 按动作、目标类型、操作者、时间范围筛选。
- 展示时间、操作者、动作、目标、IP、简要详情。
- 敏感字段永远不展示。
迁移策略
- 启动时创建新表并给
users补齐新增字段。 - 引入
schema_version或在app_meta中记录迁移版本,迁移保持幂等。 - 现有 Cookie 没有
sid,升级后统一要求重新登录。 - 现有
${STEAM_CHAT_DATA_DIR}/refresh.token保留为兼容读取来源,首次成功登录并识别 SteamID 后写入steam_accounts.refresh_token和refresh_token_updated_at。 - token 迁移成功后删除旧路径
refresh.token,避免同一敏感凭据存在两份。 - 首个识别出的 Steam 账户自动创建
steam_accounts记录并设为当前活动账户。 - 普通用户默认不自动获得该账户访问权,由管理员显式授权。
- 旧聊天历史没有
steamAccountId,默认只允许管理员查看;后续可增加管理员手动认领工具。
安全要求
- 所有权限校验必须在后端执行,前端只做体验隐藏。
- 管理员不能删除或禁用最后一个启用管理员。
- 管理员不能删除当前登录用户自己。
- 用户被禁用、角色变更、重置密码、强制改密时,相关旧会话必须失效。
- 登录失败达到阈值后临时锁定账号;建议默认 5 次失败锁定 15 分钟。
- 审计日志和错误日志不得包含后台密码、Steam 密码、Steam Guard code、refresh token。
steam_accounts.refresh_token不得出现在任何 API 响应、前端状态、审计详情或结构化日志中。- Steam 账户授权失败统一返回 403,不暴露未授权账户的详情。
- 普通用户不能通过历史、会话列表、WebSocket 或媒体代理旁路读取未授权账户数据。
测试要求
新增或调整测试:
- 用户资料字段迁移和读写。
- 固定角色到权限点映射。
- 登录失败计数和锁定。
- 会话表校验、踢下线、退出全部会话。
- 用户禁用、角色变更、重置密码后旧会话失效。
- Steam 账户创建、禁用、删除和表内 refresh token 读写。
- 普通用户访问未授权 Steam 账户返回 403。
- 普通用户不能读取未授权账户历史、会话列表、好友、群组、素材和 WebSocket 数据。
- 管理员默认可访问所有 Steam 账户。
- 审计日志记录关键管理动作并脱敏敏感字段。
- 旧库迁移后已有管理员仍可登录。
验收命令:
npm run typecheck
npm test
建议落地顺序
- 数据迁移和权限点映射。
- 会话表和踢下线能力。
- 用户资料、登录安全和审计日志。
- Steam 账户表、授权关系和当前活动账户校验。
- 聊天历史增加
steamAccountId并按账户过滤。 - 前端用户详情、Steam 账户页和审计日志页。
- 清理兼容接口和补齐测试。