refactor: modularize portal server and harden session recovery

This commit is contained in:
john
2026-07-24 18:39:24 +08:00
parent 1d291fd528
commit 785731d98d
109 changed files with 29185 additions and 5836 deletions
+160 -5
View File
@@ -7,25 +7,32 @@ import { createPageDataPublicService, isPageDataPublicPath } from './page-data-p
import { createUserDataSpaceService } from './user-data-space-service.mjs';
import { writePageAccessPolicy, readPageAccessPolicy } from './page-data-policy-store.mjs';
import { publicationInternals } from './mindspace-publications.mjs';
import { createPageDataSessionStore } from './page-data-session-store.mjs';
const PAGE_ID = 'page-public-1';
const OWNER_ID = 'user-owner-1';
function createPublicationPool({ accessMode = 'public', password = null } = {}) {
function createPublicationPool({
accessMode = 'public',
password = null,
pageId = PAGE_ID,
expiresAt = null,
} = {}) {
const passwordHash = password ? publicationInternals.hashPassword(password) : null;
return {
async query(sql) {
async query(sql, params = []) {
if (sql.includes('FROM h5_publish_records')) {
if (params[0] !== pageId) return [[]];
return [
[
{
id: 'pub-record-1',
user_id: OWNER_ID,
page_id: PAGE_ID,
page_id: pageId,
access_mode: accessMode,
password_hash: passwordHash,
status: 'online',
expires_at: null,
expires_at: expiresAt,
},
],
];
@@ -80,10 +87,11 @@ async function setupPublicWorkspace(workspaceRoot, { accessMode = 'public', with
return service;
}
function createPublicService(workspaceRoot, poolOptions = {}) {
function createPublicService(workspaceRoot, poolOptions = {}, serviceOptions = {}) {
return createPageDataPublicService({
getPool: () => createPublicationPool(poolOptions),
resolveWorkspaceRootForOwner: () => workspaceRoot,
...serviceOptions,
});
}
@@ -206,11 +214,133 @@ test('password page update and soft delete require token', async () => {
assert.equal(deleted.deleted, true);
});
test('password page rejects missing, invalid, cross-page, and expired tokens', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-password-token-'));
await setupPublicWorkspace(workspaceRoot, {
accessMode: 'password',
withRead: true,
});
const sessionStore = createPageDataSessionStore({ ttlMs: 60_000 });
const service = createPublicService(
workspaceRoot,
{ accessMode: 'password', password: 'team-2026' },
{ sessionStore },
);
for (const headers of [{}, { 'x-page-data-token': 'invalid-token' }]) {
await assert.rejects(
() => service.listRows(PAGE_ID, 'signups', { headers }),
(error) => error.code === 'token_invalid' && error.status === 401,
);
}
const crossPage = sessionStore.issue({
pageId: 'page-other',
ownerUserId: OWNER_ID,
publicationId: 'pub-other',
accessMode: 'password',
});
await assert.rejects(
() =>
service.listRows(PAGE_ID, 'signups', {
headers: { 'x-page-data-token': crossPage.token },
}),
(error) => error.code === 'token_invalid' && error.status === 401,
);
const expiredStore = createPageDataSessionStore({ ttlMs: -1 });
const expired = expiredStore.issue({
pageId: PAGE_ID,
ownerUserId: OWNER_ID,
publicationId: 'pub-expired',
accessMode: 'password',
});
const expiredService = createPublicService(
workspaceRoot,
{ accessMode: 'password', password: 'team-2026' },
{ sessionStore: expiredStore },
);
await assert.rejects(
() =>
expiredService.listRows(PAGE_ID, 'signups', {
headers: { 'x-page-data-token': expired.token },
}),
(error) => error.code === 'token_invalid' && error.status === 401,
);
});
test('public access never allows update or delete even when policy contains those actions', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-public-mutation-'));
const ownerService = await setupPublicWorkspace(workspaceRoot, {
accessMode: 'public',
withRead: true,
});
const inserted = await ownerService.insertRowForDataset(ownerService.getDataset('signups'), {
name: '不可匿名修改',
status: 'pending',
});
const service = createPublicService(workspaceRoot, { accessMode: 'public' });
const req = { ip: '127.0.0.1', headers: {}, body: {} };
await assert.rejects(
() =>
service.updateRow(PAGE_ID, 'signups', inserted.row.id, req, {
status: 'done',
}),
(error) => error.code === 'action_not_allowed' && error.status === 403,
);
await assert.rejects(
() => service.deleteRow(PAGE_ID, 'signups', inserted.row.id, req),
(error) => error.code === 'action_not_allowed' && error.status === 403,
);
});
test('expired, unknown, and invalid-policy publications fail closed', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-public-fail-closed-'));
await setupPublicWorkspace(workspaceRoot, { accessMode: 'public', withRead: true });
const expiredService = createPublicService(workspaceRoot, {
accessMode: 'public',
expiresAt: Date.now() - 1,
});
await assert.rejects(
() => expiredService.listRows(PAGE_ID, 'signups', { headers: {} }),
(error) => error.code === 'publication_not_found' && error.status === 404,
);
const service = createPublicService(workspaceRoot, { accessMode: 'public' });
await assert.rejects(
() => service.listRows('page-missing', 'signups', { headers: {} }),
(error) => error.code === 'publication_not_found' && error.status === 404,
);
writePageAccessPolicy(workspaceRoot, {
pageId: PAGE_ID,
ownerUserId: 'different-owner',
accessMode: 'public',
datasets: {
signups: {
read: true,
columns: { read: ['id', 'name'] },
},
},
});
await assert.rejects(
() => service.listRows(PAGE_ID, 'signups', { headers: {} }),
(error) => error.code === 'invalid_policy' && error.status === 400,
);
});
test('isPageDataPublicPath allows public page data routes without auth', () => {
assert.equal(isPageDataPublicPath('/public/pages/page-1/data/signups/rows', 'POST'), true);
assert.equal(isPageDataPublicPath('/public/pages/page-1/data-auth', 'POST'), true);
assert.equal(isPageDataPublicPath('/public/pages/page-1/data/signups', 'GET'), true);
assert.equal(isPageDataPublicPath('/page-data/signups', 'GET'), false);
assert.equal(isPageDataPublicPath('/public/pages/page-1/data/signups', 'POST'), false);
assert.equal(isPageDataPublicPath('/public/pages/page-1/data/signups/rows', 'GET'), false);
assert.equal(isPageDataPublicPath('/public/pages/page-1/dataevil/signups', 'GET'), false);
assert.equal(isPageDataPublicPath('/public/pages/page-1/data-auth', 'GET'), false);
assert.equal(isPageDataPublicPath('/admin/page-data/signups', 'GET'), false);
});
async function setupCollaborationWorkspace(workspaceRoot) {
@@ -423,3 +553,28 @@ test('saveOwnerPolicyFromPublish writes policy for published page', async () =>
const saved = readPageAccessPolicy(workspaceRoot, PAGE_ID);
assert.equal(saved.datasets.signups.read, true);
});
test('saveOwnerPolicy rejects unregistered datasets without overwriting the current policy', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-owner-policy-guard-'));
await setupPublicWorkspace(workspaceRoot, { accessMode: 'password', withRead: true });
const service = createPublicService(workspaceRoot, { accessMode: 'password' });
const user = { id: OWNER_ID, workspaceRoot };
await assert.rejects(
() =>
service.saveOwnerPolicy(user, PAGE_ID, {
accessMode: 'password',
datasets: {
split_crud_uat_20260724: {
read: true,
insert: true,
columns: { read: ['id'], insert: ['title'] },
},
},
}),
(error) => error.code === 'dataset_not_found' && error.status === 404,
);
const saved = readPageAccessPolicy(workspaceRoot, PAGE_ID);
assert.deepEqual(Object.keys(saved.datasets), ['signups']);
});