import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; 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 { createPageDataSessionStore } from './page-data-session-store.mjs'; import { createUserDataSpaceService } from './user-data-space-service.mjs'; import { writePageAccessPolicy } from './page-data-policy-store.mjs'; import { publicationInternals } from './mindspace-publications.mjs'; const PAGE_ID = 'page-ops-1'; const OWNER_ID = 'user-ops-1'; const OLD_PASSWORD = 'old-pass-123'; const NEW_PASSWORD = 'new-pass-456'; function createPool({ accessMode = 'password', password = OLD_PASSWORD, onUpdate } = {}) { const passwordHash = password ? publicationInternals.hashPassword(password) : null; const state = { id: 'pub-ops-1', user_id: OWNER_ID, page_id: PAGE_ID, access_mode: accessMode, password_hash: passwordHash, status: 'online', expires_at: null, }; return { async query(sql, params) { if (sql.includes('FROM h5_publish_records') && sql.includes('WHERE pr.page_id')) { return [[{ ...state }]]; } if (sql.startsWith('UPDATE h5_publish_records SET password_hash')) { state.password_hash = params[0]; onUpdate?.(params); return [{ affectedRows: 1 }]; } return [[]]; }, }; } async function setupWorkspace(workspaceRoot) { const service = createUserDataSpaceService({ workspaceRoot }); await service.executeSql(`CREATE TABLE signups ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, created_at TEXT );`); await service.upsertDataset({ name: 'signups', table: 'signups', actions: ['read', 'insert'], columns: { read: ['id', 'name', 'phone', 'created_at'], insert: ['name', 'phone'], }, }); writePageAccessPolicy(workspaceRoot, { pageId: PAGE_ID, ownerUserId: OWNER_ID, accessMode: 'password', datasets: { signups: { read: true, insert: true, columns: { read: ['id', 'name', 'phone', 'created_at'], insert: ['name', 'phone'], }, }, }, }); return service; } function buildApp(workspaceRoot, pool, sessionStore) { const pageDataPublicService = createPageDataPublicService({ getPool: () => pool, resolveWorkspaceRootForOwner: () => workspaceRoot, sessionStore, }); const api = express.Router(); api.use(express.json()); api.use((req, _res, next) => { if (req.headers['x-test-user']) { req.currentUser = { id: OWNER_ID, workspaceRoot }; } next(); }); 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.use('/api', api); return app; } async function request(app, method, url, { body, headers } = {}) { const server = app.listen(0); try { const { port } = server.address(); const response = await fetch(`http://127.0.0.1:${port}${url}`, { method, headers: { ...(body ? { 'content-type': 'application/json' } : {}), ...headers, }, body: body ? JSON.stringify(body) : undefined, }); return { status: response.status, body: await response.json() }; } finally { await new Promise((resolve) => server.close(resolve)); } } test('ops overview returns datasets stats logs and active sessions', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-ops-overview-')); const ownerService = await setupWorkspace(workspaceRoot); await ownerService.insertDatasetRow('signups', { name: '概览项', phone: '13800000000' }); const sessionStore = createPageDataSessionStore(); sessionStore.issue({ pageId: PAGE_ID, ownerUserId: OWNER_ID, publicationId: 'pub-ops-1', accessMode: 'password', }); const app = buildApp(workspaceRoot, createPool(), sessionStore); await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data/signups/rows`, { body: { name: '公开提交', phone: '13800000001' }, headers: { 'x-page-data-token': (await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data-auth`, { body: { password: OLD_PASSWORD } })).body.data.token }, }); const overview = await request(app, 'GET', `/api/admin/page-data/policies/${PAGE_ID}/ops`, { headers: { 'x-test-user': '1' }, }); assert.equal(overview.status, 200); assert.equal(overview.body.data.datasets.length, 1); assert.equal(overview.body.data.datasets[0].stats.total, 2); assert.ok(overview.body.data.activeSessionCount >= 1); assert.ok(overview.body.data.recentLogs.length >= 1); }); test('owner can revoke all tokens and reset password', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-ops-reset-')); await setupWorkspace(workspaceRoot); const sessionStore = createPageDataSessionStore(); const auth1 = sessionStore.issue({ pageId: PAGE_ID, ownerUserId: OWNER_ID, publicationId: 'pub-ops-1', accessMode: 'password', }); const auth2 = sessionStore.issue({ pageId: PAGE_ID, ownerUserId: OWNER_ID, publicationId: 'pub-ops-1', accessMode: 'password', }); let updatedPasswordHash = null; const app = buildApp(workspaceRoot, createPool({ onUpdate: ([hash]) => { updatedPasswordHash = hash; } }), sessionStore); const revoked = await request(app, 'POST', `/api/admin/page-data/policies/${PAGE_ID}/tokens/revoke`, { headers: { 'x-test-user': '1' }, body: { revokeAll: true }, }); assert.equal(revoked.status, 200); assert.equal(revoked.body.data.revokedCount, 2); assert.equal(sessionStore.verify(auth1.token), null); assert.equal(sessionStore.verify(auth2.token), null); const reset = await request(app, 'POST', `/api/admin/page-data/policies/${PAGE_ID}/password/reset`, { headers: { 'x-test-user': '1' }, body: { password: NEW_PASSWORD }, }); assert.equal(reset.status, 200); assert.equal(reset.body.data.passwordReset, true); assert.ok(updatedPasswordHash); assert.ok(publicationInternals.verifyPassword(NEW_PASSWORD, updatedPasswordHash)); const oldAuthDenied = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data-auth`, { body: { password: OLD_PASSWORD }, }); assert.equal(oldAuthDenied.status, 401); const newAuth = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data-auth`, { body: { password: NEW_PASSWORD }, }); assert.equal(newAuth.status, 200); assert.ok(newAuth.body.data.token); }); test('owner can close dataset and public insert is denied afterwards', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-ops-close-')); await setupWorkspace(workspaceRoot); const sessionStore = createPageDataSessionStore(); const app = buildApp(workspaceRoot, createPool(), sessionStore); const auth = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data-auth`, { body: { password: OLD_PASSWORD }, }); const closed = await request(app, 'POST', `/api/admin/page-data/policies/${PAGE_ID}/datasets/signups/close`, { headers: { 'x-test-user': '1' }, }); assert.equal(closed.status, 200); assert.equal(closed.body.data.closed, true); assert.equal(closed.body.data.policyDataset.closed, true); const denied = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data/signups/rows`, { headers: { 'x-page-data-token': auth.body.data.token }, body: { name: '关闭后提交', phone: '13800000002' }, }); assert.equal(denied.status, 403); assert.equal(denied.body.error.code, 'action_not_allowed'); });