Harden auth/vault path sanitization and clean WeKnora docs
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 7-76 P0:浏览器 smoke 标准登录(无「测试账号快速登录」按钮)。
|
||||
* 优先填表提交 /auth;失败时可回退 API signIn/signUp(由调用方决定)。
|
||||
*/
|
||||
|
||||
const DEFAULT_EMAIL = process.env.MNOTE_E2E_EMAIL || "mnote.e2e@example.com";
|
||||
const DEFAULT_PASSWORD = process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!";
|
||||
const DEFAULT_USERNAME = process.env.MNOTE_E2E_USERNAME || "mnote-e2e";
|
||||
/** 方案 A:独立 ops admin(非 AI 主体、非个人笔记主账号) */
|
||||
const DEFAULT_ADMIN_EMAIL = process.env.MNOTE_ADMIN_EMAIL || "mnote.admin@example.com";
|
||||
const DEFAULT_ADMIN_PASSWORD = process.env.MNOTE_ADMIN_PASSWORD || "MnoteAdmin123!";
|
||||
const DEFAULT_ADMIN_USERNAME = process.env.MNOTE_ADMIN_USERNAME || "mnote-admin";
|
||||
const DEFAULT_UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
||||
|
||||
function resolveBaseUrl(baseUrl) {
|
||||
return String(baseUrl || process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在已打开的 /auth 页(或任意页)用标准表单登录。
|
||||
* @param {import('playwright').Page} page
|
||||
* @param {{ baseUrl?: string, email?: string, password?: string, username?: string, timeoutMs?: number, gotoAuth?: boolean }} [options]
|
||||
*/
|
||||
async function loginViaAuthForm(page, options = {}) {
|
||||
const baseUrl = resolveBaseUrl(options.baseUrl);
|
||||
const email = options.email || DEFAULT_EMAIL;
|
||||
const password = options.password || DEFAULT_PASSWORD;
|
||||
const timeoutMs = options.timeoutMs || DEFAULT_UI_TIMEOUT_MS;
|
||||
const gotoAuth = options.gotoAuth !== false;
|
||||
|
||||
if (gotoAuth || !String(page.url() || "").includes("/auth")) {
|
||||
await page.goto(`${baseUrl}/auth`, { waitUntil: "commit", timeout: timeoutMs });
|
||||
}
|
||||
|
||||
const account = page.locator("#account, input[name='account']").first();
|
||||
const passwordInput = page.locator("#password, input[name='password']").first();
|
||||
const submit = page.locator("[data-auth-submit], button[type='submit']").first();
|
||||
|
||||
await account.waitFor({ state: "visible", timeout: timeoutMs });
|
||||
await account.fill(email, { timeout: timeoutMs });
|
||||
await passwordInput.fill(password, { timeout: timeoutMs });
|
||||
await submit.click({ timeout: timeoutMs });
|
||||
}
|
||||
|
||||
/**
|
||||
* API 层 signIn;不存在时可 signUp 再 signIn。
|
||||
* @param {{ fetch: Function }} requestContext Playwright request 或兼容对象
|
||||
* @param {{ baseUrl?: string, email?: string, password?: string, username?: string, timeoutMs?: number }} [options]
|
||||
*/
|
||||
async function loginViaAuthApi(requestContext, options = {}) {
|
||||
const baseUrl = resolveBaseUrl(options.baseUrl);
|
||||
const email = options.email || DEFAULT_EMAIL;
|
||||
const password = options.password || DEFAULT_PASSWORD;
|
||||
const username = options.username || DEFAULT_USERNAME;
|
||||
const timeoutMs = options.timeoutMs || DEFAULT_UI_TIMEOUT_MS;
|
||||
|
||||
async function post(params) {
|
||||
const response = await requestContext.fetch(`${baseUrl}/api/auth`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
data: {
|
||||
action: "auth:signIn",
|
||||
args: {
|
||||
provider: "password",
|
||||
params,
|
||||
},
|
||||
},
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
payload = text;
|
||||
}
|
||||
return { ok: response.ok(), status: response.status(), payload };
|
||||
}
|
||||
|
||||
let result = await post({
|
||||
account: email,
|
||||
email,
|
||||
password,
|
||||
flow: "signIn",
|
||||
});
|
||||
if (result.ok) {
|
||||
return result.payload;
|
||||
}
|
||||
|
||||
result = await post({
|
||||
email,
|
||||
name: username,
|
||||
password,
|
||||
flow: "signUp",
|
||||
});
|
||||
if (result.ok) {
|
||||
return result.payload;
|
||||
}
|
||||
|
||||
const details =
|
||||
typeof result.payload === "string" ? result.payload : JSON.stringify(result.payload);
|
||||
throw new Error(`标准 API 登录失败: ${result.status} ${details}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 ops admin 登录(/admin/*、代签、access-policy、ai-admin settings)。
|
||||
* AI 主体 mnote-e2e 与个人账号 liaibo 默认无 admin 能力。
|
||||
*/
|
||||
async function loginAsAdminViaAuthForm(page, options = {}) {
|
||||
return loginViaAuthForm(page, {
|
||||
...options,
|
||||
email: options.email || DEFAULT_ADMIN_EMAIL,
|
||||
password: options.password || DEFAULT_ADMIN_PASSWORD,
|
||||
username: options.username || DEFAULT_ADMIN_USERNAME,
|
||||
});
|
||||
}
|
||||
|
||||
async function loginAsAdminViaAuthApi(requestContext, options = {}) {
|
||||
return loginViaAuthApi(requestContext, {
|
||||
...options,
|
||||
email: options.email || DEFAULT_ADMIN_EMAIL,
|
||||
password: options.password || DEFAULT_ADMIN_PASSWORD,
|
||||
username: options.username || DEFAULT_ADMIN_USERNAME,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在独立 browser context 里以 admin 会话执行 fn(page),不污染主测用户 cookie。
|
||||
* 用于:access-policy grants、/api/ai-admin/* 等必须 admin 的 seed。
|
||||
* @param {import('playwright').Browser} browser
|
||||
* @param {(page: import('playwright').Page) => Promise<T>} fn
|
||||
* @param {{ baseUrl?: string, timeoutMs?: number }} [options]
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
async function withAdminBrowserSession(browser, fn, options = {}) {
|
||||
const baseUrl = resolveBaseUrl(options.baseUrl);
|
||||
const timeoutMs = options.timeoutMs || DEFAULT_UI_TIMEOUT_MS;
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
try {
|
||||
await loginAsAdminViaAuthForm(page, { baseUrl, timeoutMs });
|
||||
await page
|
||||
.waitForURL((url) => !String(url).includes("/auth"), { timeout: timeoutMs })
|
||||
.catch(() => {});
|
||||
return await fn(page);
|
||||
} finally {
|
||||
await context.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_EMAIL,
|
||||
DEFAULT_PASSWORD,
|
||||
DEFAULT_USERNAME,
|
||||
DEFAULT_ADMIN_EMAIL,
|
||||
DEFAULT_ADMIN_PASSWORD,
|
||||
DEFAULT_ADMIN_USERNAME,
|
||||
loginViaAuthForm,
|
||||
loginViaAuthApi,
|
||||
loginAsAdminViaAuthForm,
|
||||
loginAsAdminViaAuthApi,
|
||||
withAdminBrowserSession,
|
||||
};
|
||||
@@ -24,7 +24,7 @@ async function postDevSeed(requestContext, baseUrl, seeds, timeoutMs) {
|
||||
[
|
||||
`/api/dev/seed 未启用(base=${normalizedBaseUrl})。`,
|
||||
"依赖 seed 的 smoke 必须使用 `npm run dev:hot` 启动;dev:hot 默认开启 MNOTE_WEB_ALLOW_DEV_FIXTURES=1。",
|
||||
"若复用 desktop:hot,请使用 `MNOTE_WEB_ALLOW_DEV_FIXTURES=1 npm run desktop:hot`。",
|
||||
"若直接跑 mnote-web 而未走 dev:hot,请显式 `MNOTE_WEB_ALLOW_DEV_FIXTURES=1`。",
|
||||
"修改 scripts/dev-hot.js 或启动环境后必须重启 dev:hot 主进程,cargo-watch 不会刷新 Node 启动环境。",
|
||||
].join(" "),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user