# `/api/auth/login-options` 登录方式选项设计 日期:`2026-04-21` ## 1. 文档目的 这份文档用于冻结 Rust `api-server` 首版 `GET /api/auth/login-options` 的返回 contract、配置来源与当前阶段边界,确保前端在登录页读取“当前可用登录方式”时,不需要依赖硬编码开关。 ## 2. 当前目标 当前阶段只解决一件事: 1. 由 `Axum` 根据服务端配置,返回当前环境启用的登录方式列表。 2. 密码登录入口由 Rust `password_entry` 固定承载,作为登录弹窗的保底入口。 本阶段明确不包含: 1. 短信或微信登录链路本身是否已经完整落地 2. 对前端返回更细粒度的 provider 配置 3. 第三方登录按钮文案、图标或 UI 布局规则 ## 3. 接口 contract ### 3.1 请求 1. 方法:`GET` 2. 路径:`/api/auth/login-options` 3. 鉴权:不需要 4. 请求体:空 ### 3.2 成功响应 ```json { "availableLoginMethods": ["phone", "password", "wechat"] } ``` 字段说明: 1. `availableLoginMethods` 为字符串数组 2. 当前阶段只允许出现: - `phone` - `password` - `wechat` ### 3.3 返回顺序 返回顺序固定为: 1. 先 `phone` 2. 再 `password` 3. 再 `wechat` 这样可以保证前端按钮顺序稳定,不因配置解析顺序变化而漂移。 ## 4. 配置来源 `api-server` 只读取以下布尔配置: 1. `SMS_AUTH_ENABLED` 2. `WECHAT_AUTH_ENABLED` 映射规则固定为: 1. `SMS_AUTH_ENABLED=true` 时返回 `phone` 2. Rust 密码登录主链可用时固定返回 `password` 3. `WECHAT_AUTH_ENABLED=true` 时返回 `wechat` 4. 短信与微信都关闭时仍返回 `["password"]` ## 5. crate 边界 ### 5.1 `api-server` 负责: 1. 读取 `AppState.config` 2. 组装 `availableLoginMethods` 3. 返回项目兼容的响应 envelope ### 5.2 `module-auth` 本接口当前阶段不依赖 `module-auth`。 ### 5.3 前端 负责: 1. 根据 `availableLoginMethods` 决定是否展示手机号 / 微信入口 2. 不再假设某种登录方式一定存在 3. 若 `/api/auth/login-options` 联调失败或返回空数组,前端仍保留 `password` 入口,避免登录弹窗显示“当前登录入口暂不可用”后无法继续操作。 ## 6. 测试要求 至少覆盖: 1. 默认配置下返回 `["password"]` 2. 同时启用短信、密码与微信时返回 `["phone", "password", "wechat"]` 3. 前端在 `login-options` 读取失败或返回空数组时,仍展示密码登录表单 ## 7. 完成定义 满足以下条件时,本任务视为完成: 1. Rust 已提供 `GET /api/auth/login-options` 2. 响应字段命名与前端约定一致 3. 配置开关可稳定映射到返回数组 4. 文档、任务清单与测试已同步更新 ## 8. 2026-05-01 前端降级修复记录 本地联调时若 `api-server` 未启动或 Vite 代理暂时返回 `500`,`GET /api/auth/login-options` 会失败。前端必须继续遵循第 5.3 节约束: 1. `AuthGate` 在 `login-options` 读取失败时设置 `availableLoginMethods = ["password"]`。 2. 该失败只代表登录方式配置探测失败,不代表登录功能不可用,因此不把 `读取登录方式失败` 写入登录弹窗错误条。 3. 登录弹窗仍展示密码登录表单,玩家可继续登录后进入创作链路。 4. 本地仍需要启动 `api-server`,否则后续 `POST /api/auth/entry` 等真实登录请求无法完成。 ## 9. 2026-05-07 本地短信入口恢复记录 如果登录弹窗里短信登录页签“像是被删了”,先不要改前端表单,优先检查本地登录方式探测结果: 1. 仓库根目录 `.env.local` 里必须显式保留 `SMS_AUTH_ENABLED=true`。 2. 本地启动请优先使用 `npm run api-server`、`npm run dev:rust` 或 `npm run dev`,这些脚本会按“shell 环境优先、`.env.local` 覆盖 `.env`”合并配置。 3. 若 `GET /api/auth/login-options` 只返回 `["password"]`,说明短信入口没有被服务端配置打开,前端只是按 contract 正常降级。 4. 当 `SMS_AUTH_ENABLED=true` 生效时,`GET /api/auth/login-options` 至少应返回 `["phone", "password"]`,短信登录页签才会重新出现。 ## 10. 2026-05-07 前端代理端口错配修复记录 如果 Rust API 直连返回 `["phone", "password"]`,但从前端域名请求 `GET /api/auth/login-options` 返回 `500`,短信页签同样会消失。此时不是登录 UI 被删除,而是 `AuthGate` 按第 5.3 节降级成 `["password"]`。 本地排查顺序固定为: 1. 先请求 `http://127.0.0.1:3000/api/auth/login-options`,确认前端代理是否成功返回 JSON。 2. 再请求当前 Rust API 目标,例如 `http://127.0.0.1:3100/api/auth/login-options` 或 `http://127.0.0.1:8082/api/auth/login-options`。 3. 若直连 API 成功而 3000 返回 `500`,检查 `RUST_SERVER_TARGET`、`GENARRATIVE_API_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 是否指向仍在监听的 API 端口。 4. `npm run dev` / `npm run dev:rust` 完整栈默认由脚本计算 API 端口;加载 `.env.local` 给后端使用后,脚本必须重新固定 `RUST_SERVER_TARGET`,避免 `.env.local` 中的旧代理目标覆盖本次启动的实际 API 端口。 5. `npm run dev:web` 只启动前端,不会自动拉起 Rust API;如果 `.env.local` / 当前环境已经显式声明 `GENARRATIVE_RUNTIME_SERVER_TARGET`、`RUST_SERVER_TARGET`、`GENARRATIVE_API_TARGET` 或 `GENARRATIVE_API_PORT`,脚本必须固定使用该目标。目标当下不可用时只打印警告,不自动切到另一个端口,避免前端进程长时间绑定到随后会停掉的临时 API。 6. 如果 `3000` 仍然返回 `500`,先确认浏览器是不是还开着旧的前端进程。当前脚本如果因为端口占用漂移到 `3001` / `3002`,应直接关掉旧进程后重启,而不是继续用旧的 3000 页面判断登录入口状态。 ## 11. 2026-05-10 `npm run api-server` 环境加载与短信 provider 排查记录 本地单独启动 `api-server` 时,环境变量合并顺序固定为: ```text 外层 shell > .env > .env.local > .env.secrets.local ``` 这保证 `.env.local` 能覆盖 `.env.example` 派生出的默认值,`.env.secrets.local` 能继续覆盖本地私密密钥配置。`scripts/api-server-dev.mjs` 不得让 `.env` 后加载并覆盖 `.env.local`,否则 `SMS_AUTH_ENABLED` 或 `SMS_AUTH_PROVIDER` 可能被压回错误值。 排查“点击获取验证码但手机收不到短信”时,除了确认 `availableLoginMethods` 包含 `phone`,还必须确认当前进程实际使用的 provider: 1. `SMS_AUTH_PROVIDER="mock"` 只用于本地 UI / 账号链路联调,不会向手机发送真实短信;此时应使用 `SMS_AUTH_MOCK_VERIFY_CODE`,默认 `123456`。 2. 真实短信链路必须使用 `SMS_AUTH_PROVIDER="aliyun"`,并在修改 `.env.local` 后重启 `api-server`,运行中的进程不会自动切换 provider。 3. 真实 provider 是否被使用,以 `api-server` 日志中的 `provider=aliyun`、`provider_request_id` 和 `provider_out_id` 为准。