Files
NixMsg/docs/api/admin-api.md
T

734 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 管理接口契约(Admin API)
| 项 | 内容 |
|---|---|
| 版本 | 0.1(T0.4 契约) |
| 对应 | [DEVELOPMENT.md](../DEVELOPMENT.md) 第 8 节、[PRD.md](../PRD.md) F17 |
| 读者 | 后台接口 A、后台网页 W、集成测试 |
本文约定管理后台 HTTP 接口的方法、路径、请求字段、成功响应、分页与错误格式。W 线可先按本文用假数据开发;A 线实现时响应字段名与语义须与本文一致。
**硬性规则**
- 所有 JSON 响应在序列化前去掉消息正文;响应中不得出现 `body` 字段(含嵌套)。
- 管理接口不开跨域。
- 时间字段为 Unix 毫秒(整数),JSON 字段名蛇形。
---
## 1. 鉴权与通用约定
### 1.1 Cookie 会话(网页登录)
| 项 | 值 |
|---|---|
| Cookie 名 | `nixmsg_admin` |
| 属性 | `HttpOnly`;`SameSite=Lax`;经 HTTPS(含受信任代理认定的 HTTPS)时加 `Secure` |
| 服务端 | 只存令牌哈希;默认有效 12 小时 |
改变状态的请求(POST / PATCH / PUT / DELETE)在使用 Cookie 鉴权时,必须带请求头:
```http
X-Nixmsg-Request: 1
```
缺少该头返回 **403**(防跨站表单)。`GET` 不要求此头。
### 1.2 API 令牌(程序调用)
```http
Authorization: Bearer nxm_...
```
- 带 Bearer 令牌时**不看** Cookie,也**不要求** `X-Nixmsg-Request`。
- 权限等同管理员,但不能调用:
- `POST /api/admin/password`
- `/api/admin/tokens` 及其子路径
- 令牌不过期;停用或删除后立即失效。
- 错误令牌按来源 IP 计入管理员登录锁定。
### 1.3 通用响应信封
成功(业务成功):
```json
{"ok": true, "data": { }}
```
失败:
```json
{"ok": false, "error": {"code": "unauthorized", "message": "未登录"}}
```
| HTTP | 常见 code | 说明 |
|---|---|---|
| 400 | `bad_request` | 字段不合法 |
| 401 | `unauthorized` | 未登录、密码错误、令牌无效 |
| 403 | `forbidden` | 缺 `X-Nixmsg-Request`、令牌无权访问该路由、已锁定等 |
| 404 | `not_found` | 资源不存在 |
| 409 | `id_taken` / `conflict` | 编号占用等 |
| 429 | `rate_limited` | 登录/令牌试错锁定 |
| 500 | `internal` | 服务器错误 |
| 503 | `busy` | 暂时不可用,可重试 |
列表类成功时 `data` 形如:
```json
{
"items": [ ],
"next_cursor": "",
"total": 0
}
```
- `next_cursor` 为空表示没有下一页。
- `total` 为符合筛选条件的总数(可选实现;契约要求有该字段,假数据可给 `items.length`)。
- 默认 `limit` 为 50,最大 200(除非单接口另有说明)。
### 1.4 操作日志
每个改变状态的请求写结构化日志:操作者(`admin` 或 `token:<名称>`)、动作、对象编号、结果、来源 IP。不写密码、令牌和正文。
---
## 2. 认证与账号
### 2.1 登录
`POST /api/admin/login`
不需要已登录。不要求 `X-Nixmsg-Request`。
请求:
```json
{"username": "admin", "password": "..."}
```
- `username` 固定为 `admin`。
- 成功时设置 Cookie `nixmsg_admin`。
成功 `data`:
```json
{"username": "admin"}
```
失败:密码错误 `401 unauthorized`;锁定 `429 rate_limited`。
### 2.2 登出
`POST /api/admin/logout`
作废当前会话 Cookie。成功 `data` 可为 `{}`。
### 2.3 当前管理员
`GET /api/admin/me`
成功 `data`:
```json
{"username": "admin", "auth": "cookie"}
```
`auth` 为 `cookie` 或 `token`(API 令牌访问时)。
### 2.4 修改管理员密码
`POST /api/admin/password`
**仅 Cookie 会话**;API 令牌调用返回 `403 forbidden`。
请求:
```json
{"old_password": "...", "new_password": "..."}
```
- 新密码至少 12 位。
- 成功后可选择保持当前会话或全部作废(实现默认:当前会话保留,其它会话作废)。成功 `data`:`{}`。
---
## 3. 概览
`GET /api/admin/overview`
成功 `data`:
```json
{
"version": "0.1.0",
"endpoints_total": 0,
"endpoints_online": 0,
"endpoints_disabled": 0,
"groups_total": 0,
"messages_pending": 0,
"messages_scheduled": 0,
"uptime_ms": 0
}
```
不含正文、不含端编号明细。
---
## 4. 端(endpoints)
### 4.1 列表
`GET /api/admin/endpoints`
查询参数:
| 参数 | 说明 |
|---|---|
| `cursor` | 分页游标 |
| `limit` | 默认 50,最大 200 |
| `source` | 可选:`admin` / `self` |
| `online` | 可选:`true` / `false` |
| `enabled` | 可选:`true` / `false` |
| `query` | 可选:编号前缀或名称包含,不区分大小写 |
成功 `data.items[]`:
```json
{
"id": "device-1",
"name": "门口",
"remark": "",
"source": "admin",
"enabled": true,
"online": false,
"online_since_ms": null,
"offline_since_ms": 1750000000000,
"talk_password_set": false,
"default_delay_ms": 0,
"created_at_ms": 1750000000000,
"login_locked": false
}
```
无密码、无正文。
### 4.2 开通
`POST /api/admin/endpoints`
请求:
```json
{
"id": "",
"name": "门口",
"remark": "",
"login_password": "",
"talk_password": "",
"default_delay_seconds": 0
}
```
字段规则同 PRD F01(编号小写、密码不以 `nst_` 开头等)。`id` / `login_password` 留空则服务器生成。
成功 `data`:
```json
{
"id": "e_ab12cd34",
"login_password": "只在本次生成时返回"
}
```
冲突:`409 id_taken`。
### 4.3 批量开通(CSV)
`POST /api/admin/endpoints/import`
- `Content-Type: text/csv` 或 `multipart/form-data`(字段名 `file`)。
- UTF-8,可带 BOM。
- 表头:`id,name,login_password,talk_password,default_delay_seconds,remark`
- 最多 1000 行。先整体校验,任一行出错则一行都不建。
校验失败(HTTP 400):
```json
{
"ok": false,
"error": {"code": "bad_request", "message": "CSV 校验失败"},
"data": {
"errors": [{"line": 3, "reason": "编号不合法"}]
}
}
```
(实现可将 `errors` 放在 `error` 旁的 `data`;W 线按 `data.errors` 读取。)
成功 `data`:
```json
{
"items": [
{"id": "e_1", "login_password": "生成或原文(仅此一次)", "name": "..."}
]
}
```
### 4.4 多选批量
`POST /api/admin/endpoints/batch`
请求:
```json
{"ids": ["a", "b"], "action": "disable"}
```
`action`:`disable` | `enable` | `delete`。
成功 `data`:
```json
{"ok_ids": ["a"], "failed": [{"id": "b", "code": "not_found"}]}
```
### 4.5 详情
`GET /api/admin/endpoints/{id}`
成功 `data` 同列表项,可额外含:
```json
{"session_issued_at_ms": null, "session_used_at_ms": null}
```
仍无密码哈希与正文。
### 4.6 修改
`PATCH /api/admin/endpoints/{id}`
请求(皆可选):
```json
{
"name": "新名",
"remark": "",
"default_delay_seconds": 0,
"enabled": true
}
```
成功 `data` 为更新后的详情(同 4.5)。
### 4.7 删除
`DELETE /api/admin/endpoints/{id}`
成功 `data`:`{}`。级联行为见 PRD F01 / DEVELOPMENT 7.6。
### 4.8 踢下线
`POST /api/admin/endpoints/{id}/kick`
只断开当前连接,不阻止令牌重连。成功 `data`:`{"kicked": true}`(当时不在线可为 `false`)。
### 4.9 重置登录密码
`POST /api/admin/endpoints/{id}/reset-login-password`
请求体可空 `{}`,或 `{"login_password": ""}`(留空则生成)。
成功 `data`:
```json
{"login_password": "只出现一次"}
```
会话令牌作废;在线连接收到 `fatal`(`password_reset`)后断开。
### 4.10 设置对话密码
`PUT /api/admin/endpoints/{id}/talk-password`
请求:
```json
{"talk_password": ""}
```
空字符串表示清除。增加对话密码版本号,旧授权失效。成功 `data`:`{"talk_password_set": false}`。
### 4.11 解除登录锁定
`POST /api/admin/endpoints/{id}/unlock`
清除该编号的两种登录锁定。成功 `data`:`{}`。
---
## 5. 注册设置
### 5.1 获取
`GET /api/admin/registration`
成功 `data`:
```json
{
"enabled": false,
"code": "明文安全码(后台可查看)",
"updated_at_ms": 1750000000000
}
```
### 5.2 更新
`PUT /api/admin/registration`
请求(字段皆可选,至少一项):
```json
{"enabled": true, "code": "新码", "generate": false}
```
- `generate: true` 时服务器生成 16 位安全码并忽略请求里的 `code`。
- 成功 `data` 同 GET。
---
## 6. API 令牌
以下路由**仅 Cookie 会话**;Bearer 调用返回 `403 forbidden`。
### 6.1 列表
`GET /api/admin/tokens`
成功 `data.items[]`:
```json
{
"id": 1,
"name": "ops",
"enabled": true,
"created_at_ms": 1750000000000,
"last_used_at_ms": null
}
```
不含令牌明文。
### 6.2 创建
`POST /api/admin/tokens`
请求:
```json
{"name": "ops"}
```
成功 `data`:
```json
{
"id": 1,
"name": "ops",
"token": "nxm_只出现一次",
"created_at_ms": 1750000000000
}
```
### 6.3 修改
`PATCH /api/admin/tokens/{id}`
请求(可选字段):
```json
{"name": "new", "enabled": false}
```
成功 `data` 同列表项。
### 6.4 删除
`DELETE /api/admin/tokens/{id}`
成功 `data`:`{}`。
---
## 7. 群(groups)
### 7.1 列表
`GET /api/admin/groups`
查询:`cursor`、`limit`、`query`(名称包含)。
成功 `data.items[]`:
```json
{
"id": "g_ab12cd34",
"name": "一组",
"owner_id": "a",
"member_count": 3,
"created_at_ms": 1750000000000
}
```
### 7.2 创建
`POST /api/admin/groups`
请求:
```json
{
"id": "",
"name": "一组",
"owner_id": "a",
"member_ids": ["b", "c"]
}
```
后台**不要求**对话密码。`id` 留空则生成。部分成员失败时群仍创建,响应列出失败项。
成功 `data`:
```json
{
"id": "g_ab12cd34",
"name": "一组",
"owner_id": "a",
"failed": [{"id": "c", "code": "not_found"}]
}
```
### 7.3 改名
`PATCH /api/admin/groups/{id}`
请求:`{"name": "新名"}`。成功 `data` 为群摘要(同列表项)。
### 7.4 解散
`DELETE /api/admin/groups/{id}`
成功 `data`:`{}`。
### 7.5 加人
`POST /api/admin/groups/{id}/members`
请求:`{"member_ids": ["d"]}`。
成功 `data`:`{"failed": []}`(结构同创建时的 `failed`)。
### 7.6 移除成员
`DELETE /api/admin/groups/{id}/members/{endpointId}`
成功 `data`:`{}`。
### 7.7 转让群主
`POST /api/admin/groups/{id}/transfer`
请求:`{"endpoint_id": "b"}`。成功 `data`:`{"owner_id": "b"}`。
### 7.8 群详情(成员分页)
契约补充:`GET /api/admin/groups/{id}`(DEVELOPMENT 路由表未单列,但列表改名/解散需要详情;W 线可用)
查询:`cursor`、`limit`。
成功 `data`:
```json
{
"id": "g_ab12cd34",
"name": "一组",
"owner_id": "a",
"created_at_ms": 1750000000000,
"members": [
{"id": "a", "name": "", "online": true, "joined_at_ms": 1750000000000}
],
"next_cursor": ""
}
```
---
## 8. 消息记录(messages)
响应**不得**含正文或 `body` 字段。
### 8.1 列表
`GET /api/admin/messages`
查询参数(PRD F17):
| 参数 | 说明 |
|---|---|
| `cursor` / `limit` | 分页 |
| `sender_id` | 发送方 |
| `endpoint_id` | 接收端(投递表) |
| `group_id` | 目标群 |
| `state` | 消息级状态:`scheduled` / `dispatched` / `completed` |
| `from_ms` / `to_ms` | 按 `created_at` 时间范围 |
成功 `data.items[]`:
```json
{
"seq": 1,
"id": "018f...",
"sender_id": "a",
"dest_kind": "endpoint",
"dest_id": "b",
"state": "completed",
"reason": "",
"send_at_ms": 1750000010000,
"created_at_ms": 1750000000000,
"keep": false,
"receipt": true,
"content_type": "text/plain",
"delivery_counts": {
"pending": 0,
"accepted": 1,
"recalled": 0,
"expired": 0,
"dropped": 0,
"rejected": 0
}
}
```
### 8.2 详情(各接收端)
`GET /api/admin/messages/{seq}`
成功 `data`:
```json
{
"seq": 1,
"id": "018f...",
"sender_id": "a",
"dest_kind": "group",
"dest_id": "g_1",
"state": "dispatched",
"reason": "",
"send_at_ms": 1750000010000,
"created_at_ms": 1750000000000,
"meta": {},
"content_type": "text/plain",
"deliveries": [
{
"endpoint_id": "b",
"state": "pending",
"reason": "",
"attempts": 1,
"pushed_at_ms": 1750000010100,
"updated_at_ms": 1750000010100
}
],
"next_cursor": ""
}
```
`deliveries` 可分页(`cursor` / `limit` 查询参数)。无正文。
---
## 9. 只读运行参数
`GET /api/admin/settings`
成功 `data`(只读,对应配置与生效值;不含密钥):
```json
{
"listen": ":7443",
"admin_listen": "",
"session_idle_days": 30,
"record_retention_days": 7,
"idempotency_hours": 24,
"receipt_retention_days": 7,
"sqlite_synchronous": "FULL",
"limits": {
"max_body_bytes": 262144,
"max_meta_bytes": 4096,
"max_frame_bytes": 786432,
"max_ttl_seconds": 2592000,
"max_schedule_seconds": 31536000,
"max_group_members": 1000,
"grace_seconds": 60,
"ack_timeout_seconds": 300,
"delivery_window": 32,
"receipt_window": 64,
"requests_per_second": 50,
"max_pending_per_sender": 10000,
"max_pending_per_receiver": 10000
}
}
```
---
## 10. 路由速查
| 方法 | 路径 | Cookie 可变 | Bearer | 节 |
|---|---|---|---|---|
| POST | `/api/admin/login` | — | — | 2.1 |
| POST | `/api/admin/logout` | 要 | 可 | 2.2 |
| GET | `/api/admin/me` | — | 可 | 2.3 |
| POST | `/api/admin/password` | 要 | **否** | 2.4 |
| GET | `/api/admin/overview` | — | 可 | 3 |
| GET | `/api/admin/endpoints` | — | 可 | 4.1 |
| POST | `/api/admin/endpoints` | 要 | 可 | 4.2 |
| POST | `/api/admin/endpoints/import` | 要 | 可 | 4.3 |
| POST | `/api/admin/endpoints/batch` | 要 | 可 | 4.4 |
| GET | `/api/admin/endpoints/{id}` | — | 可 | 4.5 |
| PATCH | `/api/admin/endpoints/{id}` | 要 | 可 | 4.6 |
| DELETE | `/api/admin/endpoints/{id}` | 要 | 可 | 4.7 |
| POST | `/api/admin/endpoints/{id}/kick` | 要 | 可 | 4.8 |
| POST | `/api/admin/endpoints/{id}/reset-login-password` | 要 | 可 | 4.9 |
| PUT | `/api/admin/endpoints/{id}/talk-password` | 要 | 可 | 4.10 |
| POST | `/api/admin/endpoints/{id}/unlock` | 要 | 可 | 4.11 |
| GET | `/api/admin/registration` | — | 可 | 5.1 |
| PUT | `/api/admin/registration` | 要 | 可 | 5.2 |
| GET | `/api/admin/tokens` | — | **否** | 6.1 |
| POST | `/api/admin/tokens` | 要 | **否** | 6.2 |
| PATCH | `/api/admin/tokens/{id}` | 要 | **否** | 6.3 |
| DELETE | `/api/admin/tokens/{id}` | 要 | **否** | 6.4 |
| GET | `/api/admin/groups` | — | 可 | 7.1 |
| POST | `/api/admin/groups` | 要 | 可 | 7.2 |
| GET | `/api/admin/groups/{id}` | — | 可 | 7.8 |
| PATCH | `/api/admin/groups/{id}` | 要 | 可 | 7.3 |
| DELETE | `/api/admin/groups/{id}` | 要 | 可 | 7.4 |
| POST | `/api/admin/groups/{id}/members` | 要 | 可 | 7.5 |
| DELETE | `/api/admin/groups/{id}/members/{endpointId}` | 要 | 可 | 7.6 |
| POST | `/api/admin/groups/{id}/transfer` | 要 | 可 | 7.7 |
| GET | `/api/admin/messages` | — | 可 | 8.1 |
| GET | `/api/admin/messages/{seq}` | — | 可 | 8.2 |
| GET | `/api/admin/settings` | — | 可 | 9 |
「Cookie 可变」列:改变状态且走 Cookie 时必须带 `X-Nixmsg-Request: 1`。
---
## 11. 假数据开发提示(W 线)
- 可用静态 JSON 或 MSW;列表返回 2~3 条样例即可。
- 重置密码 / 创建令牌 / 开通生成密码的响应只展示一次,刷新后不再出现明文。
- 任意管理接口响应用测试断言:序列化字符串中不出现 `"body"` 键。