510 lines
17 KiB
Markdown
510 lines
17 KiB
Markdown
# 用户管理增强设计
|
||
|
||
## 背景
|
||
|
||
当前后台已经具备基础用户系统:
|
||
|
||
- 首次初始化管理员。
|
||
- 后台账号密码登录。
|
||
- `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` 表上增加字段:
|
||
|
||
```sql
|
||
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 + 会话表”:
|
||
|
||
```sql
|
||
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`:
|
||
|
||
```json
|
||
{
|
||
"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 账户资源表:
|
||
|
||
```sql
|
||
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 账户的授权关系:
|
||
|
||
```sql
|
||
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
|
||
|
||
新增本地审计日志表:
|
||
|
||
```sql
|
||
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 会话。后端维护当前活动账户:
|
||
|
||
```text
|
||
app_meta.active_steam_account_id
|
||
```
|
||
|
||
所有依赖 Steam 账户上下文的接口都必须先解析当前活动账户:
|
||
|
||
1. 校验后台用户会话。
|
||
2. 读取当前 Steam 状态和 `active_steam_account_id`。
|
||
3. 如果接口依赖 Steam 在线,继续要求 Steam 状态为 `online`。
|
||
4. 校验当前用户是否可访问该 Steam 账户。
|
||
5. 执行业务逻辑。
|
||
|
||
普通用户未被授权访问当前活动账户时返回 403:
|
||
|
||
```json
|
||
{
|
||
"error": "Steam account access denied"
|
||
}
|
||
```
|
||
|
||
如果当前没有活动 Steam 账户,依赖 Steam 的接口返回 503:
|
||
|
||
```json
|
||
{
|
||
"error": "Steam account is not connected",
|
||
"steamStatus": "logged_out"
|
||
}
|
||
```
|
||
|
||
### 需要保护的接口
|
||
|
||
以下接口必须校验 Steam 账户访问权:
|
||
|
||
- `GET /api/steam/status`
|
||
- `GET /api/friends`
|
||
- `GET /api/groups`
|
||
- `GET /api/emoticons`
|
||
- `GET /history`
|
||
- `GET /conversations`
|
||
- `GET /proxy/sticker/:type`
|
||
- `GET /proxy/image`
|
||
- `POST /message`
|
||
- `POST /image`
|
||
- WebSocket 握手后的所有聊天、历史、好友、群组、素材消息类型
|
||
|
||
管理员管理接口仍只校验管理员权限:
|
||
|
||
- Steam 登录、退出、切换账户。
|
||
- Steam 账户维护。
|
||
- 用户 Steam 账户授权维护。
|
||
|
||
### 聊天历史隔离
|
||
|
||
现有聊天历史是 JSONL 文件。为了避免用户读取未授权账户历史,新增日志字段:
|
||
|
||
```json
|
||
{
|
||
"steamAccountId": "7656119...",
|
||
"id": "target-steam-id",
|
||
"message": "..."
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- 新写入的历史必须带 `steamAccountId`。
|
||
- 查询历史和会话列表时必须传入当前活动账户,并只返回该账户记录。
|
||
- 旧历史没有 `steamAccountId`,迁移期只允许管理员查看。
|
||
- 当前活动账户首次成功识别后,可以由管理员触发一次“认领旧历史到该 Steam 账户”的维护动作;v1 可以先不做自动认领。
|
||
|
||
## 后端 API
|
||
|
||
### 当前用户
|
||
|
||
`GET /api/auth/me` 增加字段:
|
||
|
||
```json
|
||
{
|
||
"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/password`
|
||
- `DELETE /api/users/:id`
|
||
- `POST /api/auth/password`
|
||
- `POST /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、简要详情。
|
||
- 敏感字段永远不展示。
|
||
|
||
## 迁移策略
|
||
|
||
1. 启动时创建新表并给 `users` 补齐新增字段。
|
||
2. 引入 `schema_version` 或在 `app_meta` 中记录迁移版本,迁移保持幂等。
|
||
3. 现有 Cookie 没有 `sid`,升级后统一要求重新登录。
|
||
4. 现有 `${STEAM_CHAT_DATA_DIR}/refresh.token` 保留为兼容读取来源,首次成功登录并识别 SteamID 后写入 `steam_accounts.refresh_token` 和 `refresh_token_updated_at`。
|
||
5. token 迁移成功后删除旧路径 `refresh.token`,避免同一敏感凭据存在两份。
|
||
6. 首个识别出的 Steam 账户自动创建 `steam_accounts` 记录并设为当前活动账户。
|
||
7. 普通用户默认不自动获得该账户访问权,由管理员显式授权。
|
||
8. 旧聊天历史没有 `steamAccountId`,默认只允许管理员查看;后续可增加管理员手动认领工具。
|
||
|
||
## 安全要求
|
||
|
||
- 所有权限校验必须在后端执行,前端只做体验隐藏。
|
||
- 管理员不能删除或禁用最后一个启用管理员。
|
||
- 管理员不能删除当前登录用户自己。
|
||
- 用户被禁用、角色变更、重置密码、强制改密时,相关旧会话必须失效。
|
||
- 登录失败达到阈值后临时锁定账号;建议默认 5 次失败锁定 15 分钟。
|
||
- 审计日志和错误日志不得包含后台密码、Steam 密码、Steam Guard code、refresh token。
|
||
- `steam_accounts.refresh_token` 不得出现在任何 API 响应、前端状态、审计详情或结构化日志中。
|
||
- Steam 账户授权失败统一返回 403,不暴露未授权账户的详情。
|
||
- 普通用户不能通过历史、会话列表、WebSocket 或媒体代理旁路读取未授权账户数据。
|
||
|
||
## 测试要求
|
||
|
||
新增或调整测试:
|
||
|
||
- 用户资料字段迁移和读写。
|
||
- 固定角色到权限点映射。
|
||
- 登录失败计数和锁定。
|
||
- 会话表校验、踢下线、退出全部会话。
|
||
- 用户禁用、角色变更、重置密码后旧会话失效。
|
||
- Steam 账户创建、禁用、删除和表内 refresh token 读写。
|
||
- 普通用户访问未授权 Steam 账户返回 403。
|
||
- 普通用户不能读取未授权账户历史、会话列表、好友、群组、素材和 WebSocket 数据。
|
||
- 管理员默认可访问所有 Steam 账户。
|
||
- 审计日志记录关键管理动作并脱敏敏感字段。
|
||
- 旧库迁移后已有管理员仍可登录。
|
||
|
||
验收命令:
|
||
|
||
```bash
|
||
npm run typecheck
|
||
npm test
|
||
```
|
||
|
||
## 建议落地顺序
|
||
|
||
1. 数据迁移和权限点映射。
|
||
2. 会话表和踢下线能力。
|
||
3. 用户资料、登录安全和审计日志。
|
||
4. Steam 账户表、授权关系和当前活动账户校验。
|
||
5. 聊天历史增加 `steamAccountId` 并按账户过滤。
|
||
6. 前端用户详情、Steam 账户页和审计日志页。
|
||
7. 清理兼容接口和补齐测试。
|