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
+107 -1
View File
@@ -6,9 +6,15 @@ import test from 'node:test';
import express from 'express';
import { attachPageDataRoutes } from './page-data-routes.mjs';
import { createPageDataService } from './page-data-service.mjs';
import { createPageDataPublicService } from './page-data-public-service.mjs';
import { createPageDataPublicService, isPageDataPublicPath } from './page-data-public-service.mjs';
import { createUserDataSpaceService } from './user-data-space-service.mjs';
import { publicationInternals } from './mindspace-publications.mjs';
import { writePageAccessPolicy } from './page-data-policy-store.mjs';
import {
resolvePortalAccessEnforcementConfig,
resolvePortalAccessEnforcementDecision,
shouldOverridePortalLegacyGlobalAuth,
} from './server/portal-access-policy.mjs';
const PAGE_ID = 'page-integration-1';
const OWNER_ID = 'user-integration-1';
@@ -110,6 +116,42 @@ function buildApp(workspaceRoot) {
return app;
}
function buildEnforcedPageDataApp(workspaceRoot) {
const pageDataPublicService = createPageDataPublicService({
getPool: () => createPool('public'),
resolveWorkspaceRootForOwner: () => workspaceRoot,
});
const enforcementConfig = resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'page-data-public',
});
const api = express.Router();
api.use(express.json());
api.use((req, res, next) => {
const decision = resolvePortalAccessEnforcementDecision(
{ path: req.path, method: req.method },
{ isPageDataPublicPath },
enforcementConfig,
);
if (shouldOverridePortalLegacyGlobalAuth(decision, false)) return next();
return res.status(401).json({ error: { code: 'unauthorized' } });
});
attachPageDataRoutes(api, {
sendData: (res, _req, data, status = 200) => res.status(status).json({ data }),
sendError: (res, _req, status, code, message) =>
res.status(status).json({ error: { code, message } }),
getPageDataService: () =>
createPageDataService({
resolveWorkspaceRoot: async () => workspaceRoot,
}),
getPageDataPublicService: () => pageDataPublicService,
});
const app = express();
app.set('trust proxy', true);
app.use('/api', api);
return app;
}
async function request(app, method, url, { body, headers } = {}) {
const server = app.listen(0);
try {
@@ -140,6 +182,21 @@ test('integration: owner private API and public insert coexist without breaking
assert.equal(ownerInsert.status, 201);
assert.equal(ownerInsert.body.data.row.title, 'owner 写入');
const ownerInvalidInsert = await request(
app,
'POST',
'/api/admin/page-data/entries/rows',
{
headers: { 'x-test-user': '1' },
body: { forbidden_column: 'blocked' },
},
);
assert.equal(ownerInvalidInsert.status, 403);
assert.equal(
ownerInvalidInsert.body.error.code,
'columns_not_allowed',
);
const ownerList = await request(app, 'GET', '/api/admin/page-data/entries?limit=10', {
headers: { 'x-test-user': '1' },
});
@@ -184,6 +241,55 @@ test('integration: owner private API and public insert coexist without breaking
assert.equal(stats.total, 2);
});
test('integration: page-data-public enforcement delegates to route policy without opening adjacent APIs', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-enforced-'));
await setupWorkspace(workspaceRoot);
writePageAccessPolicy(workspaceRoot, {
pageId: PAGE_ID,
ownerUserId: OWNER_ID,
accessMode: 'public',
datasets: {
entries: {
read: false,
insert: true,
columns: {
read: ['id', 'title', 'created_at'],
insert: ['title'],
},
},
},
});
const app = buildEnforcedPageDataApp(workspaceRoot);
const inserted = await request(
app,
'POST',
`/api/public/pages/${PAGE_ID}/data/entries/rows`,
{ body: { title: '灰度公开写入' } },
);
assert.equal(inserted.status, 201);
assert.equal(inserted.body.data.row.title, '灰度公开写入');
const policyDenied = await request(
app,
'GET',
`/api/public/pages/${PAGE_ID}/data/entries`,
);
assert.equal(policyDenied.status, 403);
assert.equal(policyDenied.body.error.code, 'action_not_allowed');
for (const [method, url] of [
['GET', '/api/admin/page-data/entries'],
['GET', '/api/plaza/v1/categories'],
['GET', `/api/public/pages/${PAGE_ID}/data/entries/rows`],
['POST', `/api/public/pages/${PAGE_ID}/data/entries`],
]) {
const denied = await request(app, method, url);
assert.equal(denied.status, 401, `${method} ${url}`);
assert.equal(denied.body.error.code, 'unauthorized');
}
});
test('integration: public insert rejects SQL injection style payload keys', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-integration-sql-'));
await setupWorkspace(workspaceRoot);