Files
memind/page-access-visitor.mjs
T
john 6b0c633a75 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>
2026-07-08 14:52:49 +08:00

150 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { assertSafeSqlIdentifier } from './user-data-space-service.mjs';
export const PAGE_DATA_VISITOR_ROLES = new Set(['viewer', 'editor', 'owner']);
export const DEFAULT_ROLE_PERMISSIONS = {
viewer: { read: true, insert: false, update: false, softDelete: false },
editor: { read: true, insert: true, update: true, softDelete: true },
owner: { read: true, insert: true, update: true, softDelete: true, hardDelete: false },
};
export function normalizeRowPolicy(raw) {
if (!raw || typeof raw !== 'object') return null;
const scope = String(raw.scope ?? 'all_rows').trim();
if (!['all_rows', 'own_rows', 'owner_only'].includes(scope)) {
throw Object.assign(new Error(`不支持的 rowPolicy.scope${scope}`), { code: 'invalid_policy' });
}
const ownerColumn = assertSafeSqlIdentifier(
raw.ownerColumn ?? raw.owner_column ?? 'created_by_user_id',
'rowPolicy.ownerColumn',
);
return { scope, ownerColumn };
}
export function normalizeVisitorAssignments(raw) {
if (!raw || typeof raw !== 'object') return {};
const visitors = {};
for (const [userId, value] of Object.entries(raw)) {
const visitorId = String(userId ?? '').trim();
if (!visitorId) continue;
const role = typeof value === 'string' ? value.trim() : String(value?.role ?? '').trim();
if (!PAGE_DATA_VISITOR_ROLES.has(role) || role === 'owner') {
throw Object.assign(new Error(`访问者角色无效:${role}`), { code: 'invalid_policy' });
}
visitors[visitorId] = role;
}
return visitors;
}
export function normalizeRoleOverrides(raw) {
if (!raw || typeof raw !== 'object') return {};
const roles = {};
for (const [roleName, permissions] of Object.entries(raw)) {
const role = String(roleName ?? '').trim();
if (!PAGE_DATA_VISITOR_ROLES.has(role)) {
throw Object.assign(new Error(`角色定义无效:${role}`), { code: 'invalid_policy' });
}
roles[role] = {
read: Boolean(permissions?.read),
insert: Boolean(permissions?.insert),
update: Boolean(permissions?.update),
softDelete: Boolean(permissions?.softDelete ?? permissions?.soft_delete),
hardDelete: Boolean(permissions?.hardDelete ?? permissions?.hard_delete),
};
}
return roles;
}
export function resolveVisitorRole(policy, visitorUserId) {
const visitorId = String(visitorUserId ?? '').trim();
if (!visitorId) return null;
if (visitorId === policy.ownerUserId) return 'owner';
const assigned = policy.visitors?.[visitorId];
if (assigned) return assigned;
const defaultRole = String(policy.defaultVisitorRole ?? 'deny').trim();
if (defaultRole === 'deny') return null;
if (!PAGE_DATA_VISITOR_ROLES.has(defaultRole)) return null;
return defaultRole;
}
export function resolveRolePermissions(policy, role) {
const defaults = DEFAULT_ROLE_PERMISSIONS[role] ?? DEFAULT_ROLE_PERMISSIONS.viewer;
const overrides = policy.roles?.[role];
if (!overrides) return { ...defaults };
return {
read: overrides.read ?? defaults.read,
insert: overrides.insert ?? defaults.insert,
update: overrides.update ?? defaults.update,
softDelete: overrides.softDelete ?? defaults.softDelete,
hardDelete: overrides.hardDelete ?? defaults.hardDelete,
};
}
export function roleAllowsAction(rolePermissions, action) {
if (!rolePermissions) return false;
if (action === 'read') return Boolean(rolePermissions.read);
if (action === 'insert') return Boolean(rolePermissions.insert);
if (action === 'update') return Boolean(rolePermissions.update);
if (action === 'soft_delete') return Boolean(rolePermissions.softDelete);
if (action === 'hard_delete') return Boolean(rolePermissions.hardDelete);
return false;
}
export function buildEffectiveDatasetForRole(registryDataset, policyDataset, rolePermissions) {
if (!registryDataset || !policyDataset || !rolePermissions) return null;
const actions = [];
if (policyDataset.read && rolePermissions.read) actions.push('read');
if (policyDataset.insert && rolePermissions.insert) actions.push('insert');
if (policyDataset.update && rolePermissions.update) actions.push('update');
if (policyDataset.softDelete && rolePermissions.softDelete) actions.push('soft_delete');
if (policyDataset.hardDelete && rolePermissions.hardDelete) actions.push('hard_delete');
if (!actions.length) return null;
const columns = {
read: policyDataset.columns?.read ?? registryDataset.columns?.read ?? [],
insert: policyDataset.columns?.insert ?? registryDataset.columns?.insert ?? [],
update: policyDataset.columns?.update ?? registryDataset.columns?.update ?? [],
soft_delete: policyDataset.columns?.soft_delete ?? registryDataset.columns?.soft_delete ?? ['id'],
};
return {
name: registryDataset.name,
table: registryDataset.table,
description: registryDataset.description,
actions,
columns,
limits: policyDataset.limits ?? registryDataset.limits,
rowPolicy: policyDataset.rowPolicy ?? null,
};
}
export function resolveRowAccessScope(rowPolicy, {
visitorRole,
visitorUserId,
ownerUserId,
tableColumns = [],
} = {}) {
if (!rowPolicy || rowPolicy.scope === 'all_rows') {
return { bypass: true, whereClause: null, ownerColumn: null, visitorUserId };
}
const isOwner = visitorRole === 'owner' || visitorUserId === ownerUserId;
if (rowPolicy.scope === 'owner_only') {
if (isOwner) return { bypass: true, whereClause: null, ownerColumn: null, visitorUserId };
throw Object.assign(new Error('当前数据集仅页面所有者可访问'), { code: 'row_not_allowed' });
}
if (rowPolicy.scope === 'own_rows') {
if (isOwner) return { bypass: true, whereClause: null, ownerColumn: rowPolicy.ownerColumn, visitorUserId };
const column = rowPolicy.ownerColumn;
const hasColumn = tableColumns.some((item) => item.name === column);
if (!hasColumn) {
throw Object.assign(new Error(`行级权限列不存在:${column}`), { code: 'row_policy_invalid' });
}
const safeVisitorId = String(visitorUserId ?? '').replace(/'/g, "''");
return {
bypass: false,
whereClause: `"${column}" = '${safeVisitorId}'`,
ownerColumn: column,
visitorUserId,
};
}
return { bypass: true, whereClause: null, ownerColumn: null, visitorUserId };
}