15 KiB
管理接口契约(Admin API)
| 项 | 内容 |
|---|---|
| 版本 | 0.1(T0.4 契约) |
| 对应 | DEVELOPMENT.md 第 8 节、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 鉴权时,必须带请求头:
X-Nixmsg-Request: 1
缺少该头返回 403(防跨站表单)。GET 不要求此头。
1.2 API 令牌(程序调用)
Authorization: Bearer nxm_...
- 带 Bearer 令牌时不看 Cookie,也不要求
X-Nixmsg-Request。 - 权限等同管理员,但不能调用:
POST /api/admin/password/api/admin/tokens及其子路径
- 令牌不过期;停用或删除后立即失效。
- 错误令牌按来源 IP 计入管理员登录锁定。
1.3 通用响应信封
成功(业务成功):
{"ok": true, "data": { }}
失败:
{"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 形如:
{
"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。
请求:
{"username": "admin", "password": "..."}
username固定为admin。- 成功时设置 Cookie
nixmsg_admin。
成功 data:
{"username": "admin"}
失败:密码错误 401 unauthorized;锁定 429 rate_limited。
2.2 登出
POST /api/admin/logout
作废当前会话 Cookie。成功 data 可为 {}。
2.3 当前管理员
GET /api/admin/me
成功 data:
{"username": "admin", "auth": "cookie"}
auth 为 cookie 或 token(API 令牌访问时)。
2.4 修改管理员密码
POST /api/admin/password
仅 Cookie 会话;API 令牌调用返回 403 forbidden。
请求:
{"old_password": "...", "new_password": "..."}
- 新密码至少 12 位。
- 成功后可选择保持当前会话或全部作废(实现默认:当前会话保留,其它会话作废)。成功
data:{}。
3. 概览
GET /api/admin/overview
成功 data:
{
"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[]:
{
"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
请求:
{
"id": "",
"name": "门口",
"remark": "",
"login_password": "",
"talk_password": "",
"default_delay_seconds": 0
}
字段规则同 PRD F01(编号小写、密码不以 nst_ 开头等)。id / login_password 留空则服务器生成。
成功 data:
{
"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):
{
"ok": false,
"error": {"code": "bad_request", "message": "CSV 校验失败"},
"data": {
"errors": [{"line": 3, "reason": "编号不合法"}]
}
}
(实现可将 errors 放在 error 旁的 data;W 线按 data.errors 读取。)
成功 data:
{
"items": [
{"id": "e_1", "login_password": "生成或原文(仅此一次)", "name": "..."}
]
}
4.4 多选批量
POST /api/admin/endpoints/batch
请求:
{"ids": ["a", "b"], "action": "disable"}
action:disable | enable | delete。
成功 data:
{"ok_ids": ["a"], "failed": [{"id": "b", "code": "not_found"}]}
4.5 详情
GET /api/admin/endpoints/{id}
成功 data 同列表项,可额外含:
{"session_issued_at_ms": null, "session_used_at_ms": null}
仍无密码哈希与正文。
4.6 修改
PATCH /api/admin/endpoints/{id}
请求(皆可选):
{
"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:
{"login_password": "只出现一次"}
会话令牌作废;在线连接收到 fatal(password_reset)后断开。
4.10 设置对话密码
PUT /api/admin/endpoints/{id}/talk-password
请求:
{"talk_password": ""}
空字符串表示清除。增加对话密码版本号,旧授权失效。成功 data:{"talk_password_set": false}。
4.11 解除登录锁定
POST /api/admin/endpoints/{id}/unlock
清除该编号的两种登录锁定。成功 data:{}。
5. 注册设置
5.1 获取
GET /api/admin/registration
成功 data:
{
"enabled": false,
"code": "明文安全码(后台可查看)",
"updated_at_ms": 1750000000000
}
5.2 更新
PUT /api/admin/registration
请求(字段皆可选,至少一项):
{"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[]:
{
"id": 1,
"name": "ops",
"enabled": true,
"created_at_ms": 1750000000000,
"last_used_at_ms": null
}
不含令牌明文。
6.2 创建
POST /api/admin/tokens
请求:
{"name": "ops"}
成功 data:
{
"id": 1,
"name": "ops",
"token": "nxm_只出现一次",
"created_at_ms": 1750000000000
}
6.3 修改
PATCH /api/admin/tokens/{id}
请求(可选字段):
{"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[]:
{
"id": "g_ab12cd34",
"name": "一组",
"owner_id": "a",
"member_count": 3,
"created_at_ms": 1750000000000
}
7.2 创建
POST /api/admin/groups
请求:
{
"id": "",
"name": "一组",
"owner_id": "a",
"member_ids": ["b", "c"]
}
后台不要求对话密码。id 留空则生成。部分成员失败时群仍创建,响应列出失败项。
成功 data:
{
"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:
{
"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[]:
{
"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:
{
"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(只读,对应配置与生效值;不含密钥):
{
"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"键。