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:
+442
-26
@@ -2,15 +2,35 @@ import { resolveMindSpaceUserPublishDir } from './mindspace-runtime-config.mjs';
|
||||
import { publicationInternals } from './mindspace-publications.mjs';
|
||||
import {
|
||||
buildEffectiveDataset,
|
||||
buildPageDataPolicyFromPublishInput,
|
||||
closePolicyDataset,
|
||||
normalizePageAccessPolicy,
|
||||
policyAllowsAction,
|
||||
} from './page-access-policy.mjs';
|
||||
import {
|
||||
extractPageDataCaptchaToken,
|
||||
stripPageDataMetaFields,
|
||||
verifyPageDataCaptcha,
|
||||
} from './page-data-captcha.mjs';
|
||||
import {
|
||||
buildEffectiveDatasetForRole,
|
||||
resolveRowAccessScope,
|
||||
resolveRolePermissions,
|
||||
resolveVisitorRole,
|
||||
roleAllowsAction,
|
||||
} from './page-access-visitor.mjs';
|
||||
import { readPageAccessPolicy, writePageAccessPolicy } from './page-data-policy-store.mjs';
|
||||
import {
|
||||
createPageDataRateLimiter,
|
||||
createPageDataSessionStore,
|
||||
hashClientMeta,
|
||||
} from './page-data-session-store.mjs';
|
||||
import { appendPageDataLog, listPageDataLogs } from './page-data-log-store.mjs';
|
||||
import {
|
||||
getPageDataPolicyIndex,
|
||||
listPageDataPolicyIndexByOwner,
|
||||
upsertPageDataPolicyIndex,
|
||||
} from './page-data-policy-index.mjs';
|
||||
import { createUserDataSpaceService } from './user-data-space-service.mjs';
|
||||
|
||||
function mapPublicError(error) {
|
||||
@@ -45,12 +65,18 @@ function mapPublicError(error) {
|
||||
if (code === 'forbidden') {
|
||||
return { status: 403, code: 'forbidden', message: error.message };
|
||||
}
|
||||
if (code === 'row_not_allowed' || code === 'row_policy_invalid') {
|
||||
return { status: 403, code: code ?? 'row_not_allowed', message: error.message };
|
||||
}
|
||||
if (code === 'unauthorized') {
|
||||
return { status: 401, code: 'unauthorized', message: error.message };
|
||||
}
|
||||
if (code === 'invalid_payload' || code === 'payload_too_large' || code === 'invalid_policy') {
|
||||
return { status: 400, code: code ?? 'invalid_request', message: error.message };
|
||||
}
|
||||
if (code === 'captcha_required' || code === 'captcha_invalid') {
|
||||
return { status: error.status ?? 400, code, message: error.message };
|
||||
}
|
||||
return {
|
||||
status: 400,
|
||||
code: 'page_data_failed',
|
||||
@@ -145,7 +171,7 @@ export function createPageDataPublicService(deps = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveEffectiveDataset(ownerUserId, policy, datasetName) {
|
||||
function resolveEffectiveDataset(ownerUserId, policy, datasetName, rolePermissions = null) {
|
||||
const policyDataset = policy.datasets?.[datasetName];
|
||||
if (!policyDataset) {
|
||||
throw Object.assign(new Error('dataset 未授权'), { code: 'action_not_allowed' });
|
||||
@@ -155,11 +181,24 @@ export function createPageDataPublicService(deps = {}) {
|
||||
if (!registryDataset) {
|
||||
throw Object.assign(new Error('dataset 不存在'), { code: 'dataset_not_found' });
|
||||
}
|
||||
const effective = buildEffectiveDataset(registryDataset, policyDataset);
|
||||
const effective = rolePermissions
|
||||
? buildEffectiveDatasetForRole(registryDataset, policyDataset, rolePermissions)
|
||||
: buildEffectiveDataset(registryDataset, policyDataset);
|
||||
if (!effective) {
|
||||
throw Object.assign(new Error('dataset 未授权'), { code: 'action_not_allowed' });
|
||||
}
|
||||
return { ownerService, effective };
|
||||
return { ownerService, effective, policyDataset };
|
||||
}
|
||||
|
||||
function resolveRowScope(ownerService, effective, policyDataset, visitorContext) {
|
||||
if (!policyDataset?.rowPolicy || !visitorContext) return null;
|
||||
const tableColumns = ownerService.listTableColumns(effective.table);
|
||||
return resolveRowAccessScope(policyDataset.rowPolicy, {
|
||||
visitorRole: visitorContext.visitorRole,
|
||||
visitorUserId: visitorContext.visitorUserId,
|
||||
ownerUserId: visitorContext.ownerUserId,
|
||||
tableColumns,
|
||||
});
|
||||
}
|
||||
|
||||
function enforceRateLimit(req, publication, action) {
|
||||
@@ -198,10 +237,24 @@ export function createPageDataPublicService(deps = {}) {
|
||||
if (!viewerId) {
|
||||
throw Object.assign(new Error('需要登录后才能访问页面数据'), { code: 'auth_required' });
|
||||
}
|
||||
const visitorRole = resolveVisitorRole(policy, viewerId);
|
||||
if (!visitorRole) {
|
||||
throw Object.assign(new Error('当前账号未被授权访问此页面数据'), { code: 'forbidden' });
|
||||
}
|
||||
const rolePermissions = resolveRolePermissions(policy, visitorRole);
|
||||
if (!policyAllowsAction(policy, datasetName, action)) {
|
||||
throw Object.assign(new Error(`dataset 未授权 ${action}`), { code: 'action_not_allowed' });
|
||||
}
|
||||
return { accessMode, session: { sessionId: viewerId, ownerUserId: publication.user_id } };
|
||||
if (!roleAllowsAction(rolePermissions, action)) {
|
||||
throw Object.assign(new Error(`当前角色未授权 ${action}`), { code: 'action_not_allowed' });
|
||||
}
|
||||
return {
|
||||
accessMode,
|
||||
session: { sessionId: viewerId, ownerUserId: publication.user_id, visitorUserId: viewerId },
|
||||
visitorRole,
|
||||
rolePermissions,
|
||||
ownerUserId: publication.user_id,
|
||||
};
|
||||
}
|
||||
|
||||
throw Object.assign(new Error('当前页面访问模式不支持 Page Data API'), { code: 'action_not_allowed' });
|
||||
@@ -242,10 +295,27 @@ export function createPageDataPublicService(deps = {}) {
|
||||
return runPublicAction(async () => {
|
||||
const publication = await queryPublication(pageId);
|
||||
const policy = await loadPolicy(publication);
|
||||
resolveAccessContext({ publication, policy, req, action: 'read', datasetName });
|
||||
const access = resolveAccessContext({ publication, policy, req, action: 'read', datasetName });
|
||||
enforceRateLimit(req, publication, 'read');
|
||||
const { ownerService, effective } = resolveEffectiveDataset(publication.user_id, policy, datasetName);
|
||||
return ownerService.readRowsForDataset(effective, query);
|
||||
const { ownerService, effective, policyDataset } = resolveEffectiveDataset(
|
||||
publication.user_id,
|
||||
policy,
|
||||
datasetName,
|
||||
access.rolePermissions ?? null,
|
||||
);
|
||||
const rowScope = resolveRowScope(
|
||||
ownerService,
|
||||
effective,
|
||||
policyDataset,
|
||||
access.accessMode === 'login_required'
|
||||
? {
|
||||
visitorRole: access.visitorRole,
|
||||
visitorUserId: access.session?.visitorUserId,
|
||||
ownerUserId: access.ownerUserId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
return ownerService.readRowsForDataset(effective, { ...query, rowScope });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -253,8 +323,13 @@ export function createPageDataPublicService(deps = {}) {
|
||||
return runPublicAction(async () => {
|
||||
const publication = await queryPublication(pageId);
|
||||
const policy = await loadPolicy(publication);
|
||||
resolveAccessContext({ publication, policy, req, action: 'read', datasetName });
|
||||
const { ownerService, effective } = resolveEffectiveDataset(publication.user_id, policy, datasetName);
|
||||
const access = resolveAccessContext({ publication, policy, req, action: 'read', datasetName });
|
||||
const { ownerService, effective } = resolveEffectiveDataset(
|
||||
publication.user_id,
|
||||
policy,
|
||||
datasetName,
|
||||
access.rolePermissions ?? null,
|
||||
);
|
||||
return ownerService.getSchemaForDataset(effective);
|
||||
});
|
||||
}
|
||||
@@ -263,30 +338,112 @@ export function createPageDataPublicService(deps = {}) {
|
||||
return runPublicAction(async () => {
|
||||
const publication = await queryPublication(pageId);
|
||||
const policy = await loadPolicy(publication);
|
||||
resolveAccessContext({ publication, policy, req, action: 'read', datasetName });
|
||||
const { ownerService, effective } = resolveEffectiveDataset(publication.user_id, policy, datasetName);
|
||||
return ownerService.getStatsForDataset(effective);
|
||||
const access = resolveAccessContext({ publication, policy, req, action: 'read', datasetName });
|
||||
const { ownerService, effective, policyDataset } = resolveEffectiveDataset(
|
||||
publication.user_id,
|
||||
policy,
|
||||
datasetName,
|
||||
access.rolePermissions ?? null,
|
||||
);
|
||||
const rowScope = resolveRowScope(
|
||||
ownerService,
|
||||
effective,
|
||||
policyDataset,
|
||||
access.accessMode === 'login_required'
|
||||
? {
|
||||
visitorRole: access.visitorRole,
|
||||
visitorUserId: access.session?.visitorUserId,
|
||||
ownerUserId: access.ownerUserId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
return ownerService.getStatsForDataset(effective, { rowScope });
|
||||
});
|
||||
}
|
||||
|
||||
async function insertRow(pageId, datasetName, req, payload) {
|
||||
function recordPublicLog(workspaceRoot, publication, {
|
||||
datasetName,
|
||||
action,
|
||||
rowId,
|
||||
req,
|
||||
session,
|
||||
accessMode,
|
||||
visitorRole,
|
||||
summary,
|
||||
}) {
|
||||
appendPageDataLog(workspaceRoot, publication.page_id, {
|
||||
pageId: publication.page_id,
|
||||
dataset: datasetName,
|
||||
action,
|
||||
rowId: rowId ?? null,
|
||||
accessMode,
|
||||
actor: {
|
||||
type: accessMode === 'public' ? 'anonymous' : accessMode,
|
||||
userId: req.currentUser?.id ?? session?.visitorUserId ?? null,
|
||||
visitorUserId: session?.visitorUserId ?? req.currentUser?.id ?? null,
|
||||
role: visitorRole ?? null,
|
||||
sessionId: session?.sessionId ?? null,
|
||||
ipHash: hashClientMeta(req.ip ?? req.socket?.remoteAddress),
|
||||
userAgentHash: hashClientMeta(req.headers?.['user-agent']),
|
||||
},
|
||||
summary: summary ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async function insertRow(pageId, datasetName, req, payloadInput) {
|
||||
return runPublicAction(async () => {
|
||||
const publication = await queryPublication(pageId);
|
||||
const policy = await loadPolicy(publication);
|
||||
const { session } = resolveAccessContext({
|
||||
const access = resolveAccessContext({
|
||||
publication,
|
||||
policy,
|
||||
req,
|
||||
action: 'insert',
|
||||
datasetName,
|
||||
});
|
||||
if (access.accessMode === 'public') {
|
||||
await verifyPageDataCaptcha(
|
||||
extractPageDataCaptchaToken(req, payloadInput),
|
||||
req.ip ?? req.socket?.remoteAddress,
|
||||
);
|
||||
}
|
||||
const payload = stripPageDataMetaFields(payloadInput);
|
||||
enforceRateLimit(req, publication, 'insert');
|
||||
const { ownerService, effective } = resolveEffectiveDataset(publication.user_id, policy, datasetName);
|
||||
return ownerService.insertRowForDataset(effective, payload, {
|
||||
const { ownerService, effective, policyDataset } = resolveEffectiveDataset(
|
||||
publication.user_id,
|
||||
policy,
|
||||
datasetName,
|
||||
access.rolePermissions ?? null,
|
||||
);
|
||||
const rowScope = resolveRowScope(
|
||||
ownerService,
|
||||
effective,
|
||||
policyDataset,
|
||||
access.accessMode === 'login_required'
|
||||
? {
|
||||
visitorRole: access.visitorRole,
|
||||
visitorUserId: access.session?.visitorUserId,
|
||||
ownerUserId: access.ownerUserId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
const result = await ownerService.insertRowForDataset(effective, payload, {
|
||||
createdIpHash: hashClientMeta(req.ip ?? req.socket?.remoteAddress),
|
||||
userAgentHash: hashClientMeta(req.headers?.['user-agent']),
|
||||
pageDataSessionId: session?.sessionId ?? null,
|
||||
pageDataSessionId: access.session?.sessionId ?? null,
|
||||
rowScope,
|
||||
});
|
||||
recordPublicLog(resolveWorkspaceRoot(publication.user_id), publication, {
|
||||
datasetName,
|
||||
action: 'insert',
|
||||
rowId: result.row?.id ?? null,
|
||||
req,
|
||||
session: access.session,
|
||||
accessMode: access.accessMode,
|
||||
visitorRole: access.visitorRole,
|
||||
summary: { fields: Object.keys(payload ?? {}) },
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,7 +451,7 @@ export function createPageDataPublicService(deps = {}) {
|
||||
return runPublicAction(async () => {
|
||||
const publication = await queryPublication(pageId);
|
||||
const policy = await loadPolicy(publication);
|
||||
const { session } = resolveAccessContext({
|
||||
const access = resolveAccessContext({
|
||||
publication,
|
||||
policy,
|
||||
req,
|
||||
@@ -302,10 +459,39 @@ export function createPageDataPublicService(deps = {}) {
|
||||
datasetName,
|
||||
});
|
||||
enforceRateLimit(req, publication, 'update');
|
||||
const { ownerService, effective } = resolveEffectiveDataset(publication.user_id, policy, datasetName);
|
||||
return ownerService.updateRowForDataset(effective, rowId, payload, {
|
||||
updatedByLabel: req.body?.updated_by_label ?? session?.sessionId ?? 'public',
|
||||
const { ownerService, effective, policyDataset } = resolveEffectiveDataset(
|
||||
publication.user_id,
|
||||
policy,
|
||||
datasetName,
|
||||
access.rolePermissions ?? null,
|
||||
);
|
||||
const rowScope = resolveRowScope(
|
||||
ownerService,
|
||||
effective,
|
||||
policyDataset,
|
||||
access.accessMode === 'login_required'
|
||||
? {
|
||||
visitorRole: access.visitorRole,
|
||||
visitorUserId: access.session?.visitorUserId,
|
||||
ownerUserId: access.ownerUserId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
const result = await ownerService.updateRowForDataset(effective, rowId, payload, {
|
||||
updatedByLabel: req.body?.updated_by_label ?? access.session?.sessionId ?? 'public',
|
||||
rowScope,
|
||||
});
|
||||
recordPublicLog(resolveWorkspaceRoot(publication.user_id), publication, {
|
||||
datasetName,
|
||||
action: 'update',
|
||||
rowId,
|
||||
req,
|
||||
session: access.session,
|
||||
accessMode: access.accessMode,
|
||||
visitorRole: access.visitorRole,
|
||||
summary: { fields: Object.keys(payload ?? {}) },
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -313,7 +499,7 @@ export function createPageDataPublicService(deps = {}) {
|
||||
return runPublicAction(async () => {
|
||||
const publication = await queryPublication(pageId);
|
||||
const policy = await loadPolicy(publication);
|
||||
const { session } = resolveAccessContext({
|
||||
const access = resolveAccessContext({
|
||||
publication,
|
||||
policy,
|
||||
req,
|
||||
@@ -321,13 +507,48 @@ export function createPageDataPublicService(deps = {}) {
|
||||
datasetName,
|
||||
});
|
||||
enforceRateLimit(req, publication, 'soft_delete');
|
||||
const { ownerService, effective } = resolveEffectiveDataset(publication.user_id, policy, datasetName);
|
||||
return ownerService.softDeleteRowForDataset(effective, rowId, {
|
||||
deletedBy: session?.sessionId ?? 'public',
|
||||
const { ownerService, effective, policyDataset } = resolveEffectiveDataset(
|
||||
publication.user_id,
|
||||
policy,
|
||||
datasetName,
|
||||
access.rolePermissions ?? null,
|
||||
);
|
||||
const rowScope = resolveRowScope(
|
||||
ownerService,
|
||||
effective,
|
||||
policyDataset,
|
||||
access.accessMode === 'login_required'
|
||||
? {
|
||||
visitorRole: access.visitorRole,
|
||||
visitorUserId: access.session?.visitorUserId,
|
||||
ownerUserId: access.ownerUserId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
const result = await ownerService.softDeleteRowForDataset(effective, rowId, {
|
||||
deletedBy: access.session?.sessionId ?? 'public',
|
||||
rowScope,
|
||||
});
|
||||
recordPublicLog(resolveWorkspaceRoot(publication.user_id), publication, {
|
||||
datasetName,
|
||||
action: 'soft_delete',
|
||||
rowId,
|
||||
req,
|
||||
session: access.session,
|
||||
accessMode: access.accessMode,
|
||||
visitorRole: access.visitorRole,
|
||||
summary: null,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
function syncPolicyIndex(policy) {
|
||||
const pool = getPool();
|
||||
if (!pool || !policy) return null;
|
||||
return upsertPageDataPolicyIndex(pool, policy).catch(() => null);
|
||||
}
|
||||
|
||||
async function getOwnerPolicy(user, pageId) {
|
||||
if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 });
|
||||
const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id);
|
||||
@@ -355,7 +576,196 @@ export function createPageDataPublicService(deps = {}) {
|
||||
},
|
||||
{ fallbackPageId: pageId, fallbackOwnerUserId: user.id },
|
||||
);
|
||||
return writePageAccessPolicy(workspaceRoot, policy);
|
||||
const saved = writePageAccessPolicy(workspaceRoot, policy);
|
||||
await syncPolicyIndex(saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
async function saveOwnerPolicyFromPublish(user, pageId, { datasetName, capabilities = {} } = {}) {
|
||||
if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 });
|
||||
const publication = await queryPublication(pageId).catch(() => null);
|
||||
if (!publication || publication.user_id !== user.id) {
|
||||
throw Object.assign(new Error('页面不存在或未发布'), { code: 'publication_not_found', status: 404 });
|
||||
}
|
||||
const normalizedDatasetName = String(datasetName ?? '').trim();
|
||||
if (!normalizedDatasetName) {
|
||||
throw Object.assign(new Error('缺少 dataset 名称'), { code: 'invalid_request', status: 400 });
|
||||
}
|
||||
const ownerService = createOwnerService(user.id);
|
||||
const registryDataset = ownerService.getDataset(normalizedDatasetName);
|
||||
if (!registryDataset) {
|
||||
throw Object.assign(new Error('dataset 未注册'), { code: 'dataset_not_found', status: 404 });
|
||||
}
|
||||
const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id);
|
||||
const policy = buildPageDataPolicyFromPublishInput({
|
||||
pageId,
|
||||
ownerUserId: user.id,
|
||||
accessMode: publication.access_mode,
|
||||
datasetName: registryDataset.name,
|
||||
registryDataset,
|
||||
capabilities,
|
||||
});
|
||||
const saved = writePageAccessPolicy(workspaceRoot, policy);
|
||||
await syncPolicyIndex(saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
async function listOwnerPolicies(user) {
|
||||
if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 });
|
||||
const pool = getPool();
|
||||
if (!pool) return [];
|
||||
return listPageDataPolicyIndexByOwner(pool, user.id);
|
||||
}
|
||||
|
||||
async function getPageOpsOverview(user, pageId) {
|
||||
if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 });
|
||||
const publication = await queryPublication(pageId);
|
||||
if (publication.user_id !== user.id) {
|
||||
throw Object.assign(new Error('无权访问该页面运维信息'), { code: 'forbidden', status: 403 });
|
||||
}
|
||||
const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id);
|
||||
const policy = readPageAccessPolicy(workspaceRoot, pageId);
|
||||
if (!policy) {
|
||||
throw Object.assign(new Error('页面数据策略未配置'), { code: 'policy_not_found', status: 404 });
|
||||
}
|
||||
const ownerService = createOwnerService(user.id);
|
||||
const datasets = [];
|
||||
for (const [name, config] of Object.entries(policy.datasets ?? {})) {
|
||||
let stats = null;
|
||||
try {
|
||||
if (ownerService.getDataset(name)) {
|
||||
stats = ownerService.getDatasetStats(name);
|
||||
}
|
||||
} catch {
|
||||
stats = null;
|
||||
}
|
||||
datasets.push({
|
||||
name,
|
||||
read: config.read,
|
||||
insert: config.insert,
|
||||
update: config.update,
|
||||
softDelete: config.softDelete,
|
||||
closed: Boolean(config.closed),
|
||||
closedAt: config.closedAt ?? null,
|
||||
stats,
|
||||
});
|
||||
}
|
||||
const logs = listPageDataLogs(workspaceRoot, pageId, { limit: 50, offset: 0 });
|
||||
const recentSubmissions = logs.filter((entry) => entry.action === 'insert').slice(0, 10);
|
||||
const activeSessions = sessionStore.listByPageId(pageId);
|
||||
const index = await getPageDataPolicyIndex(getPool(), pageId);
|
||||
const registeredDatasets = ownerService.listDatasets().map((dataset) => dataset.name);
|
||||
const configuredNames = new Set(Object.keys(policy.datasets ?? {}));
|
||||
const unconfiguredDatasets = registeredDatasets.filter((name) => !configuredNames.has(name));
|
||||
return {
|
||||
pageId,
|
||||
publication: {
|
||||
id: publication.id,
|
||||
pageId: publication.page_id,
|
||||
accessMode: publication.access_mode,
|
||||
status: publication.status,
|
||||
},
|
||||
policy,
|
||||
index,
|
||||
registeredDatasets,
|
||||
unconfiguredDatasets,
|
||||
datasets,
|
||||
recentLogs: logs.slice(0, 20),
|
||||
recentSubmissions,
|
||||
activeSessionCount: activeSessions.length,
|
||||
activeSessions,
|
||||
};
|
||||
}
|
||||
|
||||
async function revokePageDataTokens(user, pageId, { token = null, revokeAll = false } = {}) {
|
||||
if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 });
|
||||
const publication = await queryPublication(pageId);
|
||||
if (publication.user_id !== user.id) {
|
||||
throw Object.assign(new Error('无权撤销该页面令牌'), { code: 'forbidden', status: 403 });
|
||||
}
|
||||
let revokedCount = 0;
|
||||
if (revokeAll) {
|
||||
revokedCount = sessionStore.revokeByPageId(pageId);
|
||||
} else if (token) {
|
||||
revokedCount = sessionStore.revoke(token) ? 1 : 0;
|
||||
} else {
|
||||
throw Object.assign(new Error('请提供 token 或设置 revokeAll'), { code: 'invalid_request', status: 400 });
|
||||
}
|
||||
const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id);
|
||||
appendPageDataLog(workspaceRoot, pageId, {
|
||||
pageId,
|
||||
action: 'revoke_tokens',
|
||||
dataset: null,
|
||||
rowId: null,
|
||||
accessMode: publication.access_mode,
|
||||
actor: { type: 'owner', userId: user.id, role: 'owner' },
|
||||
summary: { revokedCount, revokeAll: Boolean(revokeAll) },
|
||||
});
|
||||
return { pageId, revokedCount, revokeAll: Boolean(revokeAll) };
|
||||
}
|
||||
|
||||
async function resetPageDataPassword(user, pageId, password) {
|
||||
if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 });
|
||||
const publication = await queryPublication(pageId);
|
||||
if (publication.user_id !== user.id) {
|
||||
throw Object.assign(new Error('无权重置该页面口令'), { code: 'forbidden', status: 403 });
|
||||
}
|
||||
if (publication.access_mode !== 'password') {
|
||||
throw Object.assign(new Error('当前页面不是口令访问模式'), { code: 'auth_not_required', status: 400 });
|
||||
}
|
||||
const normalizedPassword = publicationInternals.normalizePassword(password, true);
|
||||
const passwordHash = publicationInternals.hashPassword(normalizedPassword);
|
||||
const pool = getPool();
|
||||
if (!pool) {
|
||||
throw Object.assign(new Error('数据库未配置'), { code: 'feature_disabled', status: 503 });
|
||||
}
|
||||
await pool.query(
|
||||
`UPDATE h5_publish_records SET password_hash = ?, updated_at = ? WHERE id = ? AND user_id = ?`,
|
||||
[passwordHash, Date.now(), publication.id, user.id],
|
||||
);
|
||||
const revokedCount = sessionStore.revokeByPageId(pageId);
|
||||
const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id);
|
||||
appendPageDataLog(workspaceRoot, pageId, {
|
||||
pageId,
|
||||
action: 'password_reset',
|
||||
dataset: null,
|
||||
rowId: null,
|
||||
accessMode: publication.access_mode,
|
||||
actor: { type: 'owner', userId: user.id, role: 'owner' },
|
||||
summary: { revokedSessions: revokedCount },
|
||||
});
|
||||
return { pageId, passwordReset: true, revokedSessions: revokedCount };
|
||||
}
|
||||
|
||||
async function closePageDataset(user, pageId, datasetName) {
|
||||
if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 });
|
||||
const publication = await queryPublication(pageId);
|
||||
if (publication.user_id !== user.id) {
|
||||
throw Object.assign(new Error('无权关闭该页面 dataset'), { code: 'forbidden', status: 403 });
|
||||
}
|
||||
const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id);
|
||||
const policy = readPageAccessPolicy(workspaceRoot, pageId);
|
||||
if (!policy) {
|
||||
throw Object.assign(new Error('页面数据策略未配置'), { code: 'policy_not_found', status: 404 });
|
||||
}
|
||||
const closedPolicy = closePolicyDataset(policy, datasetName);
|
||||
const saved = writePageAccessPolicy(workspaceRoot, closedPolicy);
|
||||
await syncPolicyIndex(saved);
|
||||
appendPageDataLog(workspaceRoot, pageId, {
|
||||
pageId,
|
||||
action: 'close_dataset',
|
||||
dataset: datasetName,
|
||||
rowId: null,
|
||||
accessMode: publication.access_mode,
|
||||
actor: { type: 'owner', userId: user.id, role: 'owner' },
|
||||
summary: null,
|
||||
});
|
||||
return {
|
||||
pageId,
|
||||
dataset: datasetName,
|
||||
closed: true,
|
||||
policyDataset: saved.datasets[datasetName],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -368,6 +778,12 @@ export function createPageDataPublicService(deps = {}) {
|
||||
softDeleteRow,
|
||||
getOwnerPolicy,
|
||||
saveOwnerPolicy,
|
||||
saveOwnerPolicyFromPublish,
|
||||
listOwnerPolicies,
|
||||
getPageOpsOverview,
|
||||
revokePageDataTokens,
|
||||
resetPageDataPassword,
|
||||
closePageDataset,
|
||||
queryPublication,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user