# 萌芽账户认证中心 API 文档

访问 **`GET /`** 或 **`GET /api`**（无鉴权）可得到 JSON 格式的简要说明（服务名、版本、`/api/docs` 与 `/api/health` 入口、路由前缀摘要）。

接入地址：
- 统一登录前端：`https://auth.shumengya.top`
- 后端 API：`https://auth.api.shumengya.top`
- 本地开发 API：`http://<host>:8080`

对外接入建议：
1. 第三方应用按钮跳转到统一登录前端。
2. 登录成功后回跳到业务站点。
3. 业务站点使用回跳带回的 `token` 调用后端 API。

示例按钮：
```html
<a href="https://auth.shumengya.top/?redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&state=abc123">
  使用萌芽统一账户认证登录
</a>
```

回跳说明：
- 用户已登录时，统一登录前端会提示“继续授权”或“切换账号”。
- 登录成功后会回跳到 `redirect_uri`（或 `return_url`），并在 URL **`#fragment`**（哈希）中带上令牌与用户信息（见下表）。
- 第三方应用拿到 `token` 后，建议调用 **`POST /api/auth/verify`**（无副作用、适合网关鉴权）或 **`GET /api/auth/me`**（会更新访问记录，适合业务拉全量资料）校验并解析用户身份。

### 统一登录前端：查询参数

| 参数 | 必填 | 说明 |
|------|------|------|
| `redirect_uri` | 与 `return_url` 至少其一 | 登录成功后的回跳地址，须进行 URL 编码；可为绝对 URL 或相对路径（相对路径相对统一登录站点解析）。 |
| `return_url` | 同上 | 与 `redirect_uri` 同义，二者都传时优先 `redirect_uri`。 |
| `state` | 否 | OAuth 风格透传字符串；回跳时原样写入哈希参数，供业务防 CSRF 或关联会话。 |
| `prompt` | 否 | 预留；前端可读，当前可用于将来扩展交互策略。 |
| `client_id` | 否 | 第三方应用稳定标识（字母数字开头，可含 `_.:-`，最长 64）。写入用户「应用接入记录」，并随登录请求提交给后端。 |
| `client_name` | 否 | 展示用名称（最长 128），与 `client_id` 配对；可选。 |

### 回跳 URL：`#` 哈希参数

成功授权后，前端将使用 [`URLSearchParams`](https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams) 写入哈希，例如：`https://app.example.com/auth/callback#token=...&expiresAt=...&account=...&username=...&state=...`。

| 参数 | 说明 |
|------|------|
| `token` | JWT，调用受保护接口时放在请求头 `Authorization: Bearer <token>`。 |
| `expiresAt` | 过期时间，RFC3339（与签发侧一致，当前默认为登录时起算 **7 天**）。 |
| `account` | 账户名（与 JWT `sub` 一致）。 |
| `username` | 展示用昵称，可能为空。 |
| `state` | 若登录请求携带了 `state`，则原样返回。 |

业务站点回调页应用脚本读取 `location.hash`，解析后**仅在 HTTPS 环境**将 `token` 存于内存或安全存储，并尽快用后端 **`POST /api/auth/verify`** 校验（勿仅信任哈希中的明文字段）。

### 第三方后端接入建议

1. **仅信服务端**：回调页将 `token` 交给自有后端，由后端请求 `POST https://<api-host>/api/auth/verify`（JSON body：`{"token":"..."}`），根据 `valid` 与 `user.account` 建立会话。
2. **CORS**：浏览器直连 API 时须后端已配置 CORS（本服务默认允许任意 `Origin`）；若从服务端发起请求则不受 CORS 限制。
3. **令牌过期**：`verify` / `me` 返回 401 或 `verify` 中 `valid:false` 时，应引导用户重新走统一登录。

## 认证与统一登录

### 登录获取统一令牌
`POST /api/auth/login`

请求：
```json
{
  "account": "demo",
  "password": "demo123",
  "clientId": "my-app",
  "clientName": "我的应用"
}
```

`clientId` / `clientName` 可选；规则与请求头 `X-Auth-Client` / `X-Auth-Client-Name` 一致。传入且格式合法时，会在登录成功后写入该用户的 **应用接入记录**（见下文 `authClients`）。

响应：
```json
{
  "token": "jwt-token",
  "expiresAt": "2026-03-14T12:00:00Z",
  "user": {
    "account": "demo",
    "username": "示例用户",
    "email": "demo@example.com",
    "level": 0,
    "sproutCoins": 10,
    "secondaryEmails": ["demo2@example.com"],
    "phone": "13800000000",
    "avatarUrl": "https://example.com/avatar.png",
    "websiteUrl": "https://example.com",
    "bio": "### 简介",
    "createdAt": "2026-03-14T12:00:00Z",
    "updatedAt": "2026-03-14T12:00:00Z"
  }
}
```

若账户已被管理员封禁，返回 **403**，且**不会签发 JWT**，响应示例：

```json
{
  "error": "account is banned",
  "banReason": "违规内容"
}
```

`banReason` 可能为空字符串或省略。

**常见 HTTP 状态码（登录）**

| 状态码 | 含义 |
|--------|------|
| 200 | 成功，返回 `token`、`expiresAt`、`user`。 |
| 400 | 请求体非法或缺少 `account` / `password`。 |
| 401 | 账户不存在或密码错误（统一文案 `invalid credentials`）。 |
| 403 | 账户已封禁（见上文 JSON）。 |
| 500 | 服务器内部错误（读库、签发 JWT 失败等）。 |

**JWT 概要**：算法 **HS256**；载荷含 `account`（与 `sub` 一致）、`iss`（见 `data/config/auth.json`）、`iat` / `exp`。客户端只需透传字符串，**勿在前端解析密钥**。

### 校验令牌
`POST /api/auth/verify`

请求：
```json
{
  "token": "jwt-token"
}
```

响应：
```json
{
  "valid": true,
  "user": { "account": "demo", "...": "..." }
}
```

若账户已封禁，返回 **200** 且 `valid` 为 **false**（不返回 `user` 对象），示例：

```json
{
  "valid": false,
  "error": "account is banned",
  "banReason": "违规内容"
}
```

令牌过期、签名错误、issuer 不匹配等解析失败时返回 **401**，示例：`{"valid": false, "error": "invalid token"}`。

`verify` 与 `me` 的取舍：**仅校验身份、不改变用户数据**时用 `verify`；需要最新资料、签到状态或写入「最后访问」时用 `GET /api/auth/me`（需 Bearer）。

**应用接入记录（可选）**：第三方在 **`POST /api/auth/verify`** 或 **`GET /api/auth/me`** 上携带请求头：

- `X-Auth-Client`：应用 ID（格式同登录 JSON 的 `clientId`）
- `X-Auth-Client-Name`：可选展示名

校验成功且用户未封禁时，服务端会更新该用户 JSON 中的 `authClients` 数组（`clientId`、`displayName`、`firstSeenAt`、`lastSeenAt`）。**`POST /api/auth/verify` 的响应体 `user` 仍为 `Public()`，不含 `authClients`**，避免向调用方泄露用户在其他应用的接入情况；**`GET /api/auth/me`** 与管理员列表中的 `user`（`OwnerPublic`）**包含** `authClients`，用户可在统一登录前端的个人中心查看。

### 获取当前用户信息
`GET /api/auth/me`

请求头：
`Authorization: Bearer <jwt-token>`

可选（由前端调用 `https://cf-ip-geo.smyhub.com/api` 等接口解析后传入，用于记录「最后访问 IP」与「最后显示位置」）：
- `X-Visit-Ip`：客户端公网 IP（与地理接口返回的 `ip` 一致即可）
- `X-Visit-Location`：展示用位置文案（例如将 `geo.countryName`、`regionName`、`cityName` 拼接为 `中国 四川 成都`）

**服务端回退（避免浏览器跨域导致头缺失）**：若未传 `X-Visit-Location`，后端会用 `X-Visit-Ip`；若也未传 `X-Visit-Ip`，则用连接的 `ClientIP()`（请在前置反向代理上正确传递 `X-Forwarded-For` 等，并在生产环境为 Gin 配置可信代理）。随后服务端请求 `GEO_LOOKUP_URL`（默认 `https://cf-ip-geo.smyhub.com/api?ip=<ip>`）解析展示位置并写入用户记录。

响应：
```json
{
  "user": { "account": "demo", "...": "..." },
  "checkIn": {
    "rewardCoins": 1,
    "checkedInToday": false,
    "lastCheckInDate": "",
    "lastCheckInAt": "",
    "today": "2026-03-14"
  }
}
```

> `user` 还会包含 `lastVisitAt`、`lastVisitDate`、`checkInDays`、`checkInStreak`、`visitDays`、`visitStreak` 等统计字段。

> 在登录用户本人、管理员列表等场景下，`user` 还可包含 `lastVisitIp`、`lastVisitDisplayLocation`（最近一次通过 `/api/auth/me` 上报的访问 IP 与位置文案）。**公开用户资料接口** `GET /api/public/users/:account` 与 **`POST /api/auth/verify` 的 `user` 中不包含这两项**（避免公开展示或第三方校验时令牌响应携带访问隐私）。

> 说明：密码不会返回。

若账户在登录后被封禁，持旧 JWT 调用 `GET /api/auth/me`、`PUT /api/auth/profile`、`POST /api/auth/check-in`、辅助邮箱等需登录接口时，返回 **403**，正文同登录封禁响应（`error` + 可选 `banReason`）。客户端应作废本地令牌。

### 每日签到
`POST /api/auth/check-in`

请求头：
`Authorization: Bearer <jwt-token>`

响应：
```json
{
  "checkedIn": true,
  "alreadyCheckedIn": false,
  "rewardCoins": 1,
  "awardedCoins": 1,
  "message": "签到成功",
  "user": { "account": "demo", "...": "..." }
}
```

### 更新当前用户资料
`PUT /api/auth/profile`

请求头：
`Authorization: Bearer <jwt-token>`

请求（字段可选）：
```json
{
  "password": "newpass",
  "username": "新昵称",
  "phone": "13800000000",
  "avatarUrl": "https://example.com/avatar.png",
  "websiteUrl": "https://example.com",
  "bio": "### 新简介"
}
```

说明：`websiteUrl` 须为 `http`/`https` 地址；可传空字符串清除；未写协议时服务端会补全为 `https://`。

响应：
```json
{
  "user": { "account": "demo", "...": "..." }
}
```

## 用户广场

### 获取用户公开主页
`GET /api/public/users/{account}`

说明：
- 仅支持账户名 `account`，不支持昵称查询。
- 适合第三方应用展示用户公开资料。
- 若该账户已被封禁，返回 **404** `{"error":"user not found"}`（与不存在账户相同，避免公开资料泄露）。
- 响应中含该用户**最近一次被服务端记录的**访问 IP（`lastVisitIp`）与展示用地理位置（`lastVisitDisplayLocation`，与本人中心一致）；`POST /api/auth/verify` 返回的用户 JSON **不含**上述两项。

响应：
```json
{
  "user": {
    "account": "demo",
    "username": "示例用户",
    "level": 3,
    "sproutCoins": 10,
    "avatarUrl": "https://example.com/avatar.png",
    "websiteUrl": "https://example.com",
    "lastVisitIp": "203.0.113.1",
    "lastVisitDisplayLocation": "中国 广东省 深圳市",
    "bio": "### 简介"
  }
}
```

### 公开注册策略
`GET /api/public/registration-policy`

无需鉴权。用于前端判断是否展示「邀请码」输入框。

响应：
```json
{
  "requireInviteCode": false
}
```

当 `requireInviteCode` 为 **true** 时，`POST /api/auth/register` 必须携带有效 `inviteCode`（见下节）。

### 注册账号（发送邮箱验证码）
`POST /api/auth/register`

请求：
```json
{
  "account": "demo",
  "password": "demo123",
  "username": "示例用户",
  "email": "demo@example.com",
  "inviteCode": "ABCD1234"
}
```

- `inviteCode`：可选。若服务端开启「强制邀请码」，则必填且须为管理员发放的未过期、未用尽邀请码。邀请码**不区分大小写**；成功完成 `verify-email` 创建用户后才会扣减使用次数。

响应：
```json
{
  "sent": true,
  "expiresAt": "2026-03-14T12:10:00Z"
}
```

### 验证邮箱并完成注册
`POST /api/auth/verify-email`

请求：
```json
{
  "account": "demo",
  "code": "123456"
}
```

响应：
```json
{
  "created": true,
  "user": { "account": "demo", "...": "..." }
}
```

### 忘记密码（发送重置验证码）
`POST /api/auth/forgot-password`

请求：
```json
{
  "account": "demo",
  "email": "demo@example.com"
}
```

响应：
```json
{
  "sent": true,
  "expiresAt": "2026-03-14T12:10:00Z"
}
```

### 重置密码
`POST /api/auth/reset-password`

请求：
```json
{
  "account": "demo",
  "code": "123456",
  "newPassword": "newpass"
}
```

响应：
```json
{ "reset": true }
```

### 申请添加辅助邮箱（发送验证码）
`POST /api/auth/secondary-email/request`

请求头：
`Authorization: Bearer <jwt-token>`

请求：
```json
{
  "email": "demo2@example.com"
}
```

响应：
```json
{
  "sent": true,
  "expiresAt": "2026-03-14T12:10:00Z"
}
```

### 验证辅助邮箱
`POST /api/auth/secondary-email/verify`

请求头：
`Authorization: Bearer <jwt-token>`

请求：
```json
{
  "email": "demo2@example.com",
  "code": "123456"
}
```

响应：
```json
{
  "verified": true,
  "user": { "account": "demo", "...": "..." }
}
```

## 管理端接口（需要管理员 Token）

管理员 Token 存放在 `data/config/admin.json` 中；如果文件不存在，后端启动时会自动生成并写入该文件。
请求时可使用以下任一方式携带：
- Query：`?token=<admin-token>`
- Header：`X-Admin-Token: <admin-token>`

### 签到奖励设置
`GET /api/admin/check-in/config`

`PUT /api/admin/check-in/config`

请求：
```json
{
  "rewardCoins": 1
}
```
- Header：`Authorization: Bearer <admin-token>`

### 注册策略与邀请码

`GET /api/admin/registration`

响应含 `requireInviteCode` 与 `invites` 数组（每项含 `code`、`note`、`maxUses`、`uses`、`expiresAt`、`createdAt`）。`maxUses` 为 0 表示不限次数。

`PUT /api/admin/registration`

请求：
```json
{ "requireInviteCode": true }
```

`POST /api/admin/registration/invites`

请求：
```json
{
  "note": "内测批次",
  "maxUses": 10,
  "expiresAt": "2026-12-31T15:59:59Z"
}
```

`expiresAt` 可省略；须为 RFC3339。响应 `201`，`invite` 内含服务端生成的 8 位邀请码。

`DELETE /api/admin/registration/invites/{code}`

删除指定邀请码（`code` 与存储大小写可能不同，按不区分大小写匹配）。

### 获取用户列表
`GET /api/admin/users`

响应：
```json
{
  "total": 1,
  "users": [{ "account": "demo", "...": "..." }]
}
```

### 新建用户
`POST /api/admin/users`

请求：
```json
{
  "account": "demo",
  "password": "demo123",
  "username": "示例用户",
  "email": "demo@example.com",
  "level": 0,
  "sproutCoins": 10,
  "secondaryEmails": ["demo2@example.com"],
  "phone": "13800000000",
  "avatarUrl": "https://example.com/avatar.png",
  "websiteUrl": "https://example.com",
  "bio": "### 简介"
}
```

### 更新用户
`PUT /api/admin/users/{account}`

请求（字段可选）：
```json
{
  "password": "newpass",
  "username": "新昵称",
  "level": 1,
  "secondaryEmails": ["demo2@example.com"],
  "sproutCoins": 99,
  "websiteUrl": "https://example.com",
  "banned": true,
  "banReason": "违规说明（最多 500 字）"
}
```

- `banned`：是否封禁；解封时请传 `false`，并可将 `banReason` 置为空字符串。
- `banReason`：仅当用户处于封禁状态时允许设为非空；封禁时若首次写入会记录 `bannedAt`（RFC3339，存于用户 JSON）。

管理员列表 `GET /api/admin/users` 中每条 `user` 可含 `banned`、`banReason`（不含 `bannedAt` 亦可从存储文件中查看）。

### 删除用户
`DELETE /api/admin/users/{account}`

响应：
```json
{ "deleted": true }
```

## 数据存储说明

- 用户数据：`data/users/*.json`
- 注册待验证：`data/pending/*.json`
- 密码重置记录：`data/reset/*.json`
- 辅助邮箱验证：`data/secondary/*.json`
- 管理员 Token：`data/config/admin.json`
- JWT 配置：`data/config/auth.json`
- 邮件配置：`data/config/email.json`
- 注册策略与邀请码：`data/config/registration.json`

## 快速联调用示例

```bash
# 服务根路径 JSON 说明
curl -s http://localhost:8080/ | jq .

# 登录
curl -X POST http://localhost:8080/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"account":"demo","password":"demo123"}'

# 校验令牌（推荐第三方网关先调此接口）
curl -X POST http://localhost:8080/api/auth/verify \
  -H 'Content-Type: application/json' \
  -d '{"token":"<jwt-token>"}'

# 使用令牌获取用户信息（会更新访问记录）
curl http://localhost:8080/api/auth/me \
  -H 'Authorization: Bearer <jwt-token>'
```
