Add page data delivery and publication guards
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { createPageService, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
|
||||
import { createPublicationService, publicationInternals } from './mindspace-publications.mjs';
|
||||
import { slugFromPageTitle } from './mindspace-chat-plaza.mjs';
|
||||
import { writePageAccessPolicy } from './page-data-policy-store.mjs';
|
||||
import { normalizePageAccessPolicy } from './page-access-policy.mjs';
|
||||
import { upsertPageDataPolicyIndex } from './page-data-policy-index.mjs';
|
||||
import { syncPageDataPolicyAccessMode } from './page-data-publish-sync.mjs';
|
||||
import { createMindSpacePublicUrl } from './mindspace-canonical-url.mjs';
|
||||
import { resolvePublicBaseUrl } from './user-publish.mjs';
|
||||
import { createUserDataSpaceService } from './user-data-space-service.mjs';
|
||||
import {
|
||||
assertPolicyMatchesHtmlDatasets,
|
||||
buildPageDataPolicyDatasetsFromRegistry,
|
||||
detectPageDataDatasetUsageFromHtml,
|
||||
} from './page-data-html-detect.mjs';
|
||||
|
||||
export const DEFAULT_PAGE_DATA_ADMIN_PASSWORD = '88888888';
|
||||
|
||||
export function resolvePageDataBindAccess(accessMode = 'public') {
|
||||
return publicationInternals.normalizeAccessMode(accessMode);
|
||||
}
|
||||
|
||||
/** password 模式:用户未传口令时使用 DEFAULT_PAGE_DATA_ADMIN_PASSWORD;<8 位抛错 */
|
||||
export function resolvePageDataBindPassword(accessMode, password = null) {
|
||||
const mode = resolvePageDataBindAccess(accessMode);
|
||||
if (mode !== 'password') return null;
|
||||
const explicit = String(password ?? '').trim();
|
||||
if (explicit) {
|
||||
return publicationInternals.normalizePassword(explicit, true);
|
||||
}
|
||||
return DEFAULT_PAGE_DATA_ADMIN_PASSWORD;
|
||||
}
|
||||
|
||||
function titleFromPublicHtml(content, relativePath) {
|
||||
const match = String(content ?? '').match(/<title[^>]*>([^<]+)<\/title>/i);
|
||||
const fromTitle = match?.[1]?.replace(/\s+/g, ' ').trim();
|
||||
if (fromTitle) return fromTitle;
|
||||
const basename = path.basename(String(relativePath ?? ''), '.html');
|
||||
return basename || 'MindSpace 页面';
|
||||
}
|
||||
|
||||
function normalizePublicHtmlRelativePath(relativePath) {
|
||||
const normalized = normalizeWorkspaceRelativePath(relativePath);
|
||||
if (!normalized || !normalized.startsWith('public/') || !normalized.toLowerCase().endsWith('.html')) {
|
||||
throw Object.assign(new Error('仅支持 workspace 公开 HTML 页面(public/*.html)'), {
|
||||
code: 'invalid_public_html_path',
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function ensureWorkspaceHtmlPage({ userId, relativePath, content, title, mindSpacePages }) {
|
||||
const normalized = normalizePublicHtmlRelativePath(relativePath);
|
||||
let page = await mindSpacePages.findPageByRelativePath(userId, normalized);
|
||||
const pageInput = {
|
||||
title: title ?? page?.title ?? titleFromPublicHtml(content, normalized),
|
||||
content,
|
||||
contentFormat: 'html',
|
||||
pageType: 'html',
|
||||
categoryCode: 'draft',
|
||||
};
|
||||
if (page) {
|
||||
return mindSpacePages.updatePage(
|
||||
userId,
|
||||
page.id,
|
||||
{
|
||||
...pageInput,
|
||||
expectedVersion: page.versionNo,
|
||||
},
|
||||
{
|
||||
snapshot: {
|
||||
content_mode: 'static_html',
|
||||
relative_path: normalized,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
return mindSpacePages.createFromChat(userId, pageInput, {
|
||||
snapshot: {
|
||||
content_mode: 'static_html',
|
||||
relative_path: normalized,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureWorkspaceHtmlPublished({
|
||||
userId,
|
||||
page,
|
||||
mindSpacePublications,
|
||||
accessMode,
|
||||
password,
|
||||
urlSlug,
|
||||
}) {
|
||||
const normalizedAccessMode = resolvePageDataBindAccess(accessMode);
|
||||
const resolvedPassword = resolvePageDataBindPassword(normalizedAccessMode, password);
|
||||
const existing = await mindSpacePublications.getCurrent(userId, page.id);
|
||||
if (existing?.id) {
|
||||
const accessChanged = existing.accessMode !== normalizedAccessMode;
|
||||
const needsPassword =
|
||||
normalizedAccessMode === 'password' && Boolean(resolvedPassword);
|
||||
if (accessChanged || needsPassword) {
|
||||
const updated = await mindSpacePublications.updatePublicationAccess(userId, existing.id, {
|
||||
accessMode: normalizedAccessMode,
|
||||
password: resolvedPassword,
|
||||
});
|
||||
existing.accessMode = updated.accessMode;
|
||||
existing.id = updated.id;
|
||||
existing.publicUrl = updated.publicUrl;
|
||||
}
|
||||
if (page.pageType === 'html' && mindSpacePublications.refreshOnlinePublicationHtml) {
|
||||
return (
|
||||
(await mindSpacePublications.refreshOnlinePublicationHtml(userId, page.id)) ?? existing
|
||||
);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
const preferredSlug = urlSlug || slugFromPageTitle(page.title, page.id);
|
||||
return mindSpacePublications.publish(userId, page.id, {
|
||||
pageVersionId: page.currentVersionId,
|
||||
accessMode: normalizedAccessMode,
|
||||
password: resolvedPassword,
|
||||
urlSlug: preferredSlug,
|
||||
autoAcknowledgeFindings: true,
|
||||
});
|
||||
}
|
||||
|
||||
function buildWorkspacePublicUrl(userId, relativePath) {
|
||||
const publicBaseUrl = resolvePublicBaseUrl();
|
||||
return createMindSpacePublicUrl({
|
||||
publicBaseUrl,
|
||||
ownerKey: userId,
|
||||
relativePath,
|
||||
});
|
||||
}
|
||||
|
||||
export async function bindWorkspaceHtmlForPageData({
|
||||
pool,
|
||||
h5Root,
|
||||
storageRoot,
|
||||
userId,
|
||||
workspaceRoot,
|
||||
relativePath,
|
||||
title = null,
|
||||
accessMode = 'password',
|
||||
password = null,
|
||||
pageDataPolicy = null,
|
||||
urlSlug = null,
|
||||
publicPageLimit,
|
||||
}) {
|
||||
if (!pool) throw new Error('数据库未配置,无法绑定页面');
|
||||
if (!userId) throw new Error('缺少 userId');
|
||||
if (!workspaceRoot) throw new Error('缺少 workspaceRoot');
|
||||
const normalized = normalizePublicHtmlRelativePath(relativePath);
|
||||
const absoluteHtmlPath = path.join(workspaceRoot, ...normalized.split('/'));
|
||||
const content = await fs.readFile(absoluteHtmlPath, 'utf8');
|
||||
const normalizedAccessMode = resolvePageDataBindAccess(accessMode);
|
||||
const resolvedPassword = resolvePageDataBindPassword(normalizedAccessMode, password);
|
||||
|
||||
const mindSpacePages = createPageService(pool, { h5Root, storageRoot });
|
||||
const mindSpacePublications = createPublicationService(pool, {
|
||||
h5Root,
|
||||
storageRoot,
|
||||
publicPageLimit,
|
||||
});
|
||||
|
||||
const page = await ensureWorkspaceHtmlPage({
|
||||
userId,
|
||||
relativePath: normalized,
|
||||
content,
|
||||
title,
|
||||
mindSpacePages,
|
||||
});
|
||||
|
||||
const publication = await ensureWorkspaceHtmlPublished({
|
||||
userId,
|
||||
page,
|
||||
mindSpacePublications,
|
||||
accessMode: normalizedAccessMode,
|
||||
password: resolvedPassword,
|
||||
urlSlug,
|
||||
});
|
||||
|
||||
let policy = null;
|
||||
const htmlDatasetUsage = detectPageDataDatasetUsageFromHtml(content);
|
||||
let resolvedPageDataPolicy = pageDataPolicy;
|
||||
|
||||
if (htmlDatasetUsage.size) {
|
||||
if (pageDataPolicy?.datasets) {
|
||||
assertPolicyMatchesHtmlDatasets(content, pageDataPolicy.datasets);
|
||||
} else {
|
||||
const userDataSpace = createUserDataSpaceService({ workspaceRoot });
|
||||
const autoDatasets = buildPageDataPolicyDatasetsFromRegistry({
|
||||
html: content,
|
||||
registryDatasets: userDataSpace.listDatasets(),
|
||||
usage: htmlDatasetUsage,
|
||||
});
|
||||
resolvedPageDataPolicy = {
|
||||
ownerUserId: userId,
|
||||
accessMode: normalizedAccessMode,
|
||||
datasets: autoDatasets,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedPageDataPolicy) {
|
||||
const ownerUserId = String(resolvedPageDataPolicy.ownerUserId ?? userId).trim();
|
||||
policy = normalizePageAccessPolicy(
|
||||
{
|
||||
pageId: page.id,
|
||||
ownerUserId,
|
||||
accessMode: resolvedPageDataPolicy.accessMode ?? normalizedAccessMode,
|
||||
datasets: resolvedPageDataPolicy.datasets,
|
||||
defaultVisitorRole: resolvedPageDataPolicy.defaultVisitorRole,
|
||||
visitors: resolvedPageDataPolicy.visitors,
|
||||
roles: resolvedPageDataPolicy.roles,
|
||||
},
|
||||
{ fallbackPageId: page.id, fallbackOwnerUserId: ownerUserId },
|
||||
);
|
||||
policy = writePageAccessPolicy(workspaceRoot, policy);
|
||||
await upsertPageDataPolicyIndex(pool, policy).catch(() => null);
|
||||
policy =
|
||||
syncPageDataPolicyAccessMode(workspaceRoot, page.id, publication.accessMode, ownerUserId) ??
|
||||
policy;
|
||||
}
|
||||
|
||||
return {
|
||||
pageId: page.id,
|
||||
pageTitle: page.title,
|
||||
relativePath: normalized,
|
||||
publicationId: publication.id,
|
||||
publicationAccessMode: publication.accessMode,
|
||||
publicationPasswordApplied: normalizedAccessMode === 'password',
|
||||
publicationUrl: publication.publicUrl,
|
||||
workspaceUrl: buildWorkspacePublicUrl(userId, normalized),
|
||||
policy,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user