用户注册与管理 API
Rocky 当前提供两个用户身份相关的 API:注册(/api/register)和登录(/api/auth)。两者都是 Next.js BFF 路由,背后写入 Postgres 的 auth.users 表 + 调用 Canton Validator 分配 party。
Token 模型基于 Canton JWT,由 Validator 签发。如果不熟悉 Party / Validator,请先阅读 Canton 网络介绍。
一、概览
| 维度 | 当前实现 |
|---|---|
| 认证模型 | Email + Password(bcrypt 哈希) |
| Token 类型 | Canton JWT(由 Validator 签发,与 party 绑定) |
| Session 存储 | 浏览器 localStorage |
| 用户表 | auth.users (Postgres) |
| Party 分配 | DevNet 走 Validator /v0/admin/users;LocalNet 走 Daml admin API |
Rocky 的 auth.users 表与每个用户的 Canton Party 一一对应。注册时同时完成:
- 校验 email / password / username
- 调用 Validator 分配 Canton Party(hint = username)
- INSERT 到
auth.users(含 bcrypt 密码哈希) - 返回 Canton JWT + party + email + user_id
后续所有需要身份的接口(perp 下单 / claim MTC 等)都通过 user_id 或 party 在 BFF 层验证。
二、POST /api/register — 注册
新建一个 Rocky 账户。
2.1 Request
POST /api/register
Content-Type: application/json
{
"email": "alice@example.com",
"password": "alice-secret-123",
"username": "alice"
}
| 字段 | 类型 | 必填 | 规则 |
|---|---|---|---|
email | string | 是 | 匹配 /^[^\s@]+@[^\s@]+\.[^\s@]+$/。大小写不敏感(存储原文,比较时 lower(email)) |
password | string | 是 | 长度 ≥ 8。无字符复杂度要求(DevNet 简化) |
username | string | 是 | 匹配 /^[a-zA-Z0-9_-]{3,32}$/。作为 Canton party hint 使用 |
2.2 Response — 200 OK
{
"success": true,
"token": "eyJhbGciOiJSUzI1NiIs...",
"party": "alice::1220ca15423a1b049bf8e84132360f246057aa18f5e9e36a77db535b75bd287cceff",
"username": "alice",
"email": "alice@example.com",
"user_id": "df94d294-5319-49d6-ba8d-aa08094bc1e4"
}
| 字段 | 含义 |
|---|---|
token | Canton JWT。用于调用需要 user-act-as 的链上接口 |
party | 完整 Canton party id(前缀 = username,后缀 = participant 指纹) |
user_id | auth.users.user_id 主键(UUIDv4),后续所有 perp 接口的 user_id 参数都用这个 |
2.3 Response — 错误
| HTTP | error message | 触发条件 |
|---|---|---|
400 | "valid email is required" | email 缺失 / 格式不对 |
400 | "password must be at least 8 characters" | password 缺失 / 长度 < 8 |
400 | "username must be 3-32 characters (letters, numbers, underscore, hyphen)" | username 缺失 / 不匹配正则 |
409 | "an account with this email already exists" | email 已存在(大小写不敏感) |
409 | "username is already taken" | username 已存在 |
500 | <canton validator error> | Validator 分配 party 失败(DevNet)/ Daml admin API 失败(LocalNet) |
2.4 Example
curl -s -X POST https://demo.rocky.exchange/api/register \
-H 'Content-Type: application/json' \
-d '{
"email": "alice@example.com",
"password": "alice-secret-123",
"username": "alice"
}' | jq
2.5 后台副作用
成功一次后:
auth.users新增一行- Canton 上多了一个 party,UUIDv5(party) 是用户在 perp 系统中的
user_id派生方式(前端getPerpUserId()) - 用户即可登录 / 下单 / 充值 CC / 触发 MTC 奖励
三、POST /api/auth — 登录
凭 email + password 换取 Canton JWT。
3.1 Request
POST /api/auth
Content-Type: application/json
{
"email": "alice@example.com",
"password": "alice-secret-123"
}
| 字段 | 类型 | 必填 | 规则 |
|---|---|---|---|
email | string | 是 | 大小写不敏感,按 lower(email) 查询 |
password | string | 是 | bcrypt verify 与存储的 hash 比较 |
3.2 Response — 200 OK
{
"token": "eyJhbGciOiJSUzI1NiIs...",
"party": "alice::1220ca15423a1b049bf8e84132360f246057aa18f5e9e36a77db535b75bd287cceff",
"username": "alice",
"email": "alice@example.com",
"user_id": "df94d294-5319-49d6-ba8d-aa08094bc1e4"
}
注意:与 /api/register 不同的是,登录的 response 没有 success: true 字段(早期约定差异,未来会统一)。
3.3 Response — 错误
| HTTP | error message | 触发条件 |
|---|---|---|
400 | "email is required" | email 缺失 |
400 | "password is required" | password 缺失 |
401 | "invalid email or password" | email 不存在 OR password 错误(同一错误,故意不区分以避免邮箱枚举) |
500 | <canton authToken error> | Validator 签发 JWT 失败 |
3.4 Example
curl -s -X POST https://demo.rocky.exchange/api/auth \
-H 'Content-Type: application/json' \
-d '{
"email": "alice@example.com",
"password": "alice-secret-123"
}' | jq
3.5 安全注意
- 错误统一为
"invalid email or password",不要泄露 email 是否存在。客户端不应根据 status code 区分两种情况 - 当前没有速率限制 / IP 锁定(DevNet 简化),生产环境需补
- 密码以 bcrypt 哈希存储(10 rounds),永不返回 / 永不日志
四、Token 与 localStorage
登录 / 注册成功后,前端会把 4 个值写入浏览器 localStorage:
| key | value | 用途 |
|---|---|---|
mtc_token | Canton JWT | 调用链上接口时附带 |
mtc_party | 完整 Canton party id | 链上合约的 user-act-as |
mtc_username | username | 显示用 |
mtc_email | 显示用 |
历史名前缀
mtc_*沿用自旧品牌(MTC 即 Mining Token Credit),未做统一改名以避免破坏现有 session。
4.1 退出(Logout)
没有专门的 logout API。前端只需清掉这 4 个 localStorage 项 + 跳转到 /。Token 仍然在 Canton 那侧有效直到自然过期(典型 1 小时),但本地不再持有。
// TopNav.tsx — handleLogout
localStorage.removeItem("mtc_token");
localStorage.removeItem("mtc_party");
localStorage.removeItem("mtc_username");
localStorage.removeItem("mtc_email");
router.push("/");
4.2 Token 续期
Canton JWT 自然过期后用户会被服务端拒绝。当前没有 refresh token 机制——用户必须重新登录。计划改进:
- 加
POST /api/auth/refresh接口,凭旧 token + user_id 换新 token - 或将 token 寿命延长到 7 天,配合敏感操作再次验证
五、数据存储
5.1 auth.users schema
参见 services/internal-ledger/migrations/20260525002_auth_users.sql:
CREATE TABLE auth.users (
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
username TEXT NOT NULL UNIQUE,
party TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX users_email_lower_idx ON auth.users (lower(email));
5.2 与 auth.api_keys 的关系
auth.api_keys 是另一张表(funnel 做市机器人用,HMAC 密钥)。两表完全独立:
auth.users— 网页登录用户auth.api_keys— 程序化交易的 API key
互不影响。
六、规划中的接口
以下接口当前尚未实现,仅做规划:
| 接口 | 方法 | 用途 |
|---|---|---|
/api/auth/me | GET | 用 token 换当前用户的完整信息(用于刷新页面后恢复 session) |
/api/auth/refresh | POST | 用旧 token 换新 token,延长会话 |
/api/auth/password | PUT | 修改密码(需要旧密码) |
/api/auth/email | PUT | 修改邮箱(需要重新验证邮箱所有权 → 需要 SMTP) |
/api/auth/forgot-password | POST | "忘记密码"流程(需要 SMTP 发送 reset link) |
/api/auth/logout | POST | 服务端主动失效 token(需要 token revocation list) |
/api/admin/users | GET / DELETE | 管理员后台:列出 / 禁用用户 |
实现时机:依赖 SMTP 接入(密码重置)+ token revocation 机制(admin 禁用 / 主动 logout)。
七、相关文档
- Canton 网络介绍 — Party / Validator 概念
- CC 充值流程 — user_id 在 perp 接口中的用法
- MTC 挖矿奖励 — token + party 在挖矿场景的用法
- 仓库源码:
mtc-exchange/src/app/api/register/route.tsmtc-exchange/src/app/api/auth/route.tsmtc-exchange/src/lib/users.tsmtc-exchange/src/lib/passwords.tsservices/internal-ledger/migrations/20260525002_auth_users.sql