Bundle
@weibaohui/user-management
dsh 插件 · 用户管理:给 dsh web 加登录门禁——未登录访问弹登录/注册页,首个注册者自动成为管理员;支持 TOTP 两步验证(验证器 App 扫码/手输密钥绑定);管理员可管理所有用户(删除、重置密码、角色调整、重置两步验证)并查看登录记录与访问记录,普通用户只能查看和修改自己的信息。
- Source
- weibaohui
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 7 hours ago
Readme
# @weibaohui/user-management
[](https://github.com/topics/dsh-plugin)
[](https://www.npmjs.com/package/@weibaohui/user-management)
**用户管理 + 登录门禁**:自带一个 HTTPS 网关(默认 `https://<本机IP>:19843`)挡在 dsh web 前面——未登录访问任何页面自动跳到登录/注册页,API 与 WebSocket 一律 401;内置管理员/普通用户两级角色,管理员管所有用户(新增、删除、禁用、重置密码、角色调整、IP 封禁、重置两步验证)并查看登录记录、访问记录、操作日志,普通用户只能查看和修改自己的信息。支持 **TOTP 两步验证**(Google Authenticator / 1Password 等验证器 App 扫码绑定,密码之后再加一道动态码)与**注册审批制**(自助注册默认待管理员启用)。

**两步验证演示**(登录动态码 + 扫码绑定,亮色 / 暗色):
| 亮色主题 | 暗色主题 |
|---|---|
|  |  |
## 核心功能
- **HTTPS 登录网关**:独立 `node:https` 监听器(默认端口 19843)反代 loopback 的 dsh web——所有请求先过会话校验,未登录的页面访问 302 跳 `/login`,API 请求、WebSocket 升级一律 401。登录页为插件自带的独立页面(登录 / 注册双 Tab),不依赖宿主前端,深浅色自适应
- **首个注册者即管理员**:系统内没有任何账号时,第一个注册的账号自动成为管理员,此后注册的都是普通用户
- **注册审批制(默认)**:`autoActivate` 关闭时,自助注册的账号(首个管理员除外)落为**禁用**状态——注册页提示「账号需管理员审核启用后方可登录」,管理员在用户表点「启用」即完成审批;开关打开则注册即激活直接登录。审批动作零新界面,复用用户表的禁用/启用按钮与状态徽章
- **用户管理(仅管理员)**:用户列表(角色 / 状态 / 两步验证 / 创建时间 / 最后登录)、**新增用户(可选管理员或普通用户角色)**、删除用户、重置密码(生成随机临时密码,仅展示一次)、提升 / 降级角色、**禁用 / 启用账号**(禁用立即踢出会话并拒绝登录)、**重置两步验证**(用户丢手机的恢复通道);最后一个管理员不可删、不可降、不可禁
- **TOTP 两步验证(RFC 6238,默认关闭,自行开启)**:设置页扫码(二维码)或手输密钥绑定验证器 App,输入一个实时动态码完成绑定;开启后登录 = 用户名 + 密码 + 6 位动态码(登录页动态码框**常驻选填**:未开启留空即登,开启的账号必须校验,填错行内提示);登录表单带「记住用户名」勾选项;动态码 **30 秒一换、一次一用**(±1 窗口容时钟漂移,用过的码立即作废);连续错 5 次锁定 60 秒防爆破(内存态,重启清零);关闭需输登录密码确认,管理员可对任意用户重置。零第三方依赖,兼容 Google Authenticator / 1Password / 微软验证器 / Aegis 等
- **侧栏用户身份**:dsh 侧栏左上角显示当前用户头像 + 用户名(适配侧栏收缩),点击弹出用户菜单——修改密码(改密后其他会话全部登出)、退出登录
- **三本审计账**:登录记录(登录 / 登录失败 / 登出 / 改密 / 注册 / 重置密码 / 角色变更 / 删除 / 禁用 / 封禁等)、访问记录(页面级访问)、操作日志(经过网关的每一次 API 调用与 WebSocket 连接,含方法 / 路径 / 响应状态 / 来源 IP),均支持按用户、类型、路径、状态过滤
- **IP 封禁**:封禁列表 + 日志 IP 列一键封禁——命中地址在会话校验之前就被 403 拒绝(登录页也看不到);回环地址与当前请求所用 IP 服务端拒绝封禁,防止把自己锁在门外
- **HTTPS 证书引导**:自动签发 100 年自签证书(SAN 覆盖本机全部 IP + 每个 IP 的 `sslip.io` / `nip.io` 通配 DNS 别名 + localhost);设置页「HTTPS 证书」页一键下载 PEM/DER、核对指纹、复制 macOS / Windows / Linux 导入命令——导入信任链后浏览器不再弹证书警告
- **会话持久**:7 天滑动过期,dsh 重启不掉线;HttpOnly Cookie,密码 scrypt 加盐哈希;认证与数据层零第三方依赖——TOTP 为 node:crypto 手写实现(过 RFC 6238 官方测试向量)、二维码为内置 MIT 编码器,仅证书签发用 `selfsigned`
## 安装
```bash
dsh plugin --profile web add @weibaohui/user-management -w
```
装完重启 `dsh web` 即生效。
## 使用
1. 装完重启后,通过网关访问:`https://<本机IP>:19843`(浏览器首次会提示自签证书警告,按下方「HTTPS 证书」引导导入信任链后消失)——**首个注册的账号就是管理员**
2. 管理入口:Web UI → **设置页 → 用户管理**,七个页签:用户 / 登录记录 / 访问记录 / 操作日志 / IP 封禁 / HTTPS 证书 / 两步验证
3. 管理员:用户表(新增用户 / 重置密码 / 角色调整 / 禁用启用 / 重置两步验证 / 删除)+ 四本审计/封禁台账 + 证书下载与信任引导 + 自己的两步验证
4. 普通用户:个人信息卡 + 修改密码 + 两步验证 + 自己的登录记录
5. **开启两步验证**:设置页「两步验证」页签(或个人信息卡)→ 开启 → 验证器 App 扫二维码(或手动输入密钥)→ 输入 App 显示的 6 位动态码 → 绑定完成;之后登录输用户名密码,在「两步验证码」框填入 App 现查的 6 位动态码(未开启的账号留空即可,不会报错);勾选「记住用户名」后下次登录自动带出用户名,密码交给浏览器密码管理器记忆
6. 侧栏左上角点头像/用户名:修改密码、退出登录
7. 数据与审计文件都在 `~/.dsh/user-management/`(`users.json` / `sessions.json` / `activity.jsonl` / `audit.jsonl` / `bans.json`,0600 权限,原子写;审计账本滚动保留最近 5000 条)
8. **IP 封禁按连接源地址(`remoteAddress`)判定**:本插件自带的网关不做代理改写,看到的即是客户端真实地址;若你在网关前面另加反代层,看到的将是反代的 IP
## 给其他插件:解析请求的用户身份
user-management 向宿主提供 cordis 服务 `user-management`,兄弟插件据此知道自己收到的请求是"谁"(归属定时任务/分享/编辑记录等场景)。
### 消费方式(host 半)
```js
// ⚠️ 不要把 'user-management' 写进静态 inject 数组 —— user-management 未安装时
// 你的插件会卡死激活。用运行时 inject,装了才有、没装就跳过:
module.exports = {
name: 'my-plugin',
inject: ['webServer'],
apply(ctx) {
ctx.inject(['user-management'], (scope) => {
const um = scope['user-management']
ctx.effect(() => ctx.webServer.register({
kind: 'prefix', path: '/my-plugin/api',
handler: async (req, res) => {
const user = await um.resolveRequest(req) // 解析请求 cookie
if (!user) return sendJson(res, 401, { error: '未登录' })
if (user.role !== 'admin') return sendJson(res, 403, { error: '需要管理员' })
// user.username / user.id 可用于操作归属
},
}), 'my-plugin: api')
})
},
}
```
### API
| 方法 | 说明 |
|---|---|
| `resolveRequest(req)` | 从请求的 `um_session` cookie 解析当前用户(推荐入口) |
| `resolveToken(token)` | 已自行取出 token 时的底层变体 |
返回 `UmUser`:`{ id, username, role: 'admin'|'user', disabled, totpEnabled, createdAt, lastLoginAt }`;以下情况一律返回 **`null`**:未登录、会话过期/伪造、用户被禁用或删除(禁用/删除即刻失效)、user-management 自身存储故障(不向消费方抛错)。服务一 provide 即可安全调用(内部等待存储就绪)。
### 浏览器端(client 半)不需要这个服务
页面里直接 `fetch('/user-management/api/session')` 即可,网关本地应答 `{ user: {...} | null }`(HttpOnly cookie 自动携带)。
### 边界
这只回答"这个请求是谁发的",**不构成数据隔离**——dsh 宿主的会话与工作区仍是全实例共享。另外身份可信的前提是 dsh web 只监听 loopback(本插件网关架构的默认要求):一旦有人绕过网关直连宿主端口,cookie 校验仍由 store 把关,但请保持宿主不对外暴露。
## Remote Gateway 配置
v0.4 起,本插件自带 HTTPS 远程访问网关:dsh web 留在 loopback(`127.0.0.1:3080`),网关(独立 `node:https` 监听器)反代到它——**网关是唯一对外入口,认证不可绕过**。配置走 `~/.dsh/settings.yaml` 的 `user-management:` 段(也支持设置页热生效)。
### 字段
| 字段 | 默认 | 说明 |
|---|---|---|
| `enabled` | `true` | 关掉则不启动网关监听器 |
| `autoActivate` | `false` | **注册审批开关**。`false`(默认,审批制):自助注册的账号(首个管理员除外)直接落为**禁用**状态,注册页提示等待管理员审核,管理员在用户表点「启用」即完成审批;`true`:注册即激活、直接登录(历史行为) |
| `listenHost` | `0.0.0.0` | 网关监听地址;`127.0.0.1` 仅本机可达 |
| `port` | `19843` | 网关 HTTPS 端口 |
| `sites[]` | `[]`(=自动) | 站点白名单 + 证书;**空 = 自动枚举本机所有 IP**(含 `sslip.io` / `nip.io` 别名)。配了 sites 与自动列表**合并**而非替换:见下方「场景 3」 |
| `sites[].hosts` | — | Host 白名单(域名/IP,支持 `*.example.com` 通配)。**不带 cert/key 的 site 的 hosts 合并进自签 site**(去重) |
| `sites[].cert` / `sites[].key` | `''` | PEM 文件路径(fullchain + privkey);**配了则保留为独立 SNI site**(按域名选证书),不配则按 hosts 自签 |
| `title` | `DSH 控制台` | 登录页标题 |
| `sessionDays` / `loginFailLimit` / `lockoutSeconds` / `maxBodyBytes` | `7` / `5` / `60` / `16384` | 预留字段(当前未接线:会话由 store 管、body 上限在 API 内);TOTP 防爆破锁定内置为**连续错 5 次锁 60 秒**(内存态,重启清零) |
### 场景 1:零配置(推荐 · 私网)
不配 `sites` → 网关自动枚举本机所有非 loopback IP(IPv4 + IPv6,含 Tailscale)填进 hosts,并签发 **100 年自签证书**(SAN 覆盖这些 IP + localhost);`listenHost` 默认 `0.0.0.0`。打开 `https://<本机任意 IP>:19843` → 信任自签证书 → 注册/登录(**首个访问者即管理员**)。
### 场景 2:域名 + 证书
```yaml
user-management:
listenHost: '0.0.0.0'
port: 19843
sites:
- hosts: ['dsh.example.com']
cert: '/etc/letsencrypt/live/dsh.example.com/fullchain.pem'
key: '/etc/letsencrypt/live/dsh.example.com/privkey.pem'
```
- `cert` / `key` 是 PEM 文件路径(fullchain + privkey,如 `certbot certonly -d <域名>` 申请);配了就加载你的证书,不配则按 hosts 自签。
- 多域名走多项 `sites`(每项自己的 `hosts` + `cert` + `key`),网关按 SNI 选证书。
### 场景 3:加一个不在本机网卡上的公网 IP(NAT)
服务器有个 NAT 进来的公网 IP(不在本机网卡上,不会被自动枚举到)。配 `sites`(**不配 cert/key**)即可把它**合并**进自签 site——本机 IP / LAN / Tailscale 访问不丢,公网 IP 也进自签证书 SAN:
```yaml
user-management:
listenHost: '0.0.0.0'
port: 19843
sites:
- hosts: ['111.228.30.150'] # NAT 公网 IP,不带 cert/key -> 合并进自签 site
```
- **合并而非替换**(v0.5.4+):配的 hosts 加到自动枚举列表里(localhost + 本机所有 IP + sslip/nip 别名 + 你配的 IP),自签证书 SAN 覆盖全部;带 cert/key 的 site 仍保留为独立 SNI site。
- 早期版本配 `sites` 会**替换**自动列表,本机 IP 全丢 → 本地/LAN/Tailscale 访问被 421。v0.5.4 起改为加法合并。
- 公网 IP 走自签证书仍有「不受信任 CA」警告(导入信任链后消失);要真正零警告,给 `<ip>.sslip.io` 等用 Let's Encrypt 签真证书(见「场景 2」写法)。
### 安全边界
- **零配置下首个访问者即管理员**——仅适用于可信私网(Tailscale 等);公网暴露前请:配 `sites` 白名单 + 用域名证书 + 先在 loopback(`https://127.0.0.1:19843`)注册首个 admin 再对外。
- 自签证书 100 年有效,持久化在 `~/.dsh/user-management/certs/`;**改了 hosts/SAN 后删旧证书文件重启才会重签**(否则复用旧证书保指纹稳定)。合并新增的公网 IP 同理:要让旧自签证书 SAN 补上该 IP,删 `localhost.crt` / `localhost.key` 重启即可重签。
- dsh web 始终留在 loopback,网关是唯一对外入口——直连 3080 绕不过认证。
## 安全边界(务必阅读)
- **门禁是"进门"级别**:进门之后,所有登录用户看到的是同一个 dsh 实例的会话与数据——本插件做的是"谁能访问",不是多用户数据隔离
- **拦截机制**:v0.4 起门禁是独立 HTTPS 网关监听器(`node:https`,见上文「Remote Gateway 配置」)——所有请求先过网关的会话校验(未登录 document 跳 `/login`,API/WS 返 401),通过后才反代到 loopback dsh web;不再依赖宿主 `webServer.server` 监听器重排(旧版 0.3 的 attachGate + 降级路由级网关已移除)
- **开放注册与审批**:注册始终开放(新账号均为普通用户)。默认为**审批制**(`autoActivate: false`):注册成功但账号处于禁用状态,无法登录,需管理员在「用户」表点「启用」放行(登录页会提示等待审核);首个注册的管理员不受此开关影响。开关打开时注册即激活直接登录。请勿将 dsh web 暴露给不受信任的网络
- **证书下载与信任引导**:网关免登录提供 `GET /user-management/api/cert`(PEM,`?format=der` 得 Windows 用的 .cer)与 `GET /user-management/api/cert-info`(SHA-256 指纹 / 有效期 / 覆盖名称)。设置页「HTTPS 证书」卡片一键下载 + 复制各系统导入命令。自签证书不会因 SAN 完整而不弹警告——**消除警告靠导入信任链**(导入后 IP / sslip.io / nip.io 三种访问方式全部干净);公网 IP 可用 Let's Encrypt 给 `<ip>.sslip.io` 签真证书实现真正零警告;**Tailscale 用户优先用 `tailscale cert` + `机器名.尾网名.ts.net`**(真 CA 证书、零警告),尾网 IP 的 SAN 别名仅作兜底
- **审计口径**:操作日志不记录请求体内容与静态资源;被门禁拒绝的请求(401/302)不记录,防止扫描刷屏;临时密码、明文密码永不落盘、不进日志
- **两步验证的边界**:TOTP 密钥以 base32 明文存于 `users.json`(0600 + 原子写)——动态码验证需要原文,无法像密码那样单向哈希,这是自托管实现的通行做法(Gitea 同款);验证码只在**密码正确后**才被要求,接口不会向未持正确密码者泄露某账号是否开启了两步验证;动态码一次一用 + 连错 5 次锁 60 秒;**丢失验证器**:由管理员在用户表「重置两步验证」恢复,最后的管理员丢手机且无第二个管理员时,需手动编辑 `users.json` 删除该用户的 `totpSecret` 字段并重启;开启两步验证不踢已有会话(已登录设备不受影响,管的是"下次进门")
## 常见问题
忘记管理员密码/用户名、IP 封错解封、证书警告、网关没起来、审计文件清理等常见问题的处理办法见 **[docs/FAQ.md](docs/FAQ.md)**。
## 联系我 :飞书群

Install
dsh plugin --profile web add github:weibaohui/user-management
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install weibaohui-user-management from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.