feat(page-data): owner CRUD, public read, and role policy UI

Expose owner PATCH/soft-delete routes, allow anonymous read/stats on
public pages when policy enables read, and add login_required role
editor to the ops panel with tests and doc updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-08 14:54:14 +08:00
parent 6b0c633a75
commit 8d629e7a4e
11 changed files with 360 additions and 9 deletions
+28
View File
@@ -174,6 +174,34 @@ test('acceptance: public insert only when authorized; read/update/delete denied
assert.equal(deleteDenied.status, 403);
});
test('acceptance: public read and stats when policy enables read', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-acc-public-read-'));
const ownerService = await setupWorkspace(workspaceRoot);
await ownerService.insertDatasetRow('leads', { name: '展示项', note: 'secret' });
writePageAccessPolicy(workspaceRoot, {
pageId: PAGE_ID,
ownerUserId: OWNER_ID,
accessMode: 'public',
datasets: {
leads: {
read: true,
columns: { read: ['id', 'name', 'created_at'] },
},
},
});
const app = buildApp(workspaceRoot);
const listed = await request(app, 'GET', `/api/public/pages/${PAGE_ID}/data/leads?limit=10`);
assert.equal(listed.status, 200);
assert.equal(listed.body.data.rows.length, 1);
assert.equal(listed.body.data.rows[0].name, '展示项');
assert.equal(listed.body.data.rows[0].note, undefined);
const stats = await request(app, 'GET', `/api/public/pages/${PAGE_ID}/data/leads/stats`);
assert.equal(stats.status, 200);
assert.equal(stats.body.data.total, 1);
});
test('acceptance: unauthorized dataset/action/column and SQL-like keys are rejected', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-acc-deny-'));
await setupWorkspace(workspaceRoot);