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。
60 lines
2.0 KiB
JavaScript
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);
|