TunnelMesh 管理后台国际化、账号管理与 UI 设计
TunnelMesh 管理后台国际化、账号管理与 UI 设计
1. 背景与目标
当前管理后台已具备登录、Agent、路由、隧道、Token 和审计等基础页面,但页面缺少统一布局与视觉规范,用户可见文本主要硬编码为英文,也没有当前用户改密和子账号管理入口。
本期目标是:
- 保留 Vue 3、TypeScript、Element Plus、Pinia 和 Vue Router 技术栈;
- 建立参考 Ant Design Vue 信息架构的浅色 SaaS 管理台视觉体系;
- 第一阶段支持简体中文和英文,按浏览器语言自动识别并允许用户切换;
- 支持当前登录用户修改自己的密码;
- 支持管理员创建、查询、启用、禁用、重置密码和删除普通子账号;
- 保持既有 RBAC、资源所有权、安全审计和敏感信息保护边界。
这是应用 MINOR 级功能迭代。users 表新增可空 deleted_at,Schema 从 v5 升级到 v6;发布必须同时提供 MySQL 5.6 与 SQLite 的相邻版本增量脚本和更新后的全量 DDL。
2. 范围与非目标
2.1 本期范围
- 全部现有后台页面的统一 App Shell、设计 token 和响应式样式;
- 全部现有后台页面及新增页面的中文、英文翻译;
- 浏览器语言识别、语言切换、语言偏好本地持久化;
- 当前用户修改密码;
- 管理员管理普通子账号;
- Dashboard 权限范围内的准确资源摘要;
- 新增 API、OpenAPI、帮助文档、单元测试、集成测试和前端测试。
2.2 非目标
- 不引入 Ant Design Vue,不替换 Element Plus;
- 不增加组织、租户、部门或多级账号树;
- 不允许自定义角色、权限点或把普通用户提升为管理员;
- 不实现强制首次登录改密、密码过期、MFA 或邮件找回;
- 修改或重置密码后不撤销已有登录 Token;
- 不在前端保存、缓存或重复展示临时明文密码;
- 不改变 Agent、Client、server-node service token 的认证模型。
3. 已确认的产品行为
3.1 语言
支持 zh-CN 和 en-US。初始语言按以下优先级选择:
- localStorage 中用户已明确选择的受支持语言;
- 按顺序扫描
navigator.languages,任何zh-*映射到zh-CN; - 其他语言映射到
en-US。
登录页和登录后的顶部栏均提供语言切换。切换立即更新业务文案、Element Plus 内置组件、日期时间格式和校验提示,并写入 localStorage。语言偏好属于浏览器本地 UI 设置,不写数据库。
3.2 当前用户修改密码
所有已登录用户均可在“个人设置 / 安全设置”中修改自己的密码。请求必须包含当前密码和新密码,前端另行要求确认新密码。服务端重新校验当前密码并使用 Argon2id 保存新哈希。
修改成功后:
- 已有登录 Token 保持有效,包括发起操作的当前会话;
- 后续新登录必须使用新密码;
- 写入不含密码或哈希的审计记录;
- UI 显示成功提示,不跳回登录页。
账号被管理员禁用后,ValidateToken 每次鉴权读取用户状态,因此既有 Token 也不能继续通过鉴权。
3.3 子账号管理
第一期“子账号”是 role=user 的普通用户,不新增父子关系字段。管理员可以管理所有普通用户,但不能通过这些接口创建、修改、禁用、重置或删除管理员账号,也不能通过接口修改角色。
创建子账号时管理员只输入用户名。服务端生成高强度随机临时密码,响应只返回一次并设置 Cache-Control: no-store。前端使用一次性密码弹窗展示;弹窗关闭、路由离开或组件卸载时清空内存变量。
管理员重置子账号密码时,服务端生成新的高强度随机临时密码并只返回一次。重置不撤销该账号已有登录 Token。
管理员可以启用或禁用普通账号。禁用账号立即阻止新登录及已有 Token 的后续鉴权,但不删除其业务资源。
删除普通账号采用逻辑删除:设置 deleted_at=当前 UTC 时间 且 disabled=1,保留用户、登录 Token、service token、Agent、路由、隧道和审计记录。已删除账号不能登录,已有登录 Token 与其拥有的 service token 在后续鉴权时也必须失效。
管理员可以在“已删除”筛选中恢复普通账号。恢复时清空 deleted_at 并设置 disabled=0,原用户名和关联资源继续使用。用户名唯一约束继续覆盖已删除记录,因此删除后的用户名不能被新账号复用。逻辑删除和恢复都不能作用于管理员账号。
4. 架构设计
4.1 后端分层
新增独立 AccountService,避免把账号生命周期继续堆入登录认证职责:
Account HTTP Handler
↓
AccountService
↓
UserRepository / AuditRepository
Handler 只负责:
- 解析和限制 JSON 请求体;
- 获取已认证 Principal;
- 执行管理员或本人边界检查;
- 将领域错误映射到 HTTP 状态和统一响应结构。
AccountService 负责:
- 用户名与密码策略校验;
- 当前密码验证;
- Argon2id 哈希和随机临时密码生成;
- 禁止管理管理员账号;
- 账号状态变更;
- 逻辑删除和恢复;
- 事务编排和结构化审计。
Repository 负责 SQL、cursor 分页、锁和事务绑定。Handler 和 Service 不直接执行 SQL。
4.2 前端模块
前端新增以下边界:
i18n/:locale 解析、偏好持久化、翻译资源和翻译 key 类型;layouts/AppShell.vue:顶部栏、侧栏、面包屑、移动端抽屉和内容区域;components/PageHeader.vue:统一页面标题、说明与操作区;components/StatusTag.vue:统一状态色和标签;components/OneTimePasswordDialog.vue:创建/重置后的临时密码展示与内存清理;views/Users.vue:管理员子账号列表与管理动作;views/AccountSecurity.vue:当前用户修改密码;stores/preferences.ts:仅管理语言和导航折叠等本地 UI 偏好;styles/:颜色、字号、间距、阴影、圆角和响应式断点。
认证 Store 继续只管理当前用户和登录 Token。账号列表和修改操作通过 API client 调用,不把临时密码放入 Pinia 或 localStorage。
5. API 设计
所有接口使用 /api/v1 和统一响应 { code, msg, data }。公开用户对象只包含 id、username、role、disabled、deletedAt、createdAt、updatedAt,永不返回 passwordHash。
5.1 修改当前用户密码
PUT /api/v1/auth/password
Authorization: Bearer <token>
Content-Type: application/json
{
"currentPassword": "...",
"newPassword": "..."
}
成功返回公开用户信息或空数据。已有登录 Token 不撤销。错误语义:
400:字段缺失或新密码不符合策略;401:未登录;403:当前密码错误。
5.2 查询子账号
GET /api/v1/users?cursor=<cursor>&limit=<limit>&status=<active|deleted|all>
仅管理员可用,使用现有 cursor 分页语义。status 默认 active。只返回 role=user 的普通账号,避免在该页面暴露或误操作管理员账号。
5.3 创建子账号
POST /api/v1/users
Content-Type: application/json
{
"username": "operator-cn"
}
服务端固定创建 role=user,生成临时密码。响应设置 Cache-Control: no-store:
{
"code": 201,
"msg": "Created",
"data": {
"user": { "id": "...", "username": "operator-cn", "role": "user", "disabled": false },
"temporaryPassword": "one-time-value"
}
}
重复用户名返回 409。该接口不持久化明文响应;如果创建成功但响应丢失,管理员可通过账号列表确认用户并执行一次密码重置。
5.4 启用或禁用子账号
PATCH /api/v1/users/{userId}
Content-Type: application/json
{ "disabled": true }
只接受 disabled 字段。重复设置相同目标状态应成功并返回当前公开用户,保证自然幂等。目标不存在返回 404,目标是管理员返回 403。
5.5 重置子账号密码
POST /api/v1/users/{userId}/reset-password
仅管理员可操作普通账号。服务端生成临时密码、更新哈希并返回一次,响应设置 Cache-Control: no-store。不撤销已有登录 Token。目标不存在返回 404,目标是管理员返回 403。
5.6 删除和恢复子账号
DELETE /api/v1/users/{userId}
仅管理员可操作普通账号。删除设置 deleted_at 和 disabled=1,不物理删除任何账号或关联记录。重复删除保持逻辑删除状态并返回当前公开用户。
POST /api/v1/users/{userId}/restore
恢复清空 deleted_at 并设置 disabled=0。重复恢复保持有效状态并返回当前公开用户。目标不存在返回 404,目标是管理员返回 403。
5.7 Dashboard 摘要
GET /api/v1/dashboard/summary
返回调用者权限范围内的准确聚合:Agent 总数和在线数、活动隧道数、托管路由数、有效 service token 数以及最近非敏感事件。管理员查询全局,普通用户按 owner 过滤。聚合在 Repository 内完成,禁止读取全表后在 Handler 中计数。
6. 校验与错误模型
用户名规范:
- 3–64 个 ASCII 字符;
- 只允许字母、数字、点、下划线和连字符;
- 去除首尾空白后校验;
- 唯一性由数据库约束保证。
用户设置的新密码规范:
- 12–128 个 Unicode 字符;
- 不得与当前密码相同;
- 服务端是最终校验边界,前端只提供即时反馈。
服务端生成的临时密码使用 crypto/rand,熵不低于现有管理员随机凭据。明文只存在于请求生命周期和前端弹窗内存中。
新增账号 API 使用稳定领域错误标识,例如:
username_invalid;username_conflict;password_policy_violation;current_password_invalid;admin_account_protected;account_deleted。
前端按领域错误标识显示本地化文案。未知错误显示本地化通用提示并保留 HTTP 状态用于排障,不直接展示 SQL、DSN 或服务端内部错误。
7. UI 与视觉规范
保留 Element Plus,参考 Ant Design Vue 的信息层级与密度:
- 页面背景
#f5f7fa,内容卡片白色; - 主色使用清晰的 SaaS 蓝色,成功、警告、危险遵循一致状态色;
- 顶部栏 48–56px,桌面侧栏约 200px;
- 页面使用面包屑、标题、辅助说明和右侧主操作;
- 卡片使用轻边框和小圆角,避免厚重阴影;
- 表格紧凑但保持可读性,操作按主次排序;
- 空状态、加载骨架、失败重试和危险确认均使用统一组件;
- 低于移动端断点时侧栏变为抽屉,表格允许横向滚动,关键操作不隐藏。
Dashboard 只展示真实聚合值。接口失败时对应区域显示不可用和重试,不用 0 或静态数字掩盖错误。
8. 国际化设计
使用 vue-i18n,不维护自制字符串替换器。翻译资源按领域拆分:
common
auth
navigation
dashboard
users
agents
routes
tunnels
tokens
audits
中文和英文必须具有相同 key 集合。用户可见标题、按钮、表单标签、占位符、状态、确认框、成功/失败提示、空状态和校验提示全部通过翻译 key 获取。协议名、ID、用户名、域名和后端数据保持原值。
日期时间统一通过 locale-aware formatter 渲染。测试使用固定时区或只断言语义,避免依赖开发机时区。
9. 安全与审计
审计动作至少包括:
account.created;account.enabled;account.disabled;account.password_changed;account.password_reset;account.deleted;account.restored。
审计 details 只包含目标用户 ID、用户名和非敏感状态,不包含当前密码、新密码、临时密码或密码哈希。
一次性密码 API 必须设置 Cache-Control: no-store。前端不得将临时密码写入日志、错误监控、URL、Pinia、localStorage、sessionStorage 或剪贴板以外的持久位置。复制操作由用户显式触发。
逻辑删除必须同时设置 disabled,确保已有登录 Token 在下一次鉴权时立即失效。service token 鉴权也必须将 owner 的 deleted_at/disabled 作为不可用状态。外部输入全部在 Handler 边界限制大小,并在 Service 再做领域校验。
10. 测试策略
10.1 Go
AccountService单元测试:用户名、密码策略、本人改密、管理员保护、随机密码、启禁用、逻辑删除、恢复和审计脱敏;- Repository contract 测试:SQLite 与 MySQL 的 active/deleted/all 分页、逻辑删除和恢复;
- HTTP API 测试:未认证、非管理员越权、管理员成功路径、404、筛选、恢复、
no-store和统一响应; - Dashboard 聚合测试:管理员全局与普通用户 owner 过滤;
- 日志和响应断言:不得包含密码、密码哈希、Authorization 或 DSN。
10.2 前端
- 浏览器语言识别与 fallback;
- 手动切换和 localStorage 持久化;
- 中文、英文 key 集合一致;
- Element Plus locale 与业务 locale 同步;
- 登录页、导航和所有现有页面不残留硬编码用户可见文本;
/users管理员路由和普通用户拒绝;/account/security当前用户改密表单;- 创建、重置、启禁用、逻辑删除、已删除筛选和恢复;
- 一次性密码弹窗关闭和组件卸载后清理;
- 响应式导航与关键页面渲染。
11. 文档与发布
同步更新:
docs/api/openapi.yaml;docs/README.md;docs/user-guide/server-admin.md;- 管理员恢复与账号排障文档;
- 前端构建和 embed 同步说明(如命令发生变化)。
发布顺序:先备份数据库并执行/验证 v5→v6 增量迁移,再发布向后兼容的新后端 API,最后发布新前端资源。前端构建后清理并同步 web/dist/ 到 internal/server/web_dist/,确保 Go embed 不残留旧哈希资源。
回滚优先回滚前端和应用;v6 的可空新增列对旧应用向后兼容,回滚应用时保留该列,不自动执行破坏性反向 DDL。灰度阶段重点观察迁移结果、登录失败率、账号 API 4xx/5xx、前端资源加载失败和管理 API P99。
12. 验收标准
- 中文浏览器首次打开显示中文,非中文浏览器显示英文;手动切换刷新后保持;
- 所有现有和新增页面均有完整中文、英文文案;
- 当前用户可验证旧密码后修改密码,已有 Token 保持有效;
- 管理员可完整管理普通子账号,不能管理管理员账号或修改角色;
- 创建和重置只展示一次服务端随机密码,响应和前端均不缓存;
- 子账号可逻辑删除和恢复;删除期间保留全部关联资源,但账号及其 Token 不再通过鉴权;
- Dashboard 显示权限范围内的真实数据;
- 页面符合已确认的 Element Plus 浅色 SaaS 风格并支持窄屏;
- v5→v6 的 MySQL 5.6/SQLite 增量脚本、全量 DDL、OpenAPI、用户文档、Go 测试、前端测试、构建、race、vet、diff 检查和 embed 校验全部通过。