Files
memind/page-data-public-service.mjs
T

802 lines
30 KiB
JavaScript

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) {
if (error?.status && error?.code && error?.message) {
return error;
}
const code = error?.code;
if (code === 'publication_not_found') {
return { status: 404, code: 'publication_not_found', message: '公开页面不存在或已下线' };
}
if (code === 'policy_not_found') {
return { status: 404, code: 'policy_not_found', message: '页面数据策略未配置' };
}
if (code === 'dataset_not_found') {
return { status: 404, code: 'dataset_not_found', message: '数据集不存在' };
}
if (code === 'action_not_allowed') {
return { status: 403, code: 'action_not_allowed', message: error.message };
}
if (code === 'columns_not_allowed') {
return { status: 403, code: 'columns_not_allowed', message: error.message };
}
if (code === 'auth_required' || code === 'token_invalid' || code === 'password_invalid') {
return { status: 401, code, message: error.message };
}
if (code === 'rate_limited') {
return { status: 429, code: 'rate_limited', message: error.message };
}
if (code === 'invalid_identifier') {
return { status: 403, code: 'columns_not_allowed', message: error.message };
}
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',
message: error instanceof Error ? error.message : '页面数据操作失败',
};
}
export function mapPageDataPublicError(error) {
return mapPublicError(error);
}
function rethrowPublicError(error) {
throw mapPublicError(error);
}
async function runPublicAction(fn) {
try {
return await fn();
} catch (error) {
rethrowPublicError(error);
}
}
function extractPageDataToken(req) {
const header = String(req.headers?.['x-page-data-token'] ?? req.headers?.authorization ?? '').trim();
if (!header) return null;
if (header.toLowerCase().startsWith('bearer ')) return header.slice(7).trim();
return header;
}
export function isPageDataPublicPath(path, method) {
if (!path.startsWith('/public/pages/')) return false;
if (method === 'POST' && /^\/public\/pages\/[^/]+\/data-auth$/.test(path)) return true;
if (method === 'POST' && /^\/public\/pages\/[^/]+\/data\/[^/]+\/rows$/.test(path)) return true;
if (method === 'PATCH' && /^\/public\/pages\/[^/]+\/data\/[^/]+\/rows\/[^/]+$/.test(path)) return true;
if (method === 'DELETE' && /^\/public\/pages\/[^/]+\/data\/[^/]+\/rows\/[^/]+$/.test(path)) return true;
if (method !== 'GET') return false;
return (
/^\/public\/pages\/[^/]+\/data\/[^/]+$/.test(path) ||
/^\/public\/pages\/[^/]+\/data\/[^/]+\/schema$/.test(path) ||
/^\/public\/pages\/[^/]+\/data\/[^/]+\/stats$/.test(path)
);
}
export function createPageDataPublicService(deps = {}) {
const getPool = deps.getPool ?? (() => null);
const resolveH5Root = deps.resolveH5Root ?? (() => process.cwd());
const resolveWorkspaceRootForOwner =
deps.resolveWorkspaceRootForOwner ??
((ownerUserId) => resolveMindSpaceUserPublishDir(resolveH5Root(), { id: ownerUserId }));
const sessionStore = deps.sessionStore ?? createPageDataSessionStore();
const rateLimiter =
deps.rateLimiter ??
createPageDataRateLimiter({
windowMs: Number(process.env.PAGE_DATA_RATE_WINDOW_MS ?? 60_000),
maxRequests: Number(process.env.PAGE_DATA_RATE_MAX_REQUESTS ?? 30),
});
async function queryPublication(pageId) {
const pool = getPool();
if (!pool) {
throw Object.assign(new Error('数据库未配置'), { code: 'feature_disabled', status: 503 });
}
const [rows] = await pool.query(
`SELECT pr.id, pr.user_id, pr.page_id, pr.access_mode, pr.password_hash, pr.status, pr.expires_at
FROM h5_publish_records pr
WHERE pr.page_id = ? AND pr.status = 'online'
ORDER BY pr.published_at DESC
LIMIT 1`,
[pageId],
);
const row = rows[0];
if (!row) {
throw Object.assign(new Error('公开页面不存在或已下线'), { code: 'publication_not_found' });
}
if (row.expires_at && Number(row.expires_at) <= Date.now()) {
throw Object.assign(new Error('公开页面不存在或已下线'), { code: 'publication_not_found' });
}
return row;
}
function resolveWorkspaceRoot(ownerUserId) {
return resolveWorkspaceRootForOwner(ownerUserId);
}
function createOwnerService(ownerUserId) {
const pool = getPool();
return createUserDataSpaceService({
workspaceRoot: resolveWorkspaceRoot(ownerUserId),
userId: ownerUserId,
query: pool ?? null,
});
}
async function resolveEffectiveDataset(ownerUserId, policy, datasetName, rolePermissions = null) {
const policyDataset = policy.datasets?.[datasetName];
if (!policyDataset) {
throw Object.assign(new Error('dataset 未授权'), { code: 'action_not_allowed' });
}
const ownerService = createOwnerService(ownerUserId);
const registryDataset = await ownerService.getDataset(datasetName);
if (!registryDataset) {
throw Object.assign(new Error('dataset 不存在'), { code: 'dataset_not_found' });
}
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, policyDataset };
}
async function resolveRowScope(ownerService, effective, policyDataset, visitorContext) {
if (!policyDataset?.rowPolicy || !visitorContext) return null;
const tableColumns = await ownerService.listTableColumns(effective.table);
return resolveRowAccessScope(policyDataset.rowPolicy, {
visitorRole: visitorContext.visitorRole,
visitorUserId: visitorContext.visitorUserId,
ownerUserId: visitorContext.ownerUserId,
tableColumns,
});
}
function enforceRateLimit(req, publication, action) {
const ip = req.ip ?? req.socket?.remoteAddress ?? 'unknown';
rateLimiter.check(`${publication.page_id}:${action}:${ip}`);
}
function resolveAccessContext({ publication, policy, req, action, datasetName }) {
const accessMode = publication.access_mode;
if (accessMode === 'public') {
if (action === 'update' || action === 'soft_delete') {
throw Object.assign(new Error('当前页面未开放此数据操作'), { code: 'action_not_allowed' });
}
if (action === 'read') {
if (!policyAllowsAction(policy, datasetName, 'read')) {
throw Object.assign(new Error('当前页面未开放此数据操作'), { code: 'action_not_allowed' });
}
return { accessMode, session: null };
}
if (!policyAllowsAction(policy, datasetName, action)) {
throw Object.assign(new Error(`dataset 未授权 ${action}`), { code: 'action_not_allowed' });
}
return { accessMode, session: null };
}
if (accessMode === 'password') {
const token = extractPageDataToken(req);
const session = token ? sessionStore.verify(token) : null;
if (!session || session.pageId !== publication.page_id) {
throw Object.assign(new Error('需要有效的页面数据访问令牌'), {
code: 'token_invalid',
});
}
if (!policyAllowsAction(policy, datasetName, action)) {
throw Object.assign(new Error(`dataset 未授权 ${action}`), { code: 'action_not_allowed' });
}
return { accessMode, session };
}
if (accessMode === 'login_required') {
const viewerId = req.currentUser?.id ?? null;
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' });
}
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' });
}
async function loadPolicy(publication) {
const workspaceRoot = resolveWorkspaceRoot(publication.user_id);
const policy = readPageAccessPolicy(workspaceRoot, publication.page_id);
if (!policy) {
throw Object.assign(new Error('页面数据策略未配置'), { code: 'policy_not_found' });
}
if (policy.ownerUserId !== publication.user_id) {
throw Object.assign(new Error('页面数据策略无效'), { code: 'invalid_policy' });
}
return policy;
}
async function authenticate(pageId, password, req) {
return runPublicAction(async () => {
const publication = await queryPublication(pageId);
enforceRateLimit(req, publication, 'data-auth');
if (publication.access_mode !== 'password') {
throw Object.assign(new Error('当前页面不需要口令认证'), { code: 'auth_not_required' });
}
if (!publicationInternals.verifyPassword(password, publication.password_hash)) {
throw Object.assign(new Error('页面口令不正确'), { code: 'password_invalid' });
}
return sessionStore.issue({
pageId: publication.page_id,
ownerUserId: publication.user_id,
publicationId: publication.id,
accessMode: publication.access_mode,
});
});
}
async function listRows(pageId, datasetName, req, query = {}) {
return runPublicAction(async () => {
const publication = await queryPublication(pageId);
const policy = await loadPolicy(publication);
const access = resolveAccessContext({ publication, policy, req, action: 'read', datasetName });
enforceRateLimit(req, publication, 'read');
const { ownerService, effective, policyDataset } = await resolveEffectiveDataset(
publication.user_id,
policy,
datasetName,
access.rolePermissions ?? null,
);
const rowScope = await 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 });
});
}
async function getSchema(pageId, datasetName, req) {
return runPublicAction(async () => {
const publication = await queryPublication(pageId);
const policy = await loadPolicy(publication);
const access = resolveAccessContext({ publication, policy, req, action: 'read', datasetName });
const { ownerService, effective } = await resolveEffectiveDataset(
publication.user_id,
policy,
datasetName,
access.rolePermissions ?? null,
);
return ownerService.getSchemaForDataset(effective);
});
}
async function getStats(pageId, datasetName, req) {
return runPublicAction(async () => {
const publication = await queryPublication(pageId);
const policy = await loadPolicy(publication);
const access = resolveAccessContext({ publication, policy, req, action: 'read', datasetName });
const { ownerService, effective, policyDataset } = await resolveEffectiveDataset(
publication.user_id,
policy,
datasetName,
access.rolePermissions ?? null,
);
const rowScope = await resolveRowScope(
ownerService,
effective,
policyDataset,
access.accessMode === 'login_required'
? {
visitorRole: access.visitorRole,
visitorUserId: access.session?.visitorUserId,
ownerUserId: access.ownerUserId,
}
: null,
);
return ownerService.getStatsForDataset(effective, { rowScope });
});
}
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 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, policyDataset } = await resolveEffectiveDataset(
publication.user_id,
policy,
datasetName,
access.rolePermissions ?? null,
);
const rowScope = await 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: 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;
});
}
async function updateRow(pageId, datasetName, rowId, req, payload) {
return runPublicAction(async () => {
const publication = await queryPublication(pageId);
const policy = await loadPolicy(publication);
const access = resolveAccessContext({
publication,
policy,
req,
action: 'update',
datasetName,
});
enforceRateLimit(req, publication, 'update');
const { ownerService, effective, policyDataset } = await resolveEffectiveDataset(
publication.user_id,
policy,
datasetName,
access.rolePermissions ?? null,
);
const rowScope = await 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;
});
}
async function deleteRow(pageId, datasetName, rowId, req) {
return runPublicAction(async () => {
const publication = await queryPublication(pageId);
const policy = await loadPolicy(publication);
const deleteAction = policyAllowsAction(policy, datasetName, 'hard_delete')
? 'hard_delete'
: 'soft_delete';
const access = resolveAccessContext({
publication,
policy,
req,
action: deleteAction,
datasetName,
});
enforceRateLimit(req, publication, deleteAction);
const { ownerService, effective, policyDataset } = await resolveEffectiveDataset(
publication.user_id,
policy,
datasetName,
access.rolePermissions ?? null,
);
const rowScope = await resolveRowScope(
ownerService,
effective,
policyDataset,
access.accessMode === 'login_required'
? {
visitorRole: access.visitorRole,
visitorUserId: access.session?.visitorUserId,
ownerUserId: access.ownerUserId,
}
: null,
);
const result = deleteAction === 'hard_delete'
? await ownerService.hardDeleteRowForDataset(effective, rowId, { rowScope })
: await ownerService.softDeleteRowForDataset(effective, rowId, {
deletedBy: access.session?.sessionId ?? 'public',
rowScope,
});
recordPublicLog(resolveWorkspaceRoot(publication.user_id), publication, {
datasetName,
action: deleteAction,
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);
const policy = readPageAccessPolicy(workspaceRoot, pageId);
if (!policy) return null;
if (policy.ownerUserId !== user.id) {
throw Object.assign(new Error('无权访问该页面策略'), { code: 'forbidden', status: 403 });
}
return policy;
}
async function saveOwnerPolicy(user, pageId, policyInput) {
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 workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id);
const policy = normalizePageAccessPolicy(
{
...policyInput,
pageId,
ownerUserId: user.id,
accessMode: policyInput.accessMode ?? publication.access_mode,
},
{ fallbackPageId: pageId, fallbackOwnerUserId: user.id },
);
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 = await 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 (await ownerService.getDataset(name)) {
stats = await 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 = (await 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 {
authenticate,
listRows,
getSchema,
getStats,
insertRow,
updateRow,
deleteRow,
softDeleteRow: deleteRow,
getOwnerPolicy,
saveOwnerPolicy,
saveOwnerPolicyFromPublish,
listOwnerPolicies,
getPageOpsOverview,
revokePageDataTokens,
resetPageDataPassword,
closePageDataset,
queryPublication,
};
}