feat(page-data): complete Phase 4-5, ops UI, and publish integration
Add visitor roles, row-level scope, owner ops APIs, MySQL policy index, Turnstile captcha, browser client SDK, publish-panel dataset binding, acceptance tests, and usage documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||
|
||||
export function isPageDataCaptchaEnabled() {
|
||||
return Boolean(String(process.env.PAGE_DATA_TURNSTILE_SECRET ?? '').trim());
|
||||
}
|
||||
|
||||
export async function verifyPageDataCaptcha(token, remoteIp, { fetchImpl = globalThis.fetch } = {}) {
|
||||
const secret = String(process.env.PAGE_DATA_TURNSTILE_SECRET ?? '').trim();
|
||||
if (!secret) {
|
||||
return { ok: true, skipped: true };
|
||||
}
|
||||
const responseToken = String(token ?? '').trim();
|
||||
if (!responseToken) {
|
||||
throw Object.assign(new Error('提交表单需要验证码'), { code: 'captcha_required', status: 400 });
|
||||
}
|
||||
const body = new URLSearchParams({
|
||||
secret,
|
||||
response: responseToken,
|
||||
});
|
||||
if (remoteIp) body.set('remoteip', String(remoteIp));
|
||||
const response = await fetchImpl(TURNSTILE_VERIFY_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!payload?.success) {
|
||||
throw Object.assign(new Error('验证码校验失败'), {
|
||||
code: 'captcha_invalid',
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
return { ok: true, skipped: false };
|
||||
}
|
||||
|
||||
export function extractPageDataCaptchaToken(req, payload) {
|
||||
return (
|
||||
String(req.headers?.['x-page-data-captcha'] ?? '').trim() ||
|
||||
String(payload?.turnstile_token ?? payload?.turnstileToken ?? payload?.captcha_token ?? '').trim() ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export const PAGE_DATA_META_FIELDS = new Set([
|
||||
'turnstile_token',
|
||||
'turnstileToken',
|
||||
'captcha_token',
|
||||
'updated_by_label',
|
||||
]);
|
||||
|
||||
export function stripPageDataMetaFields(payload) {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload;
|
||||
const cleaned = { ...payload };
|
||||
for (const key of PAGE_DATA_META_FIELDS) {
|
||||
delete cleaned[key];
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
Reference in New Issue
Block a user