release: bump version to 0.52.0
This commit is contained in:
155
docs/plans/user-registration-email-verification-plan.md
Normal file
155
docs/plans/user-registration-email-verification-plan.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 用户公开注册与邮箱验证计划
|
||||
|
||||
**状态**:待实施
|
||||
**创建日期**:2026-05-12
|
||||
**核心目标**:给 Planet 增加公开注册流程 + 邮箱验证码 + 忘记密码,让客户无需管理员介入就能开通账号;同时把 SMTP 邮件作为可复用基础服务接入 `/settings`。
|
||||
|
||||
## 背景
|
||||
|
||||
当前认证只暴露 `/auth/login`、`/auth/refresh`、`/auth/logout`、`/auth/me`(`backend/app/api/v1/auth.py`),账号只能由 `super_admin` 在 `/users` 后台创建。User 模型 `backend/app/models/user.py` 没有 `email_verified` 字段,仓库也没有任何 SMTP/邮件发送基础设施。
|
||||
|
||||
客户旅程想从"打开浏览器→注册→验证→登录"开始走(见 [文档受众分层重构计划](/home/ray/dev/linkong/planet/docs/plans/docs-audience-split-plan.md)),就必须先把这条链路在代码里跑通。
|
||||
|
||||
注册策略(已确认):
|
||||
|
||||
- 开放公开注册,任何人可在 `/register` 自助开通
|
||||
- 默认角色 `viewer`
|
||||
- 邮箱验证后立即可登录(无需管理员审批)
|
||||
- 验证仅走 SMTP 邮件,6 位数字码,10 分钟 TTL
|
||||
|
||||
## 数据模型
|
||||
|
||||
`backend/app/models/user.py` 加两列:
|
||||
|
||||
```python
|
||||
email_verified = Column(Boolean, default=False, nullable=False)
|
||||
pending_email = Column(String(255), nullable=True) # 改邮箱时临时落地待验证地址
|
||||
```
|
||||
|
||||
迁移路径:仓库目前没看到 alembic 目录,沿用 `backend/app/db/session.py` 的初始化风格在启动时跑 `ALTER TABLE users ADD COLUMN IF NOT EXISTS ...`。先确认是否存在 alembic,若有则正规迁移。
|
||||
|
||||
不另建 `verification_codes` 表 — OTP 走 **Redis**(系统已有 Redis,token blacklist 也走 Redis):
|
||||
|
||||
```
|
||||
key: otp:{purpose}:{email} purpose ∈ {register, verify_email, reset_password}
|
||||
value: { code_hash: bcrypt, attempts: int, issued_at: ts }
|
||||
TTL: 600 秒
|
||||
```
|
||||
|
||||
`{purpose}:{email}` 同时配一个限流键 `otp_rate:{purpose}:{email}`,TTL 60 秒,用于"60 秒内禁止重发"。
|
||||
|
||||
## 服务拆分
|
||||
|
||||
按项目 `services/` 单职责风格拆两个:
|
||||
|
||||
**`backend/app/services/otp.py`**(通用 OTP 原语,未来 2FA / 手机号验证可直接复用):
|
||||
|
||||
```python
|
||||
async def issue_code(email: str, purpose: OtpPurpose) -> str # 生成 6 位、写 Redis、返回明码(调用方负责送达)
|
||||
async def verify_code(email: str, purpose: OtpPurpose, code: str) -> bool # 校验并消耗
|
||||
async def check_resend_allowed(email: str, purpose: OtpPurpose) -> None # 抛 RateLimited 异常
|
||||
```
|
||||
|
||||
- 6 位数字,密码学随机
|
||||
- Redis 存 `bcrypt(code)`,不存明码
|
||||
- 校验失败计数 ≥ 5 直接失效该 key
|
||||
- 重发触发即失效旧 code
|
||||
|
||||
**`backend/app/services/email.py`**(通用 SMTP 发送,告警/摘要等后续可复用):
|
||||
|
||||
```python
|
||||
async def send_email(to: str, subject: str, html: str, text: str | None = None) -> None
|
||||
async def send_verification_email(to: str, code: str, purpose: OtpPurpose) -> None # 模板封装
|
||||
```
|
||||
|
||||
- 用 `aiosmtplib` 异步发送
|
||||
- 从 `system_settings` 的 `smtp` 命名空间读配置(host/port/username/password/from/use_tls)
|
||||
- 未配置抛 `EmailNotConfiguredError`
|
||||
- 模板用简单 HTML + 纯文本双段,按 `purpose` 切换文案
|
||||
|
||||
编排("签码 → 发邮件")在 `api/v1/auth.py` 端点里调两个服务,不在 service 内互相调用,保持单测可单独 mock。
|
||||
|
||||
## 后端端点
|
||||
|
||||
新增到 `backend/app/api/v1/auth.py`:
|
||||
|
||||
| 端点 | 入参 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| `POST /auth/register` | `username, email, password` | 用户名/邮箱查重 → 写 User `is_active=True, email_verified=False, role="viewer"` → 调 `otp.issue_code(email, "register")` → 调 `email.send_verification_email` |
|
||||
| `POST /auth/verify-email` | `email, code` | `otp.verify_code` → 置 `email_verified=True` → 直接返回 access/refresh token |
|
||||
| `POST /auth/resend-code` | `email, purpose` | `check_resend_allowed` → `issue_code` → `send_verification_email` |
|
||||
| `POST /auth/forgot-password` | `email` | 即便邮箱不存在也返回 200(防枚举);存在则签 `reset_password` 码并发邮件 |
|
||||
| `POST /auth/reset-password` | `email, code, new_password` | `verify_code(..., "reset_password")` → `user.set_password(new_password)` |
|
||||
|
||||
`/auth/login` 改造:邮箱未验证用户登录返回 `403 { code: "EMAIL_NOT_VERIFIED", email }`,前端拿到后跳验证页。
|
||||
|
||||
## SMTP 设置
|
||||
|
||||
复用 `backend/app/api/v1/settings.py` 现有 setting store,新增 `smtp` 命名空间:
|
||||
|
||||
- `smtp_host`、`smtp_port`、`smtp_username`、`smtp_password`、`smtp_from`、`smtp_from_name`、`smtp_use_tls`
|
||||
- 密码走与 collector 凭证相同的加密路径(看 `backend/app/services/` 是否已有 `credentials_encryption` 之类工具,若有直接复用)
|
||||
- `POST /settings/smtp/test` — 用当前未保存的入参试发一封到指定地址,不落库
|
||||
|
||||
未配置 SMTP 时 `/auth/register` 应返回明确错误 `503 { code: "EMAIL_PROVIDER_NOT_CONFIGURED" }`,提示管理员先去 `/settings` 配 SMTP 或用 `./planet.sh createuser` 兜底。
|
||||
|
||||
## 前端
|
||||
|
||||
**新页面**:
|
||||
|
||||
- `frontend/src/pages/Register/Register.tsx` — 两步表单:(1) 用户名/邮箱/密码 (2) 6 位验证码;60s 重发冷却;验证成功写 token,自动跳 `/admin`
|
||||
- `frontend/src/pages/VerifyEmail/VerifyEmail.tsx` — 给登录拦截 `EMAIL_NOT_VERIFIED` 时落地的页,仅"输码 + 重发"
|
||||
- `frontend/src/pages/ForgotPassword/ForgotPassword.tsx` — 两步:(1) 输邮箱 (2) 输码 + 新密码
|
||||
|
||||
**改动**:
|
||||
|
||||
- `frontend/src/pages/Login/Login.tsx` — 表单下加"注册账号"、"忘记密码"链接;接 `EMAIL_NOT_VERIFIED` 跳 `/verify-email`
|
||||
- `frontend/src/pages/Settings/Settings.tsx` — 新增 SMTP 子 tab(host/port/username/password/from/TLS + 测试发送按钮),用工作区里新建的 `frontend/src/components/ConnectionTestInput/` 做连通测试输入
|
||||
- 路由表(`frontend/src/App.tsx` 或 `frontend/src/router/*`)— 加 `/register`、`/forgot-password`、`/verify-email`
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
后端:
|
||||
|
||||
- `backend/app/models/user.py` — 加字段
|
||||
- `backend/app/schemas/user.py` — 新增 `UserRegister`、`VerifyCode`、`ResetPasswordRequest` schema
|
||||
- `backend/app/api/v1/auth.py` — 新端点 + 登录校验
|
||||
- `backend/app/services/otp.py` *(新)*
|
||||
- `backend/app/services/email.py` *(新)*
|
||||
- `backend/app/api/v1/settings.py` — SMTP 命名空间 + 测试发送
|
||||
- `backend/app/core/config.py` — SMTP 默认值/特性开关
|
||||
- `backend/app/db/session.py` — DDL 兜底(若无 alembic)
|
||||
|
||||
前端:
|
||||
|
||||
- `frontend/src/pages/Register/Register.tsx` *(新)*
|
||||
- `frontend/src/pages/VerifyEmail/VerifyEmail.tsx` *(新)*
|
||||
- `frontend/src/pages/ForgotPassword/ForgotPassword.tsx` *(新)*
|
||||
- `frontend/src/pages/Login/Login.tsx`
|
||||
- `frontend/src/pages/Settings/Settings.tsx`
|
||||
- 路由文件
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. 后端:User 模型字段 + DDL 兜底
|
||||
2. 后端:`services/otp.py`(先纯单测跑通)
|
||||
3. 后端:`services/email.py`(用 MailHog 本地试发)
|
||||
4. 后端:`/auth/register` + `/auth/verify-email` + `/auth/resend-code` + 登录拦截
|
||||
5. 后端:`/auth/forgot-password` + `/auth/reset-password`
|
||||
6. 后端:`/settings/smtp` 命名空间 + 测试发送
|
||||
7. 前端:`Settings.tsx` 加 SMTP 子 tab
|
||||
8. 前端:Register / VerifyEmail / ForgotPassword 页 + Login 入口
|
||||
|
||||
文档同步在 [文档受众分层重构计划](/home/ray/dev/linkong/planet/docs/plans/docs-audience-split-plan.md) 落地。
|
||||
|
||||
## 验证
|
||||
|
||||
- **后端单测**(仿 `backend/tests/test_settings_ai_provider.py`):
|
||||
- 注册端点用户名/邮箱查重
|
||||
- OTP 过期、错码计数、重发限流
|
||||
- 邮箱未验证用户登录返回 `EMAIL_NOT_VERIFIED`
|
||||
- SMTP 未配置时注册端点返回 `EMAIL_PROVIDER_NOT_CONFIGURED`
|
||||
- 忘记密码对不存在邮箱仍返回 200
|
||||
- **后端集测**:本机起 MailHog 或 Mailtrap,把 SMTP 指到上面,跑 register → 收码 → verify → login 一遍
|
||||
- **前端**:`source ~/.zshrc && bun run build`;启 dev server 走 `/register` → `/verify-email` → `/admin` 全流程,再试 `/forgot-password`
|
||||
- **手测**:新邮箱注册 → 收码 → 输错 → 重发 → 输对 → 登录 → 改密码 → 用新密码再登;管理员在 `/settings` 改 SMTP → 测试发送
|
||||
Reference in New Issue
Block a user