import {
isPublicWorkspaceHtmlRelativePath,
} from '../mindspace-runtime-config.mjs';
import { isPlazaEmbedRequest } from '../plaza-embed.mjs';
import {
rasterizeThumbnailSvgToPng,
} from '../mindspace-thumbnail-png.mjs';
function escapePublicHtml(value) {
return String(value ?? '')
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
export function renderPublicationPasswordGate(
action,
) {
return `
受保护页面
`;
}
export function renderPublicHomepage(data) {
const cards = data.pages
.map(
(page) => `\
${escapePublicHtml(page.templateId)}
${Number(page.viewCount)} 次浏览
${escapePublicHtml(page.title)}
${escapePublicHtml(page.summary || '暂无摘要')}
`,
)
.join('');
return `
${escapePublicHtml(data.owner.displayName)} · MindSpace
MindSpace Public
${escapePublicHtml(data.owner.displayName)}
这里展示 ${escapePublicHtml(data.owner.displayName)} 当前公开发布且在线的 MindSpace 页面。
${Number(data.pageCount)} 个公开页面
${Number(data.totalViews)} 次累计浏览
/u/${escapePublicHtml(data.owner.slug)}
${
data.pages.length
? ``
: '这个主页还没有公开页面。稍后再来看看,或者直接访问作者分享给你的专属链接。'
}
`;
}
export function attachPortalPublicationRoutes({
app,
urlencodedBody,
userAuthReady = Promise.resolve(),
h5Root,
getAuthPool = () => null,
getMindSpacePages = () => null,
getMindSpacePublications = () => null,
getWorkspacePublicationDelivery = () =>
null,
getUserAuth = () => null,
sendPublishedPage,
isEmbedRequest = isPlazaEmbedRequest,
isPublicWorkspaceHtml =
isPublicWorkspaceHtmlRelativePath,
rasterizeThumbnail =
rasterizeThumbnailSvgToPng,
} = {}) {
if (
!app ||
typeof urlencodedBody !== 'function' ||
typeof sendPublishedPage !== 'function'
) {
throw new Error(
'attachPortalPublicationRoutes requires route dependencies',
);
}
async function resolvePublishedRoute(
req,
res,
password = null,
) {
await userAuthReady;
const mindSpacePublications =
getMindSpacePublications();
if (!mindSpacePublications) {
return res
.status(503)
.send('MindSpace 未启用');
}
try {
const userAuth = getUserAuth();
const viewer =
req.userSession && userAuth
? await userAuth.getMe(req.userToken)
: null;
const result =
await mindSpacePublications.resolvePublic(
req.params.ownerSlug,
req.params.urlSlug,
viewer?.id,
password,
{
userAgent: req.get('user-agent'),
referrer: req.get('referer'),
},
);
const embed = isEmbedRequest(req.query);
const raw =
String(req.query.view ?? '')
.toLowerCase() === 'raw';
if (
!password &&
!embed &&
!raw &&
result.publication?.accessMode ===
'public' &&
isPublicWorkspaceHtml(
result.workspaceRelativePath,
) &&
result.workspacePublicUrl
) {
return res.redirect(
301,
result.workspacePublicUrl,
);
}
return await sendPublishedPage(
req,
res,
result,
{
embed,
raw,
ownerSlug: req.params.ownerSlug,
},
);
} catch (error) {
if (
error?.code ===
'publication_password_required'
) {
return res
.status(password ? 403 : 200)
.send(
renderPublicationPasswordGate(
req.originalUrl,
),
);
}
if (
error?.code ===
'publication_login_required'
) {
return res
.status(401)
.send(
'请先登录 TKMind 后再访问此页面',
);
}
if (
error?.code ===
'publication_not_found'
) {
return res
.status(404)
.send('页面不存在或已下线');
}
return res
.status(500)
.send('页面加载失败');
}
}
app.use(async (req, res, next) => {
const thumbnailMatch =
/^\/u\/([^/]+)\/pages\/([^/]+)\.thumbnail\.png$/.exec(
req.path,
);
if (!thumbnailMatch) return next();
const ownerSlug = thumbnailMatch[1];
const urlSlug = thumbnailMatch[2];
await userAuthReady;
const authPool = getAuthPool();
const mindSpacePages =
getMindSpacePages();
if (!authPool || !mindSpacePages) {
return res
.status(503)
.send('MindSpace 未启用');
}
try {
const [rows] = await authPool.query(
`SELECT pr.user_id, pr.page_id
FROM h5_publish_records pr
JOIN h5_users u ON u.id = pr.user_id
WHERE COALESCE(u.slug, u.username) = ?
AND pr.url_slug = ?
AND pr.status = 'online'
ORDER BY pr.published_at DESC
LIMIT 1`,
[ownerSlug, urlSlug],
);
const row = rows[0];
if (!row) {
return res
.status(404)
.send('缩略图不存在');
}
const svg =
await mindSpacePages.renderThumbnail(
row.user_id,
row.page_id,
);
res.set('Content-Type', 'image/png');
res.set(
'Cache-Control',
'public, max-age=300',
);
return res.send(
rasterizeThumbnail(svg),
);
} catch {
return res
.status(500)
.send('缩略图加载失败');
}
});
app.get(
'/u/:ownerSlug/pages/:urlSlug',
async (req, res) =>
resolvePublishedRoute(req, res),
);
app.use(
'/u/:ownerSlug/public',
async (req, res) => {
await userAuthReady;
const ownerSlug = String(
req.params.ownerSlug ?? '',
)
.trim()
.toLowerCase();
if (!ownerSlug) {
return res
.status(404)
.json({ message: '用户不存在' });
}
const deliveryService =
getWorkspacePublicationDelivery();
if (
!deliveryService ||
typeof deliveryService
.readOwnerPublicAsset !== 'function'
) {
return res
.status(503)
.send('MindSpace 未启用');
}
try {
const delivery =
await deliveryService
.readOwnerPublicAsset({
ownerSlug,
requestPath: req.path,
});
if (
delivery.action === 'forbidden'
) {
return res
.status(403)
.json({ message: '禁止访问' });
}
if (
delivery.action !== 'deliver'
) {
return res
.status(404)
.json({
message:
delivery.reason ===
'missing_owner'
? '用户不存在'
: '文件不存在',
});
}
res.type?.(
delivery.servedName ||
'application/octet-stream',
);
if (delivery.cacheControl) {
res.set(
'Cache-Control',
delivery.cacheControl,
);
}
return res.send(
Buffer.from(
delivery.bodyBase64 || '',
'base64',
),
);
} catch {
return res
.status(500)
.send('文件加载失败');
}
},
);
app.get(
'/u/:ownerSlug',
async (req, res) => {
await userAuthReady;
const mindSpacePublications =
getMindSpacePublications();
if (!mindSpacePublications) {
return res
.status(503)
.send('MindSpace 未启用');
}
try {
const data =
await mindSpacePublications
.getPublicHomepage(
req.params.ownerSlug,
);
res.set(
'Content-Type',
'text/html; charset=utf-8',
);
res.set(
'Content-Security-Policy',
"default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'",
);
res.set(
'Cache-Control',
'public, max-age=60',
);
return res.send(
renderPublicHomepage(data),
);
} catch (error) {
if (
error?.code ===
'publication_not_found'
) {
return res
.status(404)
.send('主页不存在');
}
return res
.status(500)
.send('主页加载失败');
}
},
);
app.post(
'/u/:ownerSlug/pages/:urlSlug',
urlencodedBody,
async (req, res) =>
resolvePublishedRoute(
req,
res,
req.body?.password,
),
);
app.get('/s/:token', async (req, res) => {
await userAuthReady;
const mindSpacePublications =
getMindSpacePublications();
if (!mindSpacePublications) {
return res
.status(503)
.send('MindSpace 未启用');
}
try {
const userAuth = getUserAuth();
const viewer =
req.userSession && userAuth
? await userAuth.getMe(req.userToken)
: null;
return await sendPublishedPage(
req,
res,
await mindSpacePublications
.resolvePrivateLink(
req.params.token,
viewer?.id,
{
userAgent:
req.get('user-agent'),
referrer: req.get('referer'),
},
),
{
embed: isEmbedRequest(req.query),
raw:
String(req.query.view ?? '')
.toLowerCase() === 'raw',
},
);
} catch (error) {
if (
error?.code ===
'publication_not_found'
) {
return res
.status(404)
.send('页面不存在或已下线');
}
return res
.status(500)
.send('页面加载失败');
}
});
}