feat(M5): 前端完善 + 部署 — Vue3 全量页面、仪表盘统计与我的审计接口、systemd/sudoers/迁移脚本、部署文档

This commit is contained in:
2026-08-30 10:50:46 +08:00
parent c0e2ff975a
commit b305d7b371
67 changed files with 3209 additions and 171 deletions
+85
View File
@@ -0,0 +1,85 @@
# API 文档(RESTful v1
统一前缀 `/api/v1`;会话基于 cookieHttpOnly/Secure/SameSite`withCredentials`)。
成功响应 `{"data": ...}`;失败响应 `{"error": "..."}`HTTP 状态码语义化
400 参数/401 未登录/403 无权限/404 不存在/409 冲突/429 限速/500 内部)。
权限:`-` 公开,`admin` 管理员会话,`user` 外部用户会话。
## 认证 /auth
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | `/auth/captcha` | 图形验证码 `{captcha_id, image}`base64 PNG data URI | - |
| POST | `/auth/otp/send` | 外部用户:用户名+图形验证码 → 发 OTP 邮件(60s 冷却)。body: `{username, captcha_id, captcha_code}` | - |
| POST | `/auth/otp/login` | `{username, code}` → 建 cookie 会话 | - |
| POST | `/auth/admin/login` | `{username, password}` → 建 cookie 会话 | - |
| POST | `/auth/admin/forgot` | `{username}` → 发密码重置邮件(不存在也返回成功,防枚举) | - |
| POST | `/auth/admin/reset` | `{token, new_password}` → 重置密码 | - |
| POST | `/auth/logout` | 登出(删会话 + 清 cookie | admin/user |
| GET | `/auth/me` | 当前主体 `{user_type, id, username, email}` | admin/user |
## 用户 /usersadmin
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/users?page=&page_size=&status=&supervisor=` | 列表(分页 `{total, items}` |
| POST | `/users` | 创建。body: `{username(不含ext_), email, supervisor, purpose, ttl_days(0=默认)}``{id, username, status, expire_at}` |
| GET | `/users/:id` | 详情 |
| PATCH | `/users/:id` | 更新(可空字段省略)。body: `{email?, supervisor?, purpose?}` |
| POST | `/users/:id/disable` | 禁用(清空 authorized_keysSSH 立即失效) |
| POST | `/users/:id/enable` | 启用(按 DB 密钥恢复) |
| POST | `/users/:id/extend` | 延期。body: `{days(0=默认)}``{expire_at}`;已过期在回收期内可恢复 |
| DELETE | `/users/:id` | 删除并回收(系统账号+家目录+密钥,保留审计) |
| GET | `/users/:id/keys` | 用户密钥列表 |
## 我的 /meuser
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/me/keys` | 我的密钥列表 |
| POST | `/me/keys` | 上传公钥 `{name, public_key}`(格式/重复校验 + 同步 authorized_keys |
| PATCH | `/me/keys/:id` | 重命名 `{name}` |
| DELETE | `/me/keys/:id` | 吊销(立即失效) |
| GET | `/me/audit?page=&page_size=` | 我的审计痕迹(本人为操作者或对象) |
## 申请审批 /approvals
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| POST | `/approvals` | 提交申请 `{username, email, supervisor, purpose}`(不含 ext_ | - |
| GET | `/approvals?status=` | 列表(pending/approved/rejected | admin |
| POST | `/approvals/:id/review` | 审批 `{approve: bool, reason}`(拒绝必填理由) | admin |
## 审计 /auditadmin
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/audit?page=&page_size=&actor=&action=&resource_type=&resource_id=&since=&until=` | 查询(since/until 为 RFC3339 |
| GET | `/audit/export?since=&until=` | CSV 导出(UTF-8 BOM`Content-Disposition` 下载) |
## 系统设置 /settingsadmin
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/settings` | 全部设置项 `{items: [{key, value, overridden}]}`config 默认 + settings 覆盖) |
| PUT | `/settings` | 更新 `{key, value}`(白名单 key`policy.default_ttl` / `policy.recycle_period` / `policy.audit_retention`,值为时长) |
## 仪表盘 /adminadmin
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/admin/stats` | `{total, active, disabled, expired, expiring_soon[], pending_approvals}`(近 30 天待过期) |
## 其他
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | `/healthz` | 健康检查 `{status, version, uptime, go, timestamp}` | - |
## 审计动作一览
`auth.admin_login``auth.otp_send``auth.otp_login``auth.logout`
`user.create/update/disable/enable/extend/delete``key.create/rename/revoke`
`approval.submit/review``lifecycle.expire`(到期锁定)、`lifecycle.recycle`(回收删除)。
`detail` 为 JSON 字符串;`result``success|failed`
+176
View File
@@ -0,0 +1,176 @@
# 部署指南(M5
ws_usernode 交付形态:**单 Go 二进制**(前端已 go:embed 嵌入),配 systemd 服务 +
sudoers 白名单。可选 podman 容器镜像。本文件覆盖完整生产部署步骤与验证。
---
## 1. 构建
前置:Go ≥ 1.22、podman(构建前端需要容器,宿主机 Node 版本过旧无法直接构建)。
```bash
# 1) 构建前端并嵌入 go:embed(内部自动 podman 构建 dev 镜像 + npm install + vite build
make web-build
# 2) 编译含前端产物的单二进制
make build-embed # 产物 bin/usernode
```
> 代理环境:容器构建无法继承 proxychainsMakefile 通过 `--build-arg` 传递
> `HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY`。若宿主机 LD_PRELOAD 了
> proxychains(本环境如此),podman 拉取镜像会失败,需:
> `env -u LD_PRELOAD -u PROXYCHAINS_CONF_FILE make web-build`
验证二进制:`./bin/usernode --help`;健康检查 `curl localhost:8080/healthz`
---
## 2. systemd 部署(推荐)
### 2.1 运行用户与目录
```bash
sudo useradd -r -s /usr/sbin/nologin -d /var/lib/usernode usernode
sudo mkdir -p /var/lib/usernode /var/log/usernode /etc/usernode
sudo chown usernode:usernode /var/lib/usernode /var/log/usernode
```
### 2.2 安装二进制与配置
```bash
sudo install -o usernode -g usernode -m 0755 bin/usernode /usr/local/bin/usernode
sudo install -o root -g root -m 0644 config.example.toml /etc/usernode/config.toml
sudoedit /etc/usernode/config.toml
```
配置要点(生产):
```toml
[app]
env = "production"
base_url = "https://usernode.example.com" # 对外地址(邮件重置链接)
[server]
listen = "127.0.0.1:8080" # 置于反向代理后;或直接 0.0.0.0
trusted_proxies = ["127.0.0.1", "::1"]
[database]
driver = "mysql" # 生产 MySQL;小规模可 sqlite
dsn = "usernode:pass@tcp(127.0.0.1:3306)/usernode?charset=utf8mb4&parseTime=True&loc=UTC"
[smtp]
host = "smtp.example.com" # 邮件必配(OTP/审批/到期通知)
port = 587
username = "usernode"
password = "..." # 或 USERNODE_SMTP_PASSWORD 环境变量
from = "usernode@example.com"
[system]
sudo = true # 必须 true:经 sudo -n 白名单执行
dry_run = false # 必须 false
[audit]
archive_dir = "/var/lib/usernode/audit_archive" # 审计每日归档;留空不清理
```
### 2.3 sudoers 白名单
```bash
sudo cp deploy/sudoers.example /etc/sudoers.d/usernode
sudo visudo -c
```
白名单仅允许 `useradd/usermod/userdel/passwd/mkdir/chmod/chown/install`
(禁任意 shell),命令参数由程序内强校验。
### 2.4 初始化数据库与管理员
```bash
sudo -u usernode usermode migrate --config /etc/usernode/config.toml # 建表(先于 admin create
sudo -u usernode usernode admin create --config /etc/usernode/config.toml \
--username root --password '强密码' --email admin@example.com
```
### 2.5 安装服务并启动
```bash
sudo install -o root -g root -m 0644 deploy/usernode.service /etc/systemd/system/usernode.service
sudo systemctl daemon-reload
sudo systemctl enable --now usernode
sudo systemctl status usernode
journalctl -u usernode -f # 结构化日志(slog
```
### 2.6 反向代理(TLS 可选但建议)
nginx 示例:
```nginx
server {
listen 443 ssl;
server_name usernode.example.com;
# ssl_certificate / ssl_certificate_key ...
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
---
## 3. podman 容器(可选)
```bash
make image # 多阶段构建:node 构建前端 → Go 编译 → alpine 运行镜像
podman run -d --name usernode \
-p 8080:8080 \
-v usernode-data:/data \
-v /etc/usernode:/etc/usernode:ro \
-e USERNODE_SYSTEM_SUDO=true \
-e USERNODE_SYSTEM_DRY_RUN=false \
ws-usernode:0.3.0-m5
```
> 容器内系统账号操作:容器以 usernode 用户运行,需在宿主机(或特权容器内)
> 配置对应 sudoers;默认 `dry_run=true` 只打印计划命令,用于容器内演练。
> 容器内用户目录/系统账号与宿主机共享时,请自行评估 sudo 与挂载边界。
---
## 4. SQLite → MySQL 迁移
```bash
# 前置:MySQL 已建库、建账号;可先用指向 MySQL 的配置跑 usernode migrate 建表
MYSQL_DSN='usernode:pass@tcp(127.0.0.1:3306)/usernode?charset=utf8mb4&parseTime=True&loc=UTC' \
./deploy/migrate-sqlite2mysql.sh /path/to/usernode.db
```
脚本会导出 SQLite(结构+数据)、改写为 MySQL 兼容语法并导入,最后逐表比对行数。
详见脚本头部注释(含时间字段精度说明)。
---
## 5. 验收清单(对照 PLAN M5 验收标准)
- [ ] 管理员创建用户 → 外部用户图形验证码 + OTP(邮件或 CLI)登录 → 上传公钥 → SSH 登录服务器
- [ ] 吊销密钥立即失效;到期自动禁用并通知;回收期内可延期,超期自动回收(保留审计)
- [ ] 所有管理操作可审计、可查询、可 CSV 导出;每日归档(配置 archive_dir 后)
- [ ] SQLite 数据可迁移至 MySQLdeploy/migrate-sqlite2mysql.sh
- [ ] 单二进制部署(systemd + sudoers 白名单)通过
- [ ] podman 容器构建通过
## 6. 运维速查
| 场景 | 操作 |
|---|---|
| 重置管理员密码 | `sudo -u usernode usernode admin reset-password --config /etc/usernode/config.toml --username root --password 新密码` |
| 获取外部用户 OTP | `usernode user otp --config ... --username ext_xxx`(与邮件同码同效期) |
| 查看审计归档 | `ls /var/lib/usernode/audit_archive/` |
| 查看邮件失败记录 | 数据库 `mail_logs` 表 status='failed'(每 5 分钟自动重试) |
| 手动触发维护 | 重启服务触发 `@daily` 维护;或等待 `@every 6h` 补扫 |
| 备份 | SQLite:停服拷贝 db 文件;MySQLmysqldump |