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:
+152
-5
@@ -1,3 +1,4 @@
|
||||
import { stripPageDataMetaFields } from './page-data-captcha.mjs';
|
||||
import { mapPageDataPublicError } from './page-data-public-service.mjs';
|
||||
|
||||
function requireUser(req, res, sendError) {
|
||||
@@ -14,12 +15,12 @@ function parsePositiveInt(value, fallback) {
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
function parseRowPayload(req) {
|
||||
function parseRowPayload(req, { stripMeta = false } = {}) {
|
||||
const payload = req.body?.row && typeof req.body.row === 'object' ? req.body.row : req.body;
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return null;
|
||||
}
|
||||
return payload;
|
||||
return stripMeta ? stripPageDataMetaFields(payload) : payload;
|
||||
}
|
||||
|
||||
function handlePublicError(res, req, sendError, error) {
|
||||
@@ -44,6 +45,19 @@ export function attachPageDataRoutes(api, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/page-data/policies', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
const publicService = getPageDataPublicService?.();
|
||||
if (!publicService) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
try {
|
||||
const policies = await publicService.listOwnerPolicies(user);
|
||||
return sendData(res, req, { policies });
|
||||
} catch (error) {
|
||||
return handlePublicError(res, req, sendError, error);
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/page-data/policies/:pageId', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
@@ -58,6 +72,24 @@ export function attachPageDataRoutes(api, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/page-data/policies/:pageId/apply-publish', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
const publicService = getPageDataPublicService?.();
|
||||
if (!publicService) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
const datasetName = String(req.body?.datasetName ?? req.body?.dataset ?? '').trim();
|
||||
if (!datasetName) return sendError(res, req, 400, 'invalid_request', '缺少 datasetName');
|
||||
try {
|
||||
const policy = await publicService.saveOwnerPolicyFromPublish(user, req.params.pageId, {
|
||||
datasetName,
|
||||
capabilities: req.body?.capabilities ?? {},
|
||||
});
|
||||
return sendData(res, req, { policy });
|
||||
} catch (error) {
|
||||
return handlePublicError(res, req, sendError, error);
|
||||
}
|
||||
});
|
||||
|
||||
api.put('/page-data/policies/:pageId', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
@@ -71,6 +103,106 @@ export function attachPageDataRoutes(api, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/page-data/policies/:pageId/logs', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
const service = getPageDataService?.();
|
||||
if (!service) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
try {
|
||||
const result = await service.listPageLogs(user, req.params.pageId, {
|
||||
limit: parsePositiveInt(req.query.limit, 100),
|
||||
offset: parsePositiveInt(req.query.offset, 0),
|
||||
});
|
||||
return sendData(res, req, result);
|
||||
} catch (error) {
|
||||
const status = error?.status ?? 400;
|
||||
return sendError(res, req, status, error?.code ?? 'page_data_failed', error?.message ?? '读取失败');
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/page-data/policies/:pageId/ops', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
const publicService = getPageDataPublicService?.();
|
||||
if (!publicService) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
try {
|
||||
const overview = await publicService.getPageOpsOverview(user, req.params.pageId);
|
||||
return sendData(res, req, overview);
|
||||
} catch (error) {
|
||||
return handlePublicError(res, req, sendError, error);
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/page-data/policies/:pageId/tokens/revoke', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
const publicService = getPageDataPublicService?.();
|
||||
if (!publicService) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
try {
|
||||
const result = await publicService.revokePageDataTokens(user, req.params.pageId, {
|
||||
token: req.body?.token ?? null,
|
||||
revokeAll: Boolean(req.body?.revokeAll ?? req.body?.revoke_all),
|
||||
});
|
||||
return sendData(res, req, result);
|
||||
} catch (error) {
|
||||
return handlePublicError(res, req, sendError, error);
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/page-data/policies/:pageId/password/reset', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
const publicService = getPageDataPublicService?.();
|
||||
if (!publicService) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
const password = String(req.body?.password ?? '').trim();
|
||||
if (!password) return sendError(res, req, 400, 'invalid_request', '缺少新口令');
|
||||
try {
|
||||
const result = await publicService.resetPageDataPassword(user, req.params.pageId, password);
|
||||
return sendData(res, req, result);
|
||||
} catch (error) {
|
||||
return handlePublicError(res, req, sendError, error);
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/page-data/policies/:pageId/datasets/:dataset/close', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
const publicService = getPageDataPublicService?.();
|
||||
if (!publicService) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
try {
|
||||
const result = await publicService.closePageDataset(user, req.params.pageId, req.params.dataset);
|
||||
return sendData(res, req, result);
|
||||
} catch (error) {
|
||||
return handlePublicError(res, req, sendError, error);
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/page-data/:dataset/export', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
const service = getPageDataService?.();
|
||||
if (!service) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
try {
|
||||
const exported = await service.exportDataset(user, req.params.dataset, {
|
||||
format: req.query.format,
|
||||
include_deleted: req.query.include_deleted,
|
||||
limit: parsePositiveInt(req.query.limit, 1000),
|
||||
});
|
||||
if (exported.format === 'csv') {
|
||||
res.setHeader('content-type', 'text/csv; charset=utf-8');
|
||||
res.setHeader(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(exported.dataset)}.csv"`,
|
||||
);
|
||||
return res.status(200).send(exported.content);
|
||||
}
|
||||
return sendData(res, req, exported);
|
||||
} catch (error) {
|
||||
const status = error?.status ?? 400;
|
||||
return sendError(res, req, status, error?.code ?? 'page_data_failed', error?.message ?? '导出失败');
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/page-data/:dataset', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
@@ -82,6 +214,7 @@ export function attachPageDataRoutes(api, deps) {
|
||||
offset: parsePositiveInt(req.query.offset, 0),
|
||||
orderBy: req.query.order_by,
|
||||
orderDir: req.query.order_dir,
|
||||
include_deleted: req.query.include_deleted,
|
||||
});
|
||||
return sendData(res, req, {
|
||||
dataset: result.dataset.name,
|
||||
@@ -128,7 +261,7 @@ export function attachPageDataRoutes(api, deps) {
|
||||
if (!user) return;
|
||||
const service = getPageDataService?.();
|
||||
if (!service) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
const payload = parseRowPayload(req);
|
||||
const payload = parseRowPayload(req, { stripMeta: true });
|
||||
if (!payload) return sendError(res, req, 400, 'invalid_request', '提交数据必须是 JSON 对象');
|
||||
try {
|
||||
const result = await service.insertRow(user, req.params.dataset, payload);
|
||||
@@ -139,6 +272,20 @@ export function attachPageDataRoutes(api, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/page-data/:dataset/rows/:rowId/restore', async (req, res) => {
|
||||
const user = requireUser(req, res, sendError);
|
||||
if (!user) return;
|
||||
const service = getPageDataService?.();
|
||||
if (!service) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
try {
|
||||
const result = await service.restoreRow(user, req.params.dataset, req.params.rowId);
|
||||
return sendData(res, req, result);
|
||||
} catch (error) {
|
||||
const status = error?.status ?? 400;
|
||||
return sendError(res, req, status, error?.code ?? 'page_data_failed', error?.message ?? '恢复失败');
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/public/pages/:pageId/data-auth', async (req, res) => {
|
||||
const publicService = getPageDataPublicService?.();
|
||||
if (!publicService) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
@@ -198,7 +345,7 @@ export function attachPageDataRoutes(api, deps) {
|
||||
api.post('/public/pages/:pageId/data/:dataset/rows', async (req, res) => {
|
||||
const publicService = getPageDataPublicService?.();
|
||||
if (!publicService) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
const payload = parseRowPayload(req);
|
||||
const payload = parseRowPayload(req, { stripMeta: true });
|
||||
if (!payload) return sendError(res, req, 400, 'invalid_request', '提交数据必须是 JSON 对象');
|
||||
try {
|
||||
const result = await publicService.insertRow(req.params.pageId, req.params.dataset, req, payload);
|
||||
@@ -211,7 +358,7 @@ export function attachPageDataRoutes(api, deps) {
|
||||
api.patch('/public/pages/:pageId/data/:dataset/rows/:rowId', async (req, res) => {
|
||||
const publicService = getPageDataPublicService?.();
|
||||
if (!publicService) return sendError(res, req, 503, 'feature_disabled', 'Page Data API 未启用');
|
||||
const payload = parseRowPayload(req);
|
||||
const payload = parseRowPayload(req, { stripMeta: true });
|
||||
if (!payload) return sendError(res, req, 400, 'invalid_request', '更新数据必须是 JSON 对象');
|
||||
try {
|
||||
const result = await publicService.updateRow(
|
||||
|
||||
Reference in New Issue
Block a user