feat(auth): 短信登录默认关,换成手机号/邮箱 + 密码,发信邮箱进后台
CI / backend (push) Canceled after 0s

用户要求:内测的固定验证码 123456 等于"知道手机号就能进任何账号",先关掉;
登录改用账号密码,并多一种"邮箱当账号名"的方式;发信邮箱要能在后台改。

短信那条路(默认关):
- `SMS_LOGIN_ENABLED=false`,`/auth/sms/send` 与 `/auth/login` 统一回 2017,不是静默失败;
- 生产校验改成"开着短信登录就必须配真通道",不开时 provider 是 dev 也不拦;
- 新增 `GET /auth/methods`,登录页据此渲染,关着的路不出现(少一次"点进去才发现不能用")。

邮箱登录:
- `app_user.email` + `uq_app_user_email`(`lower(email)` 部分索引),登录与绑定都归一成小写;
- `POST /auth/login/password` 收 `phone` 或 `email`(二选一),失败统一 2013、连错 10 次锁 15 分钟;
- 绑定在 `PATCH /auth/me`:格式错 2018、被占用 2019、留空串表示解绑。

系统设置(跨租户,只有平台管理员能改):
- 新表 `system_setting`(不挂租户、无 RLS,写权限由 `deps.platform_admin` 把关);
- `GET/PUT /settings/smtp`、`POST /settings/smtp/test`,授权码只写不读,留空表示沿用旧值;
- `services/mail.py` 只用标准库 smtplib:465 走 SSL、587 走 STARTTLS,认证/拒收/网络失败都归 2020 但消息不同;
- Web 新增「系统设置」页(状态卡 + 测试发信),侧栏只对平台管理员显示入口;账号页补「邮箱」。

验证:pytest 130 passed、ruff check/format 全绿、tsc 通过、vite build 通过;
`web/e2e/login-email-smtp.mjs` 15 项全绿(含用 126 授权码真发一封测试邮件)。

顺带记下的两个坑:`platform_admin_phones` 是 list,pydantic-settings 会按 JSON 解析,
要用 `NoDecode` 交给 before-validator 拆逗号;迁移号写成 0009 会撞既有的 0009,一并改成 0010。
This commit is contained in:
Steven
2026-11-03 00:00:11 +08:00
parent 23687d6e37
commit 7558637567
29 changed files with 1702 additions and 93 deletions
+20 -1
View File
@@ -1,6 +1,6 @@
import { Link } from "react-router-dom";
import { useAuth } from "../App";
import { Logout, Logo, Settings } from "./icons";
import { Logout, Logo, Settings, Sliders } from "./icons";
/**
* 工作区里侧栏长什么样,工作区外面(工作区列表、账号设置)就长什么样。
@@ -64,6 +64,16 @@ export function SideRail({ activeId }: { activeId?: string | null }) {
<Settings size={14} className="text-ink-400" />
账号设置
</Link>
{/* 系统设置是平台级配置(全站共用一份),只给平台管理员看 */}
{user?.platform_admin ? (
<Link
to="/settings"
className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs text-ink-500 transition hover:bg-paper-100 hover:text-ink-800"
>
<Sliders size={14} className="text-ink-400" />
系统设置
</Link>
) : null}
</nav>
<div className="border-t border-line-200 px-3 py-3">
@@ -94,6 +104,15 @@ export function SideRail({ activeId }: { activeId?: string | null }) {
<span className="text-sm font-semibold text-ink-900">达道Task</span>
</Link>
<div className="flex items-center gap-1">
{user?.platform_admin ? (
<Link
to="/settings"
className="rounded-md p-1.5 text-ink-400 hover:bg-ink-100"
aria-label="系统设置"
>
<Sliders size={16} />
</Link>
) : null}
<Link
to="/account"
className="rounded-md p-1.5 text-ink-400 hover:bg-ink-100"
+8
View File
@@ -227,6 +227,14 @@ export const Settings = (p: IconProps) => (
</svg>
);
export const Sliders = (p: IconProps) => (
<svg {...base(p)}>
<path d="M4 7.5h9M17 7.5h3M4 16.5h3M11 16.5h9" />
<circle cx="15" cy="7.5" r="2" />
<circle cx="9" cy="16.5" r="2" />
</svg>
);
export const User = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="12" cy="8.5" r="3.5" />