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,93 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { normalizePageAccessPolicy } from './page-access-policy.mjs';
|
||||
import {
|
||||
buildEffectiveDatasetForRole,
|
||||
resolveRowAccessScope,
|
||||
resolveVisitorRole,
|
||||
roleAllowsAction,
|
||||
} from './page-access-visitor.mjs';
|
||||
|
||||
test('resolveVisitorRole maps owner, assigned visitors and defaults', () => {
|
||||
const policy = normalizePageAccessPolicy({
|
||||
pageId: 'page-1',
|
||||
ownerUserId: 'owner-1',
|
||||
accessMode: 'login_required',
|
||||
defaultVisitorRole: 'deny',
|
||||
visitors: { 'editor-1': 'editor', 'viewer-1': 'viewer' },
|
||||
datasets: {},
|
||||
});
|
||||
assert.equal(resolveVisitorRole(policy, 'owner-1'), 'owner');
|
||||
assert.equal(resolveVisitorRole(policy, 'editor-1'), 'editor');
|
||||
assert.equal(resolveVisitorRole(policy, 'viewer-1'), 'viewer');
|
||||
assert.equal(resolveVisitorRole(policy, 'stranger-1'), null);
|
||||
});
|
||||
|
||||
test('buildEffectiveDatasetForRole intersects dataset and role permissions', () => {
|
||||
const effective = buildEffectiveDatasetForRole(
|
||||
{
|
||||
name: 'tasks',
|
||||
table: 'tasks',
|
||||
actions: ['read', 'insert', 'update', 'soft_delete'],
|
||||
columns: {
|
||||
read: ['id', 'title'],
|
||||
insert: ['title'],
|
||||
update: ['title'],
|
||||
soft_delete: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
read: true,
|
||||
insert: true,
|
||||
update: true,
|
||||
softDelete: true,
|
||||
columns: {
|
||||
read: ['id', 'title'],
|
||||
insert: ['title'],
|
||||
update: ['title'],
|
||||
},
|
||||
},
|
||||
{ read: true, insert: false, update: false, softDelete: false },
|
||||
);
|
||||
assert.deepEqual(effective.actions, ['read']);
|
||||
assert.equal(roleAllowsAction({ read: true, insert: false }, 'insert'), false);
|
||||
});
|
||||
|
||||
test('resolveRowAccessScope enforces own_rows and owner_only', () => {
|
||||
const ownRows = resolveRowAccessScope(
|
||||
{ scope: 'own_rows', ownerColumn: 'created_by_user_id' },
|
||||
{
|
||||
visitorRole: 'editor',
|
||||
visitorUserId: 'user-a',
|
||||
ownerUserId: 'owner-1',
|
||||
tableColumns: [{ name: 'created_by_user_id' }],
|
||||
},
|
||||
);
|
||||
assert.match(ownRows.whereClause, /created_by_user_id/);
|
||||
assert.match(ownRows.whereClause, /user-a/);
|
||||
|
||||
const ownerBypass = resolveRowAccessScope(
|
||||
{ scope: 'own_rows', ownerColumn: 'created_by_user_id' },
|
||||
{
|
||||
visitorRole: 'owner',
|
||||
visitorUserId: 'owner-1',
|
||||
ownerUserId: 'owner-1',
|
||||
tableColumns: [{ name: 'created_by_user_id' }],
|
||||
},
|
||||
);
|
||||
assert.equal(ownerBypass.whereClause, null);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveRowAccessScope(
|
||||
{ scope: 'owner_only', ownerColumn: 'created_by_user_id' },
|
||||
{
|
||||
visitorRole: 'editor',
|
||||
visitorUserId: 'user-a',
|
||||
ownerUserId: 'owner-1',
|
||||
tableColumns: [],
|
||||
},
|
||||
),
|
||||
(error) => error.code === 'row_not_allowed',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user