# Auth UX Enhancement Design **日期**: 2026-02-26 **状态**: 可实施(修订版) ## 目标 本次改动聚焦 4 个问题: 1. 注册验证码页增加首次提示,降低用户困惑。 2. 增加忘记密码流程(验证码模式)。 3. 注册页增加邀请码输入(前端收集,后端暂不消费)。 4. 修复用户名非唯一导致的用户查询问题,改为搜索接口。 ## 非目标 - 不实现邀请码校验/入库。 - 不改动 Supabase 邮件模板基础设施(当前 self-hosted 已配置 recovery 模板 URL)。 - 不在本次引入新的认证机制(仅沿用 Supabase OTP + session)。 --- ## 1. 忘记密码的可落地后端方案(对外两步) ### 1.1 约束说明(关键) 当前 Python SDK 的 `verify_otp` 参数模型不支持在验码时直接携带 `new_password`。因此不能走“单接口验码并改密”的实现。 ### 1.2 可执行流程 对客户端暴露两步流程,第二步在后端内部执行两段动作: 1. `POST /auth/password-reset`:调用 Supabase `reset_password_email` 发送 recovery 验证码。 2. `POST /auth/password-reset/confirm`:接收 `email + token + new_password`,后端内部先调用 `verify_otp(type="recovery")`,再基于该会话调用 `update_user(password=...)`。 这样既匹配 SDK 能力(`verify_otp` 不支持直接带 `new_password`),又保持前端体验为两步。 ### 1.3 API 设计 #### POST /auth/password-reset 发送重置验证码。 Request ```json { "email": "string(email)", "redirect_to": "string(optional)" } ``` Response: `204 No Content` Errors: - `422` 参数错误 - `429` 频率受限 #### POST /auth/password-reset/confirm 验证 recovery 验证码并完成改密。 Request ```json { "email": "string(email)", "token": "string(6 digits)", "new_password": "string(min 6)" } ``` Response: `204 No Content` Errors: - `401` 验证码无效或过期 - `422` 参数错误 - `429` 频率受限 ### 1.4 安全边界 - 用户档案更新走 `users` 域(`/users/me` -> `UserService` -> `Profile`),仅允许公开资料字段。 - 密码修改走 `auth` 域(Supabase Auth),不复用 `users` service/repository。 - `POST /auth/password-reset/confirm` 必须在同一请求内完成“验码 + 改密”,禁止单独暴露“仅改密”接口。 - 即使伪造 `/users/me` 请求,也无法触发密码修改路径。 --- ## 2. 前端忘记密码流程 流程: `登录页` -> `忘记密码页(输入邮箱)` -> `验证码+新密码页` -> `返回登录并用新密码登录` 关键点: - 第二步页面一次提交 `email + token + new_password` 到 `/auth/password-reset/confirm`。 - 所有用户反馈统一使用 `Toast`(遵循 `apps/AGENTS.md`)。 - 错误提示优先展示后端 `detail`。 --- ## 3. 注册 UX 优化 ### 3.1 验证码发送提示 在 `register_verification_screen.dart` 首次进入页面显示: `验证码已发送,如未收到请检查垃圾邮件或确认邮箱已注册` ### 3.2 邀请码输入 在注册页新增可选字段: - Label: `邀请码(选填)` - Hint: `请输入邀请码` 前端请求体可携带 `invite_code`,后端忽略该字段,不返回错误。 --- ## 4. 用户搜索 Bug 修复 ### 4.1 问题 `GET /users/{username}` 隐含“用户名唯一”假设,实际不成立。 ### 4.2 方案 后端删除 `GET /users/{username}`,改为 `POST /users/search`。 Request ```json { "query": "string(1-100)" } ``` Response ```json [ { "id": "string", "username": "string", "avatar_url": "string|null", "bio": "string|null" } ] ``` 查询策略: - username: 模糊匹配(`ilike`) - email: 精确匹配 - 最多返回 20 条 - 返回公开字段,不返回 email ### 4.3 前端联动 必须同步迁移 `apps/lib/features/users/data/*`(`getByUsername` -> `searchUsers`),否则删除后端旧路由后前端会直接 404。 --- ## 5. 主要改动文件 ### 后端 - `backend/src/v1/auth/schemas.py` - `backend/src/v1/auth/service.py` - `backend/src/v1/auth/gateway.py` - `backend/src/v1/auth/router.py` - `backend/src/v1/users/schemas.py` - `backend/src/v1/users/repository.py` - `backend/src/v1/users/service.py` - `backend/src/v1/users/router.py` - `backend/tests/integration/test_auth_routes.py` - `backend/tests/integration/test_users_routes.py` ### 前端 - `apps/lib/features/auth/ui/screens/login_screen.dart` - `apps/lib/features/auth/ui/screens/register_screen.dart` - `apps/lib/features/auth/ui/screens/register_verification_screen.dart` - `apps/lib/features/auth/ui/screens/forgot_password_screen.dart`(新增) - `apps/lib/features/auth/ui/screens/reset_password_screen.dart`(新增) - `apps/lib/features/auth/presentation/cubits/forgot_password_cubit.dart`(新增) - `apps/lib/features/auth/presentation/cubits/reset_password_cubit.dart`(新增) - `apps/lib/features/auth/data/auth_api.dart` - `apps/lib/features/auth/data/auth_repository.dart` - `apps/lib/features/auth/data/auth_repository_impl.dart` - `apps/lib/features/users/data/users_api.dart` - `apps/lib/features/users/data/users_repository.dart` - `apps/lib/features/users/data/users_repository_impl.dart` - `apps/lib/core/router/app_router.dart` ### 文档 - `docs/runtime/runtime-route.md`(按 AGENTS 规则必须同步) --- ## 6. 验收标准 - [ ] 注册验证码页首次进入显示提示。 - [ ] 登录页出现“忘记密码”入口。 - [ ] 忘记密码流程可完整走通(发码、确认改密、重新登录)。 - [ ] 注册页可输入邀请码且不影响注册。 - [ ] `GET /users/{username}` 被移除。 - [ ] `POST /users/search` 可用且返回不含 email。 - [ ] 后端与前端相关测试通过,文档已同步。