From 6445687d56b299ba6489d9326f419a38482ee887 Mon Sep 17 00:00:00 2001 From: Mplan Date: Mon, 28 Sep 2026 13:15:04 +0800 Subject: [PATCH] docs(api): document Better Auth migration --- API.md | 448 +++++++++++++++------------------------------------------ 1 file changed, 116 insertions(+), 332 deletions(-) diff --git a/API.md b/API.md index e0d5dc8..9e127e4 100644 --- a/API.md +++ b/API.md @@ -2,9 +2,9 @@ > **读者**:前端开发者。本文档覆盖 API 的全部端点:请求参数、响应结构、可能出现的错误与已知坑点。 > -> **事实来源**:本文档的字段名、约束与错误消息以 `src/schema/*.ts`(zod 校验定义)与 `src/router/*.ts`(路由实现)为最终依据整理。若文档与代码不一致,以代码为准。 +> **事实来源**:业务接口以 `src/schema/*.ts` 与 `src/router/*.ts` 为准;认证以 `src/auth.ts` 中的 Better Auth 配置、`src/index.ts` 中的挂载路径及 `src/middleware/auth.ts` 中的业务鉴权规则为准。若文档与代码不一致,以代码为准。 > -> **重要免责**:错误消息文案(`error` 字段的中文内容)可能随时调整。**前端逻辑只能依赖 HTTP 状态码,严禁匹配错误消息字符串**。文档中列出消息原文仅供调试对照。 +> **重要免责**:错误消息文案可能调整。**前端逻辑应依赖 HTTP 状态码及需要时的机器可读错误码,严禁匹配错误消息字符串**。文档中列出消息原文仅供调试对照。 ## 目录 @@ -29,25 +29,25 @@ | 本地开发 | `http://localhost:3000`(`vc dev` 启动) | | 生产 | 以部署地址为准 | -除 `/image` 成功响应直接返回图片字节外,响应均为 `application/json`(个别异常情形返回纯文本,见 2.2)。所有响应携带两个调试响应头: +业务接口除 `/image` 成功响应直接返回图片字节外,响应通常为 `application/json`(个别异常情形返回纯文本,见 2.2)。Better Auth 的 `/auth/*` 有独立响应格式,部分操作也可能重定向,见第 4 章。响应携带两个调试响应头: - `X-Request-Id`:请求唯一 ID,反馈问题时请附上 - `X-Response-Time`:服务端处理耗时,如 `12ms` ### 1.2 命名与格式 -- 请求与响应字段一律 **snake_case**(如 `cloud_id`、`page_size`、`received_like_count`) +- 业务接口使用 **snake_case**(如 `cloud_id`、`page_size`);Better Auth 的 `/auth/*` 接口使用其原生 **camelCase** 字段 - 日期时间一律为 **ISO 8601 字符串**(如 `2026-08-15T08:30:00.000Z`),可空的日期字段值为 `null`。唯一的例外见 2.3 第 5 条 - UUID 字段为标准 UUID 字符串 ### 1.3 响应形态 -响应**没有统一包装**,分两种形态: +业务成功响应**没有统一包装**,分两种形态;`/auth/*` 不使用下表中的业务信封: | 场景 | 形态 | 示例 | |---|---|---| | 写操作成功(POST/PATCH/DELETE 的动作类端点) | 信封对象 | `{ "status_code": 200, "message": "删除成功" }` | -| 读操作成功(GET 及登录) | 裸数据 | 单资源为对象、列表为**数组**、登录为 `{ "access_token" }` | +| 读操作成功(业务 GET) | 裸数据 | 单资源为对象、列表为**数组** | ### 1.4 分页 @@ -125,7 +125,7 @@ ### 2.1 两种错误响应形状 -本 API 存在**两种**错误响应形状,前端必须分别处理: +业务接口存在**两种**错误响应形状,前端必须分别处理。`/auth/*` 使用 Better Auth 自己的错误格式,见 3.5。 **① 业务错误**(绝大多数错误)——ErrorMessage 信封: @@ -157,14 +157,14 @@ | 状态码 | 含义 | 前端通用动作 | |---|---|---| | 200 | 成功 | — | -| 201 | 创建成功(注册、上传云图) | — | +| 201 | 业务创建成功(如上传云图、管理员创建用户) | — | | 304 | 图片 ETag 未变化,无响应 body | 继续使用本地缓存 | -| 400 | 参数校验失败(形状②),或业务规则拒绝(形状①,如「新密码不能与当前密码相同」) | 检查参数;形状②可解析 `error.message` 定位字段 | -| 401 | 未认证:未携带 token,或 token 无效/过期/已失效 | 清除本地登录态,跳转登录页 | -| 403 | 已认证但无权限:角色不足、邮箱未确认、帐号被禁用、越权访问他人资源 | 按消息场景提示 | +| 400 | 参数校验失败(形状②),或业务规则拒绝(形状①) | 检查参数;形状②可解析 `error.message` 定位字段 | +| 401 | 业务接口没有有效 Better Auth 会话,或邮箱未验证、帐号被禁用 | 重新获取会话,必要时引导登录或验证邮箱 | +| 403 | 业务接口已认证但权限不足,如角色不足或越权访问他人资源 | 按场景提示 | | 404 | 资源不存在(形状①);**也可能是纯文本**(见下方警告) | 区分 content-type 后处理 | | 405 | HTTP 方法不受端点支持 | 读取 `Allow` 响应头 | -| 409 | 唯一性冲突:邮箱/用户名已被占用 | 提示用户更换 | +| 409 | 部分业务接口发生唯一性冲突,如管理员创建用户或修改用户名 | 提示用户更换;普通注册见 3.5 | | 413 | 上传请求体超过 21 MiB | 提示压缩图片后重试 | | 500 | 服务器内部错误 | 提示稍后重试,反馈时附 `X-Request-Id` | | 503 | 图片存储服务暂时不可用 | 稍后重试,反馈时附 `X-Request-Id` | @@ -176,341 +176,124 @@ 以下是文档写作时核实的现状描述,**未来可能修复**。前端如需为之写防御代码,请做好移除准备。 1. **两种错误形状并存**:400 校验错误不是业务错误信封(见 2.1)。 -2. **部分 404 是纯文本而非 JSON**,来源有三:(a) 未实现的占位端点(各章已标注);(b) `GET /profile/me` 与 `GET /admin/users` 的数据库异常会从空 `catch` 落到纯文本 404;(c) 未匹配任何路由的路径。注册与邮箱确认失败始终返回 JSON 信封。 -3. **静默续签只写 Cookie**:token 到期前的自动续签仅通过 `Set-Cookie` 下发新 token(见 3.4)。使用 Bearer header 模式的客户端**拿不到新 token**,30 分钟后必然 401,需重新登录。 -4. **`POST /admin/user` 是假端点**:它原样回显请求体(**含明文密码**),并不创建用户。 +2. **部分 404 是纯文本而非 JSON**,来源有三:(a) 未实现的占位端点(各章已标注);(b) `GET /profile/me` 与 `GET /admin/users` 的数据库异常会从空 `catch` 落到纯文本 404;(c) 未匹配任何路由的路径。Better Auth 认证端点使用自己的错误格式。 +3. **认证端点使用 Better Auth 格式**:`/auth/*` 的字段、状态码与错误响应由 Better Auth 定义,不能用业务错误信封解析;见第 4 章。 +4. **管理员创建用户需验证邮箱**:`POST /admin/user` 会通过 Better Auth 创建用户并尝试发送验证邮件;验证前不能登录。 5. **`GET /info/cloudtype`(列表)的 `created_at` 格式与其他端点不同**:是 PostgreSQL 原生字符串(如 `2026-08-15 08:30:00.123456+00`),而非 ISO 8601(`2026-08-15T08:30:00.000Z`)。详情端点 `/info/cloudtype/:id` 及其他所有端点均为 ISO 8601。前端解析该字段时需兼容两种格式。 -6. **用户名/邮箱长度不在 API 层校验**:数据库限制用户名 ≤ 16 字符、邮箱 ≤ 64 字符,但 zod 校验不拦截。超长注册与超长改名(`PATCH /profile/me`)会返回 JSON 500。前端应自行限制输入长度。 +6. **用户名/邮箱数据库长度限制**:用户名 ≤ 16 字符、邮箱 ≤ 64 字符;前端应限制输入长度。 7. **`GET /profile/:user_id` 向任意登录用户暴露对方邮箱**:任何登录用户都能查到任意用户的 `email`。前端展示他人资料时不应展示邮箱,也不应假设该接口未来仍返回邮箱。 -8. **用户名唯一性的大小写规则不一致**:注册时的查重是大小写不敏感的(`Alice`/`alice` 视为重复),但数据库唯一约束与 `PATCH /profile/me` 改名是大小写敏感的(可改成仅大小写不同的名字)。 +8. **用户名大小写**:数据库唯一约束区分大小写,注册和改名均受该约束。 --- ## 3. 认证 -### 3.1 机制概览 +### 3.1 从旧认证接口迁移 -- JWT(HS256),有效期 **30 分钟** -- 获取方式:`POST /auth/login` 成功后在响应体返回 `access_token`,**同时**通过 `Set-Cookie` 写入 HttpOnly Cookie -- 受保护端点接受两种携带方式(服务端优先读 header): - 1. 请求头 `Authorization: Bearer ` - 2. Cookie `token=`(`HttpOnly; SameSite=Strict; Path=/; Max-Age=1800`;非开发/测试环境附加 `Secure`) +后端现在由 Better Auth 处理注册、登录、邮箱验证、会话和密码。旧认证路由已移除;请替换所有旧请求与响应解析: -### 3.2 Bearer 还是 Cookie? - -| 模式 | 适用 | 注意事项 | +| 旧接口 | 当前接口 / 前端动作 | 主要变化 | |---|---|---| -| Cookie | 浏览器前端(推荐) | 登录后浏览器自动携带;`fetch` 需设 `credentials: "include"`;HttpOnly 使 JS 无法读取 token,天然防 XSS 窃取;可享受静默续签 | -| Bearer | 非浏览器客户端(App、脚本) | 自行存储 `access_token` 并注入 header;**无法静默续签**(2.3 第 3 条),30 分钟后需重新登录 | +| `POST /auth/register` | `POST /auth/sign-up/email` | 请求仍为 `name`、`email`、`password`;成功为 Better Auth 的 `200`,不会自动登录 | +| `POST /auth/login` | `POST /auth/sign-in/email` | 使用 Better Auth 会话 Cookie,不再保存旧 JWT | +| `POST /auth/logout` | `POST /auth/sign-out` | 撤销当前会话;不要只清除前端状态 | +| `POST /auth/resend-confirmation` | `POST /auth/send-verification-email` | 请求 `{ "email": "..." }` | +| `POST /auth/confirm-email` | `GET /auth/verify-email?token=...` | token 从前端验证页的 URL 读取,放入查询参数 | +| `POST /auth/forgot-password` | `POST /auth/request-password-reset` | 请求 `{ "email": "..." }` | +| `POST /auth/reset-password` | `POST /auth/reset-password` | 路径相同,请求改为 `{ "token": "...", "newPassword": "..." }`;不再提交 `email` / `new_password` | +| `PATCH /auth/password` | `POST /auth/change-password` | 请求改为 `currentPassword`、`newPassword`,可选 `revokeOtherSessions` | -### 3.3 CORS +旧路由的 `status_code` / `message` 信封、JWT 及认证相关的 snake_case 请求字段不适用于新端点。普通注册不要提交 `role`:角色由服务端设置为 `user`;创建管理员用户走第 8 章的管理接口。业务接口的路径与 snake_case 字段仍按后续章节使用。 -- 服务端仅放行**单个** origin(环境变量 `CORS_ORIGIN`,本地默认应为 `http://localhost:5173`),且 `credentials: true` -- 前端本地开发的服务端口/origin 必须与后端配置**完全一致**(协议、主机、端口),否则 Cookie 模式的请求会被浏览器拦截 -- 放行的请求头:`Content-Type`、`Authorization` -- 私有图片通过 `` 直接加载时依赖 Cookie;生产环境前端与 API 必须位于同一 site(例如 `app.example.com` 与 `api.example.com`),否则 `SameSite=Strict` Cookie 不会随图片请求发送 +### 3.2 浏览器接入 -### 3.4 有效期、续签与失效 +前端安装与后端当前版本一致的 `better-auth@1.7.6`,创建客户端时将 **API 地址连同 `/auth` 路径**传入 `baseURL`。服务端挂载路径是 `/auth`,不是 Better Auth 默认的 `/api/auth`;不要把业务 API 根地址直接用作认证客户端的完整 `baseURL`。下面是框架无关的示例;React 可从 `better-auth/react` 导入 `createAuthClient`。这与 [Better Auth 客户端的自定义路径配置](https://better-auth.com/docs/concepts/client)一致。 -- token 有效期 30 分钟(Cookie 的 `Max-Age` 同为 1800 秒) -- **静默续签**:携带剩余有效期 ≤ 300 秒的 token 访问受保护端点时,服务端会在响应中**追加 `Set-Cookie` 写入新 token**。Cookie 模式下浏览器自动替换,前端无感知;Bearer 模式收不到新 token -- 续签不适用的路径:`POST /auth/logout`、`PATCH /auth/password` -- `/image` 请求只验证身份,不触发静默续签,避免一个页面的并发图片请求重复写 Cookie -- 认证中间件每次请求都会重新检查用户仍已确认邮箱且未被管理员禁用;任一条件不满足时,已有 JWT 也会被拒绝 -- **立即失效**的情况:调用 `/auth/logout`、调用 `PATCH /auth/password` 改密、通过 `/auth/reset-password` 重置密码、用户被禁用,以及管理员修改了该用户的角色或密码(`PATCH /admin/users/:user_id` 见 8.3、`PATCH /admin/users/:user_id/password` 见 8.4)。除「用户被禁用」由中间件每次请求重新校验外,其余情况都会递增 `tokenVersion`,使该用户**所有已签发的 token 全部作废**(包括其他设备上的);用户自己改密与密码重置成功时还会写入过期 Cookie +```ts +import { createAuthClient } from "better-auth/client"; -### 3.5 认证相关错误消息对照 +const API_ORIGIN = "http://localhost:3000"; // 生产环境改成实际 API origin +export const authClient = createAuthClient({ + baseURL: `${API_ORIGIN}/auth`, + fetchOptions: { credentials: "include" }, +}); +``` -| 状态码 | 消息原文 | 场景 | -|---|---|---| -| 401 | `未登录` | 受保护端点未携带任何 token | -| 401 | `Token 无效或已过期` | token 签名无效、过期、已被吊销(登出、改密或密码重置后),或用户不再同时满足「邮箱已确认且未禁用」 | -| 403 | `权限不足` | 已登录但角色不满足(如非 admin 访问 `/admin`) | +Better Auth 浏览器客户端默认会携带凭证;上面显式写出该选项,便于与业务请求保持一致。业务接口使用原生 `fetch` 或其他 HTTP 客户端时,也要单独配置: -注意「帐号被禁用」在**登录时**报 403「帐号已被禁用」,但已登录后被禁用,后续请求报的是 401「Token 无效或已过期」。 +```ts +const response = await fetch(`${API_ORIGIN}/profile/me`, { + credentials: "include", +}); +``` ---- +浏览器会自动保存并发送 HttpOnly 会话 Cookie。不要尝试在 JavaScript 中读取 Cookie,也不要将登录响应中的 token 当作旧 JWT 存储。服务端 `CORS_ORIGIN` 必须与前端 origin(协议、域名、端口)精确匹配;生产环境 Cookie 为 `SameSite=None; Secure`,本地开发为 `SameSite=Lax`。`BETTER_AUTH_URL` 是后端公开地址;前端客户端仍需使用上面含 `/auth` 的 URL。跨源接入和凭证设置也见 [Better Auth 的 Hono 集成文档](https://better-auth.com/docs/integrations/hono)。 + +### 3.3 会话与业务权限 + +登录成功后,使用 `authClient.getSession()` 或对应框架的 `useSession()` 获取会话: + +```ts +const { error } = await authClient.signIn.email({ email, password }); +if (error) throw error; + +const { data: session } = await authClient.getSession(); +// session 为 { user, session } 或 null;退出时调用 await authClient.signOut()。 +``` + +需要业务资料、统计及角色时,再请求 `GET /profile/me`,不要将 Better Auth 的 `user` 对象当作第 1.5 节的 UserProfile。完整客户端方法见 [Better Auth 会话管理](https://better-auth.com/docs/concepts/session-management)。 + +所有受保护的业务接口都再次检查会话、邮箱验证状态、禁用状态及角色。**业务接口**在无会话、邮箱未验证或帐号被禁用时返回 `401`;会话有效但角色或资源权限不足时返回 `403`。管理员修改角色或密码后会撤销目标用户的全部会话,前端应刷新登录状态。 + +两种可选认证端点行为不同:`GET /cloud/:cloud_id` 对无效/过期凭证按匿名处理;`GET|HEAD /image/:cloud_id/:variant` 在显式提供无效凭证时返回 `401`。同时发送 `Authorization` 和 Cookie 时,业务接口优先使用 Bearer 凭证。 + +### 3.4 邮件与密码页面 + +注册后会发送验证邮件,用户需先验证邮箱再登录;验证成功后也不会自动登录。当前邮箱验证 token 有效期为 24 小时,密码重置 token 默认有效期为 1 小时。后端发出的邮件仍指向前端 `/verify-email?token=...` 与 `/reset-password?email=...&token=...` 页面;它们是**前端页面**,不是 API。验证页应将 token 交给 `GET /auth/verify-email?token=...`,重置页应以 token 和新密码调用 `POST /auth/reset-password`。邮件中的 `email` 可用于页面展示,重置请求不需要它。密码重置成功会撤销该用户的全部会话。 + +重发验证邮件使用 `authClient.sendVerificationEmail({ email })`;申请密码重置使用 `authClient.requestPasswordReset({ email })`。用户不存在时,密码重置申请仍返回通用成功结果,不要据此判断邮箱是否已注册。验证及重置流程见 [Better Auth 邮箱密码文档](https://better-auth.com/docs/authentication/email-password)。 + +### 3.5 认证错误与非浏览器客户端 + +Better Auth 端点的错误是其原生结构,通常包含 `code`、`message`;客户端调用返回 `{ data, error }`。按 HTTP 状态、客户端的 `error` 和必要时的 `error.code` 处理,不要匹配英文错误文案,也不要按业务接口的 `status_code` 或 Zod 形状解析。未验证邮箱登录会被 Better Auth 拒绝(当前版本的错误码为 `EMAIL_NOT_VERIFIED`),此时可显示验证页并提供重发邮件操作。 + +当前配置要求邮箱验证,且注册后不自动登录。Better Auth 对重复邮箱注册可能返回与新邮箱相同的 **200** 响应和临时生成的用户对象;因此注册成功仅表示「请查收验证邮件」,**不能仅凭响应中的 `user.id` 判断账户已创建,也不能将其用作已登录用户 ID**。应在验证后重新登录。该行为来自当前安装的 Better Auth `1.7.6`。 + +非浏览器客户端可使用已启用的 Bearer 插件。登录成功响应头 `set-auth-token` 暴露会话 token,后续请求使用 `Authorization: Bearer `;这不是旧 JWT,也不是密码重置或邮箱验证 token。前端浏览器仍使用 Cookie。[Better Auth Bearer 插件说明](https://better-auth.com/docs/plugins/bearer)。 ## 4. 端点 · 认证 `/auth` -### 4.1 `POST /auth/register` — 注册 +以下为本项目已启用的 Better Auth 邮箱密码功能,密码至少 8 字符。请求 JSON 使用原生 **camelCase**,响应不带业务 `status_code` 信封。前端推荐调用第 3.2 节的客户端;直接请求时使用表中方法和路径。Better Auth 还会暴露其他内置或 Admin 插件端点,其输入、权限和响应以 [Better Auth 官方文档](https://better-auth.com/docs)及当前安装版本为准。 -- **认证**:无(公开) - -**请求体**(JSON): - -| 参数 | 类型 | 必填 | 约束 | 说明 | -|---|---|---|---|---| -| `name` | string | 是 | ⚠️ 数据库限 ≤ 16 字符,API 层不校验(2.3 第 6 条) | 用户名,大小写不敏感查重 | -| `email` | string | 是 | 合法邮箱格式;⚠️ 数据库限 ≤ 64 字符 | 服务端去除首尾空白并转为小写后存储 | -| `password` | string | 是 | 无强度校验 | 密码 | - -```json -{ - "name": "cloudwatcher", - "email": "watcher@example.com", - "password": "s3cret-password" -} -``` - -**成功响应**:`201` - -```json -{ - "status_code": 201, - "message": "注册成功,请查收邮件并确认邮箱", - "email_sent": true -} -``` - -数据库提交成功但邮件暂时未发送时仍返回 `201`,`email_sent` 为 `false`,消息会提示稍后使用重发验证流程。用户、密码凭据和 24 小时有效的确认记录已经保留,不应重试注册。 - -**错误**: - -| 状态码 | 触发条件 | 消息原文 | 前端建议动作 | +| 操作 | 方法与路径 | Better Auth 客户端方法 | 请求与结果要点 | |---|---|---|---| -| 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 | -| 409 | 已确认邮箱重复注册(大小写不敏感) | `该邮箱已被注册` | 提示直接登录 | -| 409 | 未确认邮箱重复注册 | `该邮箱已被注册,请使用重发验证流程` | 引导至重发验证流程,不会覆盖原用户名或密码 | -| 409 | 用户名已使用(大小写不敏感) | `该用户名已被使用` | 提示换用户名 | -| 500 | 数据库、密码散列或服务端配置异常 | `注册失败` | 稍后重试;若此前已收到 201,不要重复注册 | +| 注册 | `POST /auth/sign-up/email` | `authClient.signUp.email({ name, email, password })` | 成功 `200`,`{ token: null, user }`;发送验证邮件,不建立会话 | +| 登录 | `POST /auth/sign-in/email` | `authClient.signIn.email({ email, password })` | 验证邮箱后才可登录;成功 `200` 并设置会话 Cookie | +| 当前会话 | `GET /auth/get-session` | `authClient.getSession()` | 有效会话返回 `{ user, session }`;无会话返回 `null` | +| 登出当前会话 | `POST /auth/sign-out` | `authClient.signOut()` | 需携带当前 Cookie 或 Bearer;撤销当前会话 | +| 重发验证邮件 | `POST /auth/send-verification-email` | `authClient.sendVerificationEmail({ email })` | `{ email }`;匿名请求对不存在或已验证邮箱也可能返回 `{ status: true }` | +| 确认邮箱 | `GET /auth/verify-email?token=...` | 可用 `fetch` 发 GET | 查询参数 `token`;无 `callbackURL` 时成功返回包含 `status: true` 的 JSON | +| 申请密码重置 | `POST /auth/request-password-reset` | `authClient.requestPasswordReset({ email })` | `{ email }`;返回通用结果,不泄露账户是否存在 | +| 重置密码 | `POST /auth/reset-password` | `authClient.resetPassword({ token, newPassword })` | `{ token, newPassword }`;成功 `{ status: true }`,撤销该用户全部会话 | +| 修改当前密码 | `POST /auth/change-password` | `authClient.changePassword({ currentPassword, newPassword, revokeOtherSessions })` | 需登录;可选择撤销其他会话 | -**注意事项**: +验证邮箱页面的请求示例: -- 新用户的 `is_disabled` 为 `false`,`email_verified_at` 为空;两个状态相互独立。注册后必须先确认邮箱,再调用 `/auth/login` -- 注册响应**不返回 token、不写 Cookie** -- 确认邮件由 `Opencloud ` 发出,同时包含 `https://cloud.catpl.top/verify-email?token=...` 链接和原始 token 备用文本。数据库仅保存使用 `JWT_SECRET` 生成的 HMAC 摘要 -- ⚠️ 并发注册(同一邮箱/用户名几乎同时提交)命中数据库唯一约束时,冲突消息文案与上方表格不同(邮箱冲突会返回 `该邮箱已被注册;若尚未验证,请使用重发验证流程`)。按本文档免责声明,前端仍只依据状态码 `409` 处理 +```ts +const token = new URLSearchParams(location.search).get("token"); +if (!token) throw new Error("缺少验证 token"); -### 4.2 `POST /auth/resend-confirmation` — 重发确认邮件 - -- **认证**:无(公开) - -**请求体**(JSON): - -| 参数 | 类型 | 必填 | 说明 | -|---|---|---|---| -| `email` | string | 是 | 与注册、登录相同:服务端去除首尾空白并转为小写;不接受旧字段 `user_email` | - -```json -{ - "email": "watcher@example.com" -} +const response = await fetch( + `${API_ORIGIN}/auth/verify-email?token=${encodeURIComponent(token)}`, + { credentials: "include" }, +); +if (!response.ok) throw new Error("邮箱验证失败"); +// 验证成功后引导用户登录;当前配置不会自动创建会话。 ``` -**成功响应**:`200` +上面的 `API_ORIGIN` 与第 3.2 节相同。直接调用 `GET /auth/verify-email` 时不需要旧接口的 JSON 请求体;若传入 `callbackURL`,Better Auth 可能改为重定向,前端应按重定向处理。 -```json -{ - "status_code": 200, - "message": "如果该邮箱符合条件,我们将发送验证邮件" -} -``` - -未知邮箱、已确认邮箱、被管理员禁用、处于 60 秒冷却期、符合发送条件以及邮件供应商失败,都会收到完全相同的状态码和响应体。前端不能根据该响应判断邮件是否实际发送,也不应显示「邮箱存在」之类的提示。 - -只有尚未确认、未被管理员禁用且不在冷却期内的用户会触发邮件发送。成功签发新 token 后,同一用户此前未消费的邮箱确认 token 全部失效;新 token 仍有 24 小时有效期,邮件内容与注册邮件相同。已消费记录会保留作为审计信息。 - -重叠请求会按用户串行处理,至多签发一个可用 token。数据库签发或 Resend 失败不会改变上述公共响应。 - -**错误**:请求体校验失败返回 `400` zod 形状(2.1)。 - -### 4.3 `POST /auth/confirm-email` — 确认邮箱 - -- **认证**:无(公开) - -前端从确认链接读取 `token`,再提交: - -```json -{ - "token": "0123456789abcdef0123456789abcdef" -} -``` - -**成功响应**:`200` - -```json -{ - "status_code": 200, - "message": "邮箱确认成功,请重新登录" -} -``` - -确认会在一个数据库事务中消费未过期、未消费且用途为 `email_confirmation` 的记录,并写入用户的邮箱确认时间。它不会签发 JWT、不会写认证 Cookie,也绝不修改管理员禁用状态。 - -| 状态码 | 触发条件 | 消息原文 | 前端建议动作 | -|---|---|---|---| -| 400 | token 未知、过期、已消费,或来自其他用途 | `确认链接无效或已过期` | 统一提示链接无效,并引导重发 | -| 500 | 数据库或服务端配置异常 | `确认邮箱失败` | 稍后重试 | - -### 4.4 `POST /auth/login` — 登录 - -- **认证**:无(公开) - -**请求体**(JSON): - -| 参数 | 类型 | 必填 | 说明 | -|---|---|---|---| -| `email` | string | 是 | 服务端去除首尾空白、转为小写后匹配 | -| `password` | string | 是 | 密码 | - -```json -{ - "email": "watcher@example.com", - "password": "s3cret-password" -} -``` - -**成功响应**:`200`,同时写入认证 Cookie(见 3.1) - -```json -{ - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." -} -``` - -**错误**: - -| 状态码 | 触发条件 | 消息原文 | 前端建议动作 | -|---|---|---|---| -| 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 | -| 401 | 邮箱不存在或密码错误(两者共用,防枚举) | `邮箱或密码错误` | 统一提示「邮箱或密码错误」 | -| 403 | 帐号被管理员禁用 | `帐号已被禁用` | 提示联系管理员 | -| 403 | 密码正确但邮箱尚未确认 | `邮箱尚未验证` | 引导至重发验证流程 | -| 500 | 服务端异常 | `登录失败` | 稍后重试 | - -### 4.5 `POST /auth/logout` — 登出 - -- **认证**:需登录(任意角色) -- **请求体**:无 - -**成功响应**:`200`,同时写入过期 Cookie - -```json -{ - "status_code": 200, - "message": "已登出" -} -``` - -**错误**: - -| 状态码 | 触发条件 | 消息原文 | 前端建议动作 | -|---|---|---|---| -| 401 | 未登录 / token 失效 | 见 3.5 | 本地直接清理登录态即可 | -| 500 | 数据库异常 | `连接数据库发生错误,登出失败` | 提示重试;⚠️ 此时服务端 token 可能仍有效 | - -**注意事项**:登出会使该用户**所有设备**的 token 立即失效(`tokenVersion` 递增),不只是当前会话。 - -### 4.6 `POST /auth/forgot-password` — 申请重置密码 - -- **认证**:无(公开) - -**请求体**(JSON): - -| 参数 | 类型 | 必填 | 说明 | -|---|---|---|---| -| `email` | string | 是 | 服务端去除首尾空白并转为小写;不接受旧字段 `user_email` | - -```json -{ - "email": "watcher@example.com" -} -``` - -**成功响应**:`200` - -```json -{ - "status_code": 200, - "message": "如果该邮箱符合条件,我们将发送密码重置邮件" -} -``` - -未知邮箱、邮箱未确认、被管理员禁用、处于 60 秒冷却期、符合发送条件以及邮件供应商失败,都会收到完全相同的状态码和响应体。只有邮箱已确认、未被禁用且不在冷却期内的用户会触发邮件发送。 - -新签发的 `password_reset` token 有效期为 30 分钟,并使此前未消费的密码重置 token 失效;它与邮箱确认 token 用 purpose 隔离。邮件由 `Opencloud ` 发出,链接到 `https://cloud.catpl.top/reset-password?email=...&token=...`。数据库和 Resend 失败不改变公共响应。 - -**错误**:请求体校验失败返回 `400` zod 形状(2.1)。没有 `/auth/forget-password` 兼容路径。 - -### 4.7 `POST /auth/reset-password` — 重置密码 - -- **认证**:无(公开) - -**请求体**(JSON): - -| 参数 | 类型 | 必填 | 说明 | -|---|---|---|---| -| `email` | string | 是 | 邮件链接中的邮箱;服务端再次规范化 | -| `token` | string | 是 | 邮件链接中的原始 token | -| `new_password` | string | 是 | 新密码;不能与当前密码相同,不额外扩大密码策略 | - -```json -{ - "email": "watcher@example.com", - "token": "0123456789abcdef0123456789abcdef", - "new_password": "new-s3cret-password" -} -``` - -**成功响应**:`200`,同时写入过期认证 Cookie - -```json -{ - "status_code": 200, - "message": "密码重置成功,请使用新密码重新登录" -} -``` - -成功时,服务端在同一数据库事务中消费 token、更新密码哈希和凭据更新时间,并递增 `tokenVersion`。此前在所有设备签发的 JWT 随即失效;响应不会签发新 JWT,用户必须重新登录。 - -token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose、有效期、未消费状态和目标用户;用户在消费时仍须邮箱已确认且未被管理员禁用。重置不会修改邮箱确认时间或管理员禁用状态。 - -| 状态码 | 触发条件 | 消息原文 | 前端建议动作 | -|---|---|---|---| -| 400 | token 未知、过期、已消费、已被替换、用途错误、邮箱不匹配,或用户不再符合状态要求 | `重置链接无效或已过期` | 统一提示链接无效,重新申请 | -| 400 | 新密码与当前密码相同 | `新密码不能与当前密码相同` | 要求输入不同密码 | -| 500 | 数据库或服务端配置异常 | `重置密码失败` | 稍后重试 | - -### 4.8 `PATCH /auth/password` — 修改密码 - -- **认证**:需登录(任意角色) - -**请求体**(JSON): - -| 参数 | 类型 | 必填 | 说明 | -|---|---|---|---| -| `current_password` | string | 是 | 当前密码 | -| `new_password` | string | 是 | 新密码,无强度校验 | - -```json -{ - "current_password": "s3cret-password", - "new_password": "new-s3cret-password" -} -``` - -**成功响应**:`200`,同时写入过期 Cookie - -```json -{ - "status_code": 200, - "message": "密码修改成功,请重新登录" -} -``` - -**错误**: - -| 状态码 | 触发条件 | 消息原文 | 前端建议动作 | -|---|---|---|---| -| 400 | 新旧密码相同 | `新密码不能与当前密码相同` | 表单前置校验可避免 | -| 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 | -| 401 | 当前密码错误 | `当前密码错误` | 提示重新输入 | -| 401 | 未登录 / token 失效 | 见 3.5 | 跳登录 | -| 404 | 用户无凭据记录(数据异常) | `未找到用户凭据` | 反馈后端,附 `X-Request-Id` | -| 500 | 服务端异常 | `修改密码时发生错误` | 稍后重试 | - -**注意事项**:改密成功后**所有已签发 token 立即失效**(含当前使用的这个),前端必须清理登录态并跳转登录页。 +管理员的业务管理操作继续使用第 8 章 `/admin/*` 路由。它们保持原业务响应格式,并在修改角色或密码后撤销目标会话;直接调用 Better Auth Admin 插件端点不会自动执行这些业务路由的附加操作。 --- @@ -605,7 +388,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose ### 5.3 `GET /cloud/:cloud_id` — 云图详情 - **认证**:可选。匿名访客按公开可见规则过滤;上传者本人可见自己的全部云图;admin 可见全部 -- 无效/过期 token 按匿名处理,**不会**返回 401 +- 无效/过期会话凭证按匿名处理,**不会**返回 401 **路径参数**: @@ -663,7 +446,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose | 状态码 | 触发条件 | 消息原文 | 前端建议动作 | |---|---|---|---| -| 401 | 未登录 / token 失效 | 见 3.5 | 引导登录 | +| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 | | 404 | 云图不存在,或不是公开可见状态 | `图片不存在` | 提示云图不可点赞;与「不存在」共用是设计意图 | | 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 | @@ -687,7 +470,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose | 状态码 | 触发条件 | 消息原文 | 前端建议动作 | |---|---|---|---| -| 401 | 未登录 / token 失效 | 见 3.5 | 引导登录 | +| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 | | 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 | **注意事项**:**完全幂等**——从未点赞、云图不存在,同样返回 200。没有 404 分支。 @@ -730,7 +513,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose | 400 | 分辨率超上限 | `图片分辨率超过限制` | 提示缩小尺寸 | | 400 | 其他图片处理失败 | `压缩参数无效` / `图片处理失败` | 提示换图重试 | | 400 | 表单字段校验失败(如 `is_hidden` 编码非法、缺字段) | zod 形状(2.1) | 检查表单 | -| 401 | 未登录 / token 失效 | 见 3.5 | 引导登录 | +| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 | | 413 | 请求体超过 21 MiB | `上传文件过大` | 提示压缩图片 | | 500 | 服务端异常(含存储/数据库失败) | `服务器发生内部错误` | 稍后重试;服务端已做失败补偿清理,可安全重试 | @@ -771,7 +554,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose |---|---|---|---| | 400 | 未提供任何修改字段 | zod 形状,issues 中含 `至少需要提供一个修改字段` | 表单前置校验 | | 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 | -| 401 | 未登录 / token 失效 | 见 3.5 | 引导登录 | +| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 | | 404 | 云图不存在或不属于当前用户 | `图片不存在` | 提示云图不存在 | | 500 | 服务端异常(含 `type_id` 不存在) | `服务器发生内部错误` | 稍后重试 | @@ -785,7 +568,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose | 状态码 | 触发条件 | 消息原文 | 前端建议动作 | |---|---|---|---| -| 401 | 未登录 / token 失效 | 见 3.5 | 引导登录 | +| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 | | 404 | 云图不存在或不属于当前用户 | `图片不存在` | 提示云图不存在 | | 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 | @@ -822,7 +605,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose | 状态码 | 触发条件 | 消息原文 | 前端建议动作 | |---|---|---|---| | 400 | ID 重复 / 数量超出 1–100 / 格式非法 | zod 形状,ID 重复时 issues 含 `图片 ID 不能重复` | 前端去重后提交 | -| 401 | 未登录 / token 失效 | 见 3.5 | 引导登录 | +| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 | | 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 | **注意事项**:**部分成功语义**——只删除属于当前用户的云图,他人或不存在的 ID 被静默跳过,消息中的 `N` 是实际删除数(可能小于请求数,甚至为 0)。前端应以 `N` 为准刷新列表,而不是假设全部删除成功。 @@ -867,7 +650,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose | 状态码 | 触发条件 | 前端建议动作 | |---|---|---| | 400 | Cloud ID 或 ImageVariant 非法 | 检查 URL | -| 401 | 显式提供的 JWT 无效、过期或已失效 | 清除本地登录态 | +| 401 | 显式提供的 Better Auth 会话无效或过期 | 清除本地登录态 | | 404 | Cloud 不存在、当前用户不可见,或对应 MinIO 对象不存在 | 统一显示占位图,不推断具体原因 | | 405 | 使用 GET/HEAD 之外的业务方法;响应含 `Allow: GET, HEAD` | 修正请求方法 | | 500 | 数据库或应用内部错误 | 稍后重试 | @@ -879,7 +662,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose ## 6. 端点 · 个人资料 `/profile` -本章所有端点**都需要登录**(任意角色)。未登录统一返回 401(见 3.5),下文错误表不再重复列出。 +本章所有端点**都需要登录**(任意角色)。未登录统一返回 401(见 3.3),下文错误表不再重复列出。 ### 6.1 `GET /profile/me` — 我的资料 @@ -1102,10 +885,11 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose | 状态码 | 触发条件 | 消息原文 | 前端建议动作 | |---|---|---|---| | 400 | 参数校验失败(`user_role` 非法 / `user_id` 非 uuid) | zod 形状(2.1) | 检查参数 | +| 400 | 管理员尝试撤销自己的 admin 角色 | `不能撤销自己的管理员角色` | 保持当前管理员角色 | | 404 | 用户不存在 | `用户不存在` | 提示用户不存在 | | 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 | -**注意事项**:修改角色会同时递增 `tokenVersion`,**该用户所有已签发 token 立即失效,必须重新登录**(见 3.4)。前端改完角色后可提示目标用户重新登录。 +**注意事项**:修改角色会撤销目标用户的全部 Better Auth 会话,必须重新登录。前端改完角色后可提示目标用户重新登录。 ### 8.4 `PATCH /admin/users/:user_id/password` — 设置用户密码 @@ -1117,7 +901,7 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose | 参数 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---| -| `new_password` | string | 是 | 无强度校验 | 新密码;管理员直接覆盖,无需旧密码 | +| `new_password` | string | 是 | 至少 8 字符 | 新密码;管理员直接覆盖,无需旧密码 | ```json { @@ -1138,15 +922,15 @@ token 必须同时匹配规范化邮箱、HMAC 摘要、`password_reset` purpose | 状态码 | 触发条件 | 消息原文 | 前端建议动作 | |---|---|---|---| -| 400 | 参数校验失败(`user_id` 非 uuid) | zod 形状(2.1) | 检查参数 | +| 400 | 参数校验失败(`user_id` 非 uuid 或密码少于 8 字符) | zod 形状(2.1) | 检查参数 | | 404 | 用户不存在(含凭据记录缺失) | `用户不存在` | 提示用户不存在 | | 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 | -**注意事项**:成功后同样递增 `tokenVersion`,该用户所有已签发 token 立即失效,必须重新登录(见 3.4)。 +**注意事项**:成功后会撤销目标用户的全部 Better Auth 会话,必须重新登录。 ### 8.5 `POST /admin/user` — 创建用户 -> ⚠️ **假端点**(2.3 第 4 条):通过 zod 校验后**原样回显请求体(含明文密码)**,不创建任何用户。响应为 `200 { name, email, password, role }`。请勿接入,更不要在任何持久化日志中记录其响应。 +管理员认证后提交 `{ "name": "Alice", "email": "alice@example.com", "password": "password123", "role": "user" }`。通过 Better Auth Admin 插件创建用户和密码凭据,再尝试发送邮箱验证邮件。成功返回 `201`,包含 `status_code`、`message`、`user_id`、`email_sent`;用户名或邮箱冲突返回 `409`。密码至少 8 字符。 ### 8.6 `POST /admin/cloudtypes` — 添加云类型