76853452f6
include AGENTS guidance updates, plan doc replacements, and utility script changes left in working tree
205 lines
5.6 KiB
Markdown
205 lines
5.6 KiB
Markdown
# 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。
|
||
- [ ] 后端与前端相关测试通过,文档已同步。
|