短信登录关掉后,生产没有任何注册入口——这一版补上最普通的那条路:
填邮箱、收验证码、设密码,注册完直接进首页。
先发码再注册,不是"注册时顺带发信"
POST /auth/email/code 只做三件事:查重、限流、发码。
填完密码才被告知"这个邮箱注册过了"最气人,所以占用(2019)在发码前就回。
邮箱既是账号名也是唯一联系方式,所以必须真验一次码
验证通过时间落 app_user.email_verified_at,/auth/me 回 email_verified。
"随手填一个"会让账号变成没人能找回的孤岛。
app_user.phone 改为可空(迁移 0011)
邮箱注册的账号本来就没有手机号。user.phone 对这类账号回空串,
成员列表同理,不再伪装成一串星号。
防刷
同邮箱 60 秒一次(2022)、验证码 10 分钟有效、错 5 次即作废(2021)。
6 位数可以被刮,不设上限等于把注册口子交给脚本。
本机联调开关 EMAIL_CODE_DEV_ECHO
为 true 时把码回给调用方且不发真信——否则每跑一次 e2e 就往真实收件箱塞一封。
生产打开会让 validate_runtime() 拒绝启动。
前端
新增 /register(邮箱、验证码、密码、确认、昵称;60 秒倒计时;
服务端关着注册时整页只说"联系管理员开通"),登录页按 /auth/methods
决定要不要显示「还没有账号?用邮箱注册」。
验证
pytest 139 passed、ruff check / format 全绿
tsc --noEmit 0、vite build 通过
e2e register.mjs 7 项:登录页入口 → 拿码 → 注册落进首页 → 退出用邮箱+密码登回来
→ 同一邮箱再发码回 2019 → 控制台零报错
e2e login-email-smtp.mjs 15 项回归通过(含真发一封测试信)
e2e account.mjs 14 项、knowledge.mjs 全绿
顺带修的两处测试
test_account 的字段白名单补 email_verified(接口确实多了这个字段);
test_config_prod 用 Settings(_env_file=None, ...) 构造,别被本机 .env 里的
EMAIL_CODE_DEV_ECHO=true 带偏——那个开关在生产是硬错误,本机却是跑 e2e 必需的。
This commit is contained in:
+41
-5
@@ -37,6 +37,8 @@
|
||||
| 2018 | 422 | 邮箱格式不对 | 就地提示 |
|
||||
| 2019 | 409 | 邮箱已被别的账号占用 | 换一个 |
|
||||
| 2020 | 502 | 发信失败(SMTP 认证/收件人/网络) | 去「系统设置」核对授权码 |
|
||||
| 2021 | 400 | 邮箱验证码不对 / 已过期 / 试错次数用尽 | 重新获取验证码 |
|
||||
| 2022 | 429 | 验证码发送过于频繁 | 等 60 秒再发 |
|
||||
| 3001 | 404 | 工作区不存在 | — |
|
||||
| 3003 | 403 | 不是该工作区成员 | **不要**提示"工作区不存在" |
|
||||
| 3004 | 403 | 角色权限不足 | 隐藏对应按钮 |
|
||||
@@ -69,19 +71,53 @@
|
||||
|
||||
## 1. 认证 `/auth`
|
||||
|
||||
登录方式有三种,**默认只开密码那一种**。服务端把"现在开着哪些"告诉前端,前端据此渲染,
|
||||
免得用户点进一条关着的路:
|
||||
登录方式有四种,**默认只开「密码」和「邮箱注册」**。服务端把"现在开着哪些"告诉前端,
|
||||
前端据此渲染,免得用户点进一条关着的路:
|
||||
|
||||
### `GET /auth/methods`
|
||||
|
||||
```json
|
||||
{ "password": true, "email_password": true, "sms_code": false }
|
||||
{ "password": true, "email_password": true, "sms_code": false, "email_register": true }
|
||||
```
|
||||
|
||||
`SMS_LOGIN_ENABLED=false`(默认)时,`POST /auth/sms/send` 与 `POST /auth/login` 都回 `2017`
|
||||
「短信验证码登录暂未开放」。理由:dev 环境的固定码等于"知道手机号就能进任何账号",
|
||||
这条路必须显式打开,生产打开时还会强制要求真短信通道(见 `deploy` 文档的启动校验)。
|
||||
|
||||
### `POST /auth/email/code`
|
||||
|
||||
```json
|
||||
{ "email": "someone@example.com" }
|
||||
```
|
||||
|
||||
发一封注册验证码(6 位数字,`EMAIL_CODE_TTL_SECONDS` 默认 600 秒有效)。
|
||||
邮箱统一按小写处理;格式不对 → `2018`;**这个邮箱已经注册过 → `2019`**(让用户在填完密码前就知道);
|
||||
60 秒内重复请求 → `2022`;发信配置没填或发不出去 → `2020`(不会假装发出去);
|
||||
`EMAIL_REGISTER_ENABLED=false` → `2017`。
|
||||
|
||||
响应 `data: { sent: true, expires_in: 600 }`。**真发信时不回显验证码**;
|
||||
只有本机联调(`EMAIL_CODE_DEV_ECHO=true`)时才带 `dev_code` 且不发信——这个开关在生产会直接拒绝启动。
|
||||
|
||||
### `POST /auth/register`
|
||||
|
||||
```json
|
||||
{ "email": "someone@example.com", "code": "123456", "password": "dadao2026", "nickname": "大道" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "user": { "id": "…", "phone": "", "email": "someone@example.com", "email_verified": true, … },
|
||||
"tokens": { "access_token": "…", "refresh_token": "…", "token_type": "bearer", "expires_in": 1800 } }
|
||||
```
|
||||
|
||||
注册成功即登录态(直接返回 `tokens`)。规则:
|
||||
|
||||
- 验证码不对 / 过期 / 试错超过 `EMAIL_CODE_MAX_ATTEMPTS`(默认 5)次 → `2021`;用完即作废,不能重放
|
||||
- 密码强度不够 → `2015`;邮箱已被占用 → `2019`;`EMAIL_REGISTER_ENABLED=false` → `2017`
|
||||
- 昵称不填就取邮箱 `@` 前面那段
|
||||
- 建出来的账号**没有手机号**(`app_user.phone` 可空),所以 `user.phone` 回空串,不是星号串
|
||||
|
||||
邮箱是这类账号的唯一账号名与联系方式,所以注册必须真验一次码——"随手填一个"会让账号变成没人能找回的孤岛。
|
||||
|
||||
### `POST /auth/sms/send`
|
||||
|
||||
```json
|
||||
@@ -153,8 +189,8 @@
|
||||
### `GET /auth/me`
|
||||
|
||||
返回 `{ user, workspaces[] }`,`workspaces[].role` 是当前用户在该工作区的角色。
|
||||
`user` 里带 `has_password`(前端据此显示"设置密码"还是"修改密码")、`email`、`platform_admin`
|
||||
(能不能进「系统设置」)。
|
||||
`user` 里带 `has_password`(前端据此显示"设置密码"还是"修改密码")、`email`、
|
||||
`email_verified`(这个邮箱是不是真验过)、`platform_admin`(能不能进「系统设置」)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -603,3 +603,48 @@
|
||||
自动注册"造出来,关掉连 e2e 都造不出测试用户。生产是关的,所以生产**暂时没有注册入口**——
|
||||
要开放注册得先做「邮箱验证码注册 / 邀请注册」,这是下一步的事。
|
||||
- 生产实拍:`2.Design/screens/prod-account-email.png`、`prod-settings-smtp.png`。
|
||||
|
||||
## 10. 邮箱验证码注册(2026-11-03)
|
||||
|
||||
### 10.1 需求
|
||||
|
||||
手机上短信登录关掉后,生产没有注册入口。用户原话:"默认用邮件验证码注册吧;"
|
||||
|
||||
### 10.2 决定
|
||||
|
||||
- **先发码再注册**:`POST /auth/email/code` → `POST /auth/register`,两步而不是"注册时顺带发信"。
|
||||
理由:用户填完密码才被告知"这个邮箱注册过了"最气人,所以发码前就查重(`2019`)。
|
||||
- **邮箱既是账号名也是唯一联系方式**:注册必须真验一次码,"随手填一个"会让账号变成没人能找回的孤岛。
|
||||
验证通过时间落 `app_user.email_verified_at`,`/auth/me` 回 `email_verified`。
|
||||
- **`app_user.phone` 改为可空**(迁移 0011):邮箱注册的账号本来就没有手机号。
|
||||
老账号不受影响;`user.phone` 对这类账号回空串而不是星号串(成员列表同理)。
|
||||
- **防刷**:同邮箱 60 秒内只能发一次(`2022`);验证码 10 分钟有效;错 5 次即作废(`2021`)——
|
||||
6 位数可以被刮,不设上限等于把注册口子交给脚本。
|
||||
- **本机联调开关** `EMAIL_CODE_DEV_ECHO`:为 true 时把码直接回给调用方且**不发真信**,
|
||||
否则每跑一次 e2e 就往真实收件箱塞一封。生产打开会让 `validate_runtime()` 拒绝启动。
|
||||
|
||||
### 10.3 后端
|
||||
|
||||
| 位置 | 内容 |
|
||||
|---|---|
|
||||
| `app/config.py` | `email_register_enabled`、`email_code_ttl_seconds=600`、`email_code_throttle_seconds=60`、`email_code_max_attempts=5`、`email_code_dev_echo`(生产校验拒绝) |
|
||||
| `app/errors.py` | `EMAIL_CODE_INVALID=2021`(400)、`EMAIL_CODE_TOO_FREQUENT=2022`(429) |
|
||||
| `app/models.py` | `AppUser.phone` 可空、`email_verified_at`;`AuthUser.phone: str \| None` |
|
||||
| `alembic/versions/0011_email_signup.py` | `phone DROP NOT NULL` + `email_verified_at` + GRANT(downgrade 先给空手机号补占位值) |
|
||||
| `app/stores.py` | `email_code_key` / `email_tries_key` / `email_throttle_key` |
|
||||
| `app/services/mail.py` | `send_verify_code(session, to=…, code=…, minutes=…)`,纯文本 + HTML 两版 |
|
||||
| `app/api/auth.py` | `POST /auth/email/code`、`POST /auth/register`、`/auth/methods` 加 `email_register` |
|
||||
|
||||
### 10.4 Web
|
||||
|
||||
- `pages/Register.tsx`(新):邮箱 / 验证码(60 秒倒计时)/ 密码(前端先按同一套规则提示)/ 确认密码 / 昵称(可不填,留空用邮箱前缀);
|
||||
注册成功直接写令牌进首页。服务端关了注册时整页只显示"联系管理员开通"。
|
||||
- `pages/Login.tsx`:`/auth/methods` 里 `email_register` 为真才显示「还没有账号?用邮箱注册」。
|
||||
- `App.tsx`:新增 `/register` 路由。
|
||||
|
||||
### 10.5 验证
|
||||
|
||||
- `backend/tests/test_email_signup.py`(9 例):真走 SMTP(monkeypatch 记账)、dev echo 不发信、
|
||||
注册即登录、码用完不能重放、错码/过期、试错上限、邮箱占用、发码节流、弱密码、注册关闭时两条接口都回 2017。
|
||||
- e2e `web/e2e/register.mjs`:登录页进注册页 → 拿码(dev echo)→ 注册 → 落进首页 → 退出用「邮箱 + 密码」登回来 →
|
||||
同一个邮箱再发码回 `2019`。
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Reference in New Issue
Block a user