Schema:
- New AdminUser model with bcrypt-hashed password (cost 10)
- Roles: admin (full) / operator (read-only ops)
- Status: active / disabled
- MustChangePassword flag forces first-login password change
Backend:
- store: Add admins [] + CRUD methods (ListAdmins strips PasswordHash)
- service: SeedDefaultAdminIfEmpty (uses env credentials on first run),
CreateAdmin, ChangePassword, ResetPassword, SetAdminStatus, DeleteAdmin
- middleware: JWT now carries user_id (UUID)
- api: login() uses bcrypt + updates last_login_at/ip, blocks disabled
- api: me() returns role + must_change_password
- api: new endpoints:
POST /api/me/password (self password change)
GET /api/admins
POST /api/admins (create)
POST /api/admins/:id/password (reset by admin)
POST /api/admins/:id/status (enable/disable)
DELETE /api/admins/:id (with self/last-admin guard)
Frontend:
- Login: must_change_password=true triggers forced change-password dialog
- Layout: admin dropdown shows role tag + 修改密码 / 退出登录
- New /admins page (admin only) with table + create/reset/status/delete
- Router guard hides /admins from non-admin accounts
- API client: Auth.changePassword, Admins.{list,create,resetPassword,setStatus,delete}
Security:
- PasswordHash stored as bcrypt $2a$10$... in db.json
- ListAdmins always returns PasswordHash=''; never leaks via API
- Login returns 403 for disabled accounts
Verified: 21/21 API tests + browser E2E (first-login forced change,
restart persistence, admin list without hash, role-based menu)
9.5 KiB
API 参考
所有 /api 路径(除 /login、/health)都需要鉴权。
鉴权
登录获取 JWT token(12 小时有效期):
POST /api/login
Content-Type: application/json
{"username": "admin", "password": "admin123"}
成功响应:
{"token": "eyJhbG...", "username": "admin"}
后续请求携带:
Authorization: Bearer <token>
下载 .ovpn 也支持通过 query 携带 token:
GET /api/instances/<iid>/users/<uid>/ovpn?host=vpn.example.com&token=<token>
通用响应
成功返回 JSON 对象或数组;失败:
{"error": "错误描述"}
HTTP 状态码:200/400/401/404/500。
1. 公共
健康检查
GET /api/health
→ 200 {"ok": true}
登录
POST /api/login
Content-Type: application/json
{"username": "admin", "password": "..."}
→ 200 {
"token": "eyJhbG...",
"username": "admin",
"role": "admin",
"must_change_password": false
}
→ 401 {"error": "用户名或密码错误"}
→ 403 {"error": "账号已被禁用"}
密码以 bcrypt 哈希存储在
data/db.json中。must_change_password=true表示首次登录(默认账号)需要立刻改密。
当前登录账号
GET /api/me
→ 200 {
"user_id": "uuid",
"username": "admin",
"role": "admin",
"must_change_password": false
}
修改自己密码
POST /api/me/password
{"old_password": "...", "new_password": "..."}
→ 200 {"ok": true}
→ 400 {"error": "原密码错误"} | {"error": "新密码至少 6 个字符"}
登出(前端清理 token)
POST /api/logout
→ 200 {"ok": true}
2. 管理员账号管理
仅 admin role 可访问。operator 访问会拒绝(由前端路由守卫 + 后端 handler 校验)。
列出账号(不含 hash)
GET /api/admins
→ 200 [AdminUser, ...] // PasswordHash 字段固定为空
新建账号
POST /api/admins
{"username": "alice", "password": "StrongPass123", "role": "operator"}
→ 200 AdminUser
重置密码(无需知道旧密码)
POST /api/admins/:id/password
{"new_password": "NewPass456"}
→ 200 {"ok": true}
启用/禁用
POST /api/admins/:id/status
{"status": "active"} | {"status": "disabled"}
→ 200 {"ok": true}
删除(不能删自己、不能删最后一个 admin)
DELETE /api/admins/:id
→ 200 {"ok": true}
→ 400 {"error": "不能删除自己"}
→ 400 {"error": "不能删除最后一个 admin 账号"}
3. 仪表盘
汇总
GET /api/dashboard
→ 200 {
"instances": 2,
"running": 1,
"users": 10,
"active_users": 9,
"online": 3,
"expiring_certs": 1,
"recent_audits": [...],
"recent_conn_logs": [...]
}
3. 实例
列表
GET /api/instances
→ 200 [Instance, ...]
详情
GET /api/instances/:id
→ 200 Instance
→ 404 {"error": "instance <id> not found"}
新建
POST /api/instances
{
"name": "prod",
"port": 1194,
"proto": "udp", // udp | tcp
"dev": "tun", // tun | tap
"subnet": "10.8.0.0/24",
"cipher": "AES-256-GCM", // 可选
"auth_digest": "SHA256", // 可选
"push_dns": "dhcp-option DNS 8.8.8.8\ndhcp-option DNS 1.1.1.1",
"push_routes": "192.168.1.0 255.255.255.0",
"extra": "",
"access_mode": "whitelist", // open(默认) | whitelist
"allow_networks": ["192.168.1.0/24", "10.0.0.0/8"] // CIDR 列表
}
→ 200 Instance
access_mode=whitelist时,服务端会生成client-connect.sh/client-disconnect.sh, 通过 OpenVPN--learn-address钩子在每次客户端连接/断开时修改 iptables FORWARD 链, 仅放行到 allow_networks 中网段的流量,其他内网访问被 REJECT。 allow_networks 为空表示完全隔离(最严格)。
更新
PUT /api/instances/:id
{...同上,id 在 URL 中}
→ 200 Instance
修改实例级
allow_networks后,所有用户的 ccd 文件会被自动重写。
删除(级联删除该实例下所有用户/证书)
DELETE /api/instances/:id
→ 200 {"ok": true}
启动
POST /api/instances/:id/start
→ 200 {"ok": true}
停止
POST /api/instances/:id/stop
→ 200 {"ok": true}
在线客户端(来自 status-version 3)
GET /api/instances/:id/online
→ 200 [
{
"CommonName": "alice",
"RealAddress": "203.0.113.10:54321",
"VPNAddress": "10.8.0.10",
"BytesRecv": 12345,
"BytesSent": 6789,
"ConnectedAt": "2026-08-09T12:34:56Z"
}
]
4. 用户
列表(可选按实例过滤)
GET /api/instances/:id/users
→ 200 [VPNUser, ...]
新建(自动签发证书)
POST /api/instances/:id/users
{
"username": "alice",
"real_name": "Alice",
"email": "alice@example.com",
"static_ip": "10.8.0.10", // 可选
"allow_networks": ["172.16.0.0/16"] // 可选,用户级白名单(在实例基础上叠加)
}
→ 200 VPNUser
修改(改 allow_networks / static_ip 等)
PUT /api/instances/:id/users/:uid
{...同上}
→ 200 VPNUser
修改用户的
allow_networks后,会重新生成 ccd 文件, 用户重新连接即可拿到新的 push route。
吊销
POST /api/instances/:id/users/:uid/revoke
→ 200 {"ok": true}
删除(同时删除证书与 .ovpn)
DELETE /api/instances/:id/users/:uid
→ 200 {"ok": true}
下载客户端配置
GET /api/instances/:id/users/:uid/ovpn?host=vpn.example.com
→ 200 application/octet-stream (.ovpn 文件)
5. 证书
到期清单
GET /api/certs
→ 200 [
{
"instance_id": "uuid",
"username": "alice",
"not_before": "2026-08-09T00:00:00Z",
"not_after": "2036-08-06T00:00:00Z",
"days_left": 3649
}
]
days_left < 0 表示已过期,< 30 在仪表盘会显示为"即将到期"。
6. 连接日志
GET /api/connlogs?instance=<id>
→ 200 [ConnectionLog, ...]
每条:
{
"instance_id": "uuid",
"common_name": "alice",
"real_ip": "203.0.113.10:54321",
"vpn_ip": "10.8.0.10",
"bytes_in": 12345,
"bytes_out": 6789,
"connected_at": "...",
"disconnected_at": null
}
7. 备份
列表
GET /api/backups
→ 200 [Backup, ...]
创建
POST /api/backups
{"note": "before upgrade"}
→ 200 Backup
恢复(覆盖现有数据)
POST /api/backups/:id/restore
→ 200 {"ok": true}
删除
DELETE /api/backups/:id
→ 200 {"ok": true}
8. 审计
GET /api/audits
→ 200 [AuditLog, ...] // 最近 500 条,倒序
每条:
{
"id": "uuid",
"time": "2026-08-09T12:34:56Z",
"user": "admin",
"action": "create_user",
"target": "alice",
"result": "ok",
"detail": "instance=<id>",
"ip": "127.0.0.1"
}
可能的 action:create_instance / update_instance / delete_instance /
start_instance / stop_instance / create_user / revoke_user /
delete_user / create_backup / restore_backup。
数据模型
Instance
{
"id": "uuid",
"name": "prod",
"port": 1194,
"proto": "udp",
"dev": "tun",
"subnet": "10.8.0.0/24",
"cipher": "AES-256-GCM",
"auth_digest": "SHA256",
"push_dns": "...",
"push_routes": "...",
"extra": "",
"access_mode": "open | whitelist",
"allow_networks": ["192.168.1.0/24", "10.0.0.0/8"],
"status": "running | stopped | error",
"pid": 12345,
"created_at": "...",
"updated_at": "..."
}
VPNUser
{
"id": "uuid",
"instance_id": "uuid",
"username": "alice",
"real_name": "Alice",
"email": "alice@example.com",
"enabled": true,
"static_ip": "10.8.0.10",
"allow_networks": ["172.16.0.0/16"],
"created_at": "...",
"revoked_at": null
}
Backup
{
"id": "uuid",
"created_at": "...",
"size": 12345,
"note": "...",
"filename": "backup-20260809-123456-abcd1234.tar.gz",
"includes": ["pki", "instances", "clients"]
}
curl 示例
完整流程(假设服务在 http://localhost:8089):
# 1. 登录
TOKEN=$(curl -s -X POST http://localhost:8089/api/login \
-H 'content-type: application/json' \
-d '{"username":"admin","password":"admin123"}' | jq -r .token)
# 2. 创建实例
curl -s -X POST http://localhost:8089/api/instances \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{
"name":"prod",
"port":1194,
"proto":"udp",
"dev":"tun",
"subnet":"10.8.0.0/24",
"cipher":"AES-256-GCM",
"auth_digest":"SHA256",
"push_dns":"dhcp-option DNS 1.1.1.1"
}'
# 3. 创建用户
IID=$(curl -s http://localhost:8089/api/instances \
-H "Authorization: Bearer $TOKEN" | jq -r '.[0].id')
curl -s -X POST http://localhost:8089/api/instances/$IID/users \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"username":"alice","email":"alice@example.com"}'
# 4. 拿 .ovpn
UID=$(curl -s http://localhost:8089/api/instances/$IID/users \
-H "Authorization: Bearer $TOKEN" | jq -r '.[0].id')
curl -s -o alice.ovpn \
"http://localhost:8089/api/instances/$IID/users/$UID/ovpn?host=vpn.example.com" \
-H "Authorization: Bearer $TOKEN"
错误码
| HTTP | 含义 |
|---|---|
| 400 | 请求参数错误或操作失败(响应体有 error 字段) |
| 401 | 未登录或 token 无效/过期 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
限制
- 单实例:Web 管理界面并发 100+ 连接无压力
- 单实例 OpenVPN:理论上限 1024 个并发客户端(受限于
topology subnet子网大小,可改用net30提高) - 审计日志最多保留 5000 条
- 连接日志最多保留 20000 条
- 备份文件不自动清理,需定期手动删除过期备份