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

15 KiB
Raw Permalink Blame History

管理接口契约(Admin API)

项 内容
版本 0.1(T0.4 契约)
对应 DEVELOPMENT.md 第 8 节、PRD.md F17
读者 后台接口 A、后台网页 W、集成测试

本文约定管理后台 HTTP 接口的方法、路径、请求字段、成功响应、分页与错误格式。W 线可先按本文用假数据开发;A 线实现时响应字段名与语义须与本文一致。

硬性规则

  • 所有 JSON 响应在序列化前去掉消息正文;响应中不得出现 body 字段(含嵌套)。
  • 管理接口不开跨域。
  • 时间字段为 Unix 毫秒(整数),JSON 字段名蛇形。

1. 鉴权与通用约定

项 值
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" 键。