Add page data delivery and publication guards
This commit is contained in:
@@ -3,7 +3,8 @@ import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { localizeGoogleFontsCss } from './mindspace-html-localize.mjs';
|
||||
import { replacePrivateResourceReferences, scanContent } from './mindspace-content-scan.mjs';
|
||||
import { pageInternals } from './mindspace-pages.mjs';
|
||||
import { pageInternals, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
|
||||
import { readWorkspacePublishHtml } from './mindspace-workspace-path.mjs';
|
||||
import { loadMindSpaceConfig } from './mindspace-config.mjs';
|
||||
import { buildPublicUrl, resolvePublicBaseUrl, resolvePublishDir } from './user-publish.mjs';
|
||||
import {
|
||||
@@ -465,6 +466,7 @@ export function createPublicationService(pool, options = {}) {
|
||||
const loadVersion = async (userId, pageId, pageVersionId) => {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT p.id AS page_id, p.title, p.summary, p.page_type, p.template_id, p.current_version_id,
|
||||
p.workspace_relative_path,
|
||||
p.source_session_id, p.source_message_id,
|
||||
p.user_id, p.space_id, pv.id AS page_version_id, pv.version_no,
|
||||
pv.bundle_asset_id, av.storage_key,
|
||||
@@ -501,6 +503,19 @@ export function createPublicationService(pool, options = {}) {
|
||||
const preparePublishContent = async (page, ownerSlug, urlSlug) => {
|
||||
let publishContent = page.content;
|
||||
if (page.page_type !== 'html') return publishContent;
|
||||
const workspaceRelativePath = normalizeWorkspaceRelativePath(
|
||||
page.workspace_relative_path ?? page.source_relative_path,
|
||||
);
|
||||
if (h5Root && workspaceRelativePath) {
|
||||
const workspaceHtml = await readWorkspacePublishHtml(
|
||||
h5Root,
|
||||
page.user_id,
|
||||
workspaceRelativePath,
|
||||
);
|
||||
if (workspaceHtml && workspaceHtml.length > String(publishContent ?? '').length) {
|
||||
publishContent = workspaceHtml;
|
||||
}
|
||||
}
|
||||
const publishDir = h5Root ? resolvePublishDir(h5Root, { id: page.user_id }) : null;
|
||||
return prepareHtmlPublishContent({
|
||||
pool,
|
||||
@@ -969,7 +984,7 @@ export function createPublicationService(pool, options = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
const updatePublicationStatus = async (userId, publicationId, { accessMode, expiresAt }) => {
|
||||
const updatePublicationAccess = async (userId, publicationId, { accessMode, password, expiresAt } = {}) => {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
@@ -981,15 +996,30 @@ export function createPublicationService(pool, options = {}) {
|
||||
const publication = rows[0];
|
||||
if (!publication) throw publicationError('发布记录不存在', 'publication_not_found');
|
||||
|
||||
const normalizedMode = normalizeAccessMode(accessMode);
|
||||
const normalizedExpiresAt = normalizeExpiresAt(expiresAt, normalizedMode === 'time_limited');
|
||||
const normalizedMode =
|
||||
accessMode != null ? normalizeAccessMode(accessMode) : publication.access_mode;
|
||||
const normalizedExpiresAt =
|
||||
expiresAt !== undefined
|
||||
? normalizeExpiresAt(expiresAt, normalizedMode === 'time_limited')
|
||||
: publication.expires_at;
|
||||
let passwordHash = null;
|
||||
if (normalizedMode === 'password') {
|
||||
const explicit = String(password ?? '').trim();
|
||||
if (explicit) {
|
||||
passwordHash = hashPassword(normalizePassword(explicit, true));
|
||||
} else if (publication.password_hash) {
|
||||
passwordHash = publication.password_hash;
|
||||
} else {
|
||||
throw publicationError('访问密码长度必须为 8 到 128 个字符', 'invalid_publish_input');
|
||||
}
|
||||
}
|
||||
const now = Date.now();
|
||||
|
||||
await conn.query(
|
||||
`UPDATE h5_publish_records
|
||||
SET access_mode = ?, expires_at = ?, user_confirmed_at = ?, updated_at = ?
|
||||
SET access_mode = ?, expires_at = ?, password_hash = ?, user_confirmed_at = ?, updated_at = ?
|
||||
WHERE id = ? AND user_id = ?`,
|
||||
[normalizedMode, normalizedExpiresAt, now, now, publicationId, userId],
|
||||
[normalizedMode, normalizedExpiresAt, passwordHash, now, now, publicationId, userId],
|
||||
);
|
||||
await conn.commit();
|
||||
|
||||
@@ -997,6 +1027,7 @@ export function createPublicationService(pool, options = {}) {
|
||||
...publication,
|
||||
access_mode: normalizedMode,
|
||||
expires_at: normalizedExpiresAt,
|
||||
password_hash: passwordHash,
|
||||
user_confirmed_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
@@ -1009,6 +1040,10 @@ export function createPublicationService(pool, options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const updatePublicationStatus = async (userId, publicationId, { accessMode, expiresAt }) => {
|
||||
return updatePublicationAccess(userId, publicationId, { accessMode, expiresAt });
|
||||
};
|
||||
|
||||
const offline = async (userId, publicationId) => {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
@@ -1097,7 +1132,7 @@ export function createPublicationService(pool, options = {}) {
|
||||
now,
|
||||
],
|
||||
);
|
||||
const html = await fs.readFile(await resolveReadableStoragePath(row.storage_key), 'utf8');
|
||||
const html = await readPublicationHtmlWithWorkspaceFallback(row);
|
||||
return {
|
||||
html: await refreshPublishedHtmlDownloadLinks(row, html),
|
||||
publication: publicationResponse({ ...row, view_count: Number(row.view_count) + 1 }),
|
||||
@@ -1111,6 +1146,24 @@ export function createPublicationService(pool, options = {}) {
|
||||
};
|
||||
};
|
||||
|
||||
// REGRESSION GUARD: mindspace-page-sync-thumbnail — publication delivery must fall back to
|
||||
// workspace HTML when publication storage snapshot is missing (isolated env / migration).
|
||||
async function readPublicationHtmlWithWorkspaceFallback(row) {
|
||||
try {
|
||||
return await fs.readFile(await resolveReadableStoragePath(row.storage_key), 'utf8');
|
||||
} catch (error) {
|
||||
const storageMissing = error?.code === 'ENOENT' || error?.code === 'storage_not_found';
|
||||
if (!storageMissing || !h5Root) throw error;
|
||||
const workspaceRelativePath = normalizeWorkspaceRelativePath(
|
||||
row.workspace_relative_path ?? row.source_relative_path,
|
||||
);
|
||||
if (!workspaceRelativePath) throw error;
|
||||
const workspaceHtml = await readWorkspacePublishHtml(h5Root, row.owner_id, workspaceRelativePath);
|
||||
if (!workspaceHtml) throw error;
|
||||
return workspaceHtml;
|
||||
}
|
||||
}
|
||||
|
||||
const refreshPublishedHtmlDownloadLinks = async (row, html) => {
|
||||
if (!h5Root || !row?.owner_id) return html;
|
||||
const source = String(html ?? '');
|
||||
@@ -1133,7 +1186,10 @@ export function createPublicationService(pool, options = {}) {
|
||||
|
||||
const resolvePublic = async (ownerSlug, urlSlug, viewerId, password, requestMeta) => {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id, av.storage_key
|
||||
`SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id,
|
||||
p.workspace_relative_path,
|
||||
av.storage_key,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path
|
||||
FROM h5_publish_records pr
|
||||
JOIN h5_users u ON u.id = pr.user_id
|
||||
JOIN h5_page_records p ON p.id = pr.page_id
|
||||
@@ -1150,7 +1206,10 @@ export function createPublicationService(pool, options = {}) {
|
||||
const resolvePrivateLink = async (token, viewerId, requestMeta) => {
|
||||
const tokenHash = crypto.createHash('sha256').update(String(token)).digest('hex');
|
||||
const [rows] = await pool.query(
|
||||
`SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id, av.storage_key
|
||||
`SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id,
|
||||
p.workspace_relative_path,
|
||||
av.storage_key,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path
|
||||
FROM h5_publish_records pr
|
||||
JOIN h5_users u ON u.id = pr.user_id
|
||||
JOIN h5_page_records p ON p.id = pr.page_id
|
||||
@@ -1251,6 +1310,7 @@ export function createPublicationService(pool, options = {}) {
|
||||
getPublicHomepage,
|
||||
getStats,
|
||||
offline,
|
||||
updatePublicationAccess,
|
||||
updatePublicationStatus,
|
||||
resolvePublic,
|
||||
resolvePrivateLink,
|
||||
|
||||
Reference in New Issue
Block a user