Files
DadaoTask/web/e2e/shots-account-login.mjs
T
Steven 7558637567
CI / backend (push) Canceled after 0s
feat(auth): 短信登录默认关,换成手机号/邮箱 + 密码,发信邮箱进后台
用户要求:内测的固定验证码 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。
2026-11-03 00:00:11 +08:00

60 lines
2.0 KiB
JavaScript

/**
* 拍两张文档用的截图:登录页(两种账号名 + 密码)、账号页(邮箱)、系统设置页。
* 需要前端 5180、后端 8100 起着;账号用 dev 的平台管理员。
*/
import { chromium } from "playwright-core";
import { mkdirSync, readFileSync } from "node:fs";
const WEB = process.env.WEB ?? "http://127.0.0.1:5180";
const API = process.env.API ?? "http://127.0.0.1:8100/api/v1";
const PHONE = process.env.PHONE ?? "13900000001";
const OUT = "e2e/artifacts";
const PWD_FILE = new URL("../../.local/e2e-account-password.txt", import.meta.url);
function savedPassword() {
try {
return readFileSync(PWD_FILE, "utf8").trim();
} catch {
return "";
}
}
const browser = await chromium.launch({
executablePath:
process.env.CHROME ??
`${process.env.LOCALAPPDATA}/ms-playwright/chromium-1223/chrome-win64/chrome.exe`,
});
const page = await browser.newPage({ viewport: { width: 1440, height: 960 } });
mkdirSync(OUT, { recursive: true });
await page.goto(`${WEB}/login`);
await page.waitForSelector("#account");
await page.screenshot({ path: `${OUT}/login-password.png` });
const r = await fetch(`${API}/auth/login/password`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone: PHONE, password: savedPassword() }),
});
const data = await r.json();
await page.addInitScript(
([a, rf]) => {
localStorage.setItem("dadao.access", a);
localStorage.setItem("dadao.refresh", rf);
},
[data.data.tokens.access_token, data.data.tokens.refresh_token],
);
await page.goto(`${WEB}/account`);
await page.waitForSelector('input[aria-label="邮箱"]');
await page.waitForTimeout(600);
await page.screenshot({ path: `${OUT}/account-page.png` });
await page.goto(`${WEB}/settings`);
await page.waitForSelector('input[aria-label="服务器地址"]');
await page.waitForTimeout(800);
await page.screenshot({ path: `${OUT}/settings-page.png` });
await browser.close();
console.log("screenshots written to", OUT);