Files
memind/server/portal-static-delivery-routes.mjs
T

310 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs';
import path from 'node:path';
import express from 'express';
import {
extractSharePreviewMeta,
injectOgTags,
renderWechatSharePreviewHtml,
} from '../mindspace-og-tags.mjs';
export function attachPortalStaticDeliveryRoutes({
app,
h5Root,
host,
port,
publishRootDir,
usersRoot,
userAuthReady = Promise.resolve(),
resolveRequestOrigin,
fetchFn = fetch,
fsApi = fs,
pathApi = path,
staticMiddleware = express.static,
injectOgTagsFn = injectOgTags,
extractSharePreviewMetaFn =
extractSharePreviewMeta,
renderWechatSharePreviewHtmlFn =
renderWechatSharePreviewHtml,
} = {}) {
if (
!app ||
!h5Root ||
!publishRootDir ||
!usersRoot ||
typeof resolveRequestOrigin !== 'function' ||
typeof fetchFn !== 'function' ||
typeof staticMiddleware !== 'function'
) {
throw new Error(
'attachPortalStaticDeliveryRoutes requires route dependencies',
);
}
app.use('/temp', (req, res) => {
res.redirect(
301,
`/${publishRootDir}${req.url}`,
);
});
app.use('/user', async (req, res, next) => {
await userAuthReady;
const parts = req.path
.split('/')
.filter(Boolean);
if (parts.length < 1) return next();
const [username, ...rest] = parts;
const targetDir = pathApi.join(
usersRoot,
username,
);
const filePath = pathApi.join(
targetDir,
...rest,
);
const resolvedRoot =
pathApi.resolve(targetDir);
const resolvedPath =
pathApi.resolve(filePath);
if (
!resolvedPath.startsWith(
resolvedRoot + pathApi.sep,
) &&
resolvedPath !== resolvedRoot
) {
return res
.status(403)
.json({ message: '禁止访问' });
}
if (!fsApi.existsSync(targetDir)) {
return res
.status(404)
.json({ message: '用户不存在' });
}
if (!fsApi.existsSync(resolvedPath)) {
return res
.status(404)
.json({ message: '文件不存在' });
}
if (fsApi.statSync(resolvedPath).isDirectory()) {
const indexPath = pathApi.join(
resolvedPath,
'index.html',
);
if (fsApi.existsSync(indexPath)) {
return res.sendFile(indexPath);
}
return res
.status(404)
.json({
message: '目录中没有 index.html',
});
}
res.sendFile(resolvedPath, (error) => {
if (error) {
res
.status(404)
.json({ message: '文件不存在' });
}
});
});
app.get(
`/${publishRootDir}/wiki/*`,
(_req, res) => {
res.sendFile(
pathApi.join(
h5Root,
publishRootDir,
'wiki',
'index.html',
),
);
},
);
app.get('/temp/wiki/*', (req, res) => {
res.redirect(
301,
`/${publishRootDir}/wiki${req.url.slice(
'/temp/wiki'.length,
)}`,
);
});
app.use(
'/plaza-covers',
staticMiddleware(
pathApi.join(
h5Root,
'public/plaza-covers',
),
{ maxAge: '7d' },
),
);
app.use(
'/brand',
staticMiddleware(
pathApi.join(h5Root, 'public/brand'),
{ maxAge: '7d' },
),
);
app.use(
'/assets',
staticMiddleware(
pathApi.join(h5Root, 'public/assets'),
{ maxAge: '1d' },
),
);
app.get(
'/dev/wechat-share-preview',
async (req, res) => {
try {
const rawTarget = String(
req.query.url ?? req.query.path ?? '',
).trim();
if (!rawTarget) {
return res
.status(400)
.type('text/plain; charset=utf-8')
.send(
'缺少 url 参数,例如 ?url=/MindSpace/john/public/demo.html',
);
}
const origin =
resolveRequestOrigin(req) ||
`http://${host}:${port}`;
const targetUrl = rawTarget.startsWith(
'http',
)
? new URL(rawTarget)
: new URL(
rawTarget.startsWith('/')
? rawTarget
: `/${rawTarget}`,
origin,
);
const upstream = await fetchFn(
targetUrl.toString(),
{
headers: {
'user-agent':
req.get('user-agent') ||
'tkmind-wechat-share-preview',
accept: 'text/html,*/*',
},
redirect: 'follow',
},
);
if (!upstream.ok) {
return res
.status(upstream.status)
.type('text/plain; charset=utf-8')
.send(
`页面请求失败:HTTP ${upstream.status}`,
);
}
let html = await upstream.text();
const pageUrl =
targetUrl.toString().split('#')[0];
const pageDirUrl = pageUrl.slice(
0,
pageUrl.lastIndexOf('/') + 1,
);
html = injectOgTagsFn(html, {
origin: targetUrl.origin,
pageUrl,
pageDirUrl,
});
const preview =
extractSharePreviewMetaFn(html, {
origin: targetUrl.origin,
pageUrl,
pageDirUrl,
});
res.set(
'Content-Type',
'text/html; charset=utf-8',
);
res.set('Cache-Control', 'no-store');
return res.send(
renderWechatSharePreviewHtmlFn(
preview,
{
note: '此预览读取页面最终 HTML 中的 Open Graph 标签,可用来对照微信里粘贴链接后的卡片效果。',
},
),
);
} catch (error) {
return res
.status(500)
.type('text/plain; charset=utf-8')
.send(
String(error?.message ?? error),
);
}
},
);
app.use(
'/dev',
staticMiddleware(
pathApi.join(h5Root, 'public/dev'),
{ maxAge: 0 },
),
);
app.get(
/^\/MP_verify_[A-Za-z0-9]+\.txt$/,
(req, res) => {
const fileName = pathApi.basename(req.path);
const filePath = pathApi.join(
h5Root,
'public',
fileName,
);
if (!fsApi.existsSync(filePath)) {
return res.status(404).end();
}
res
.type('text/plain')
.sendFile(filePath);
},
);
app.use('/auth', (_req, res) => {
res.status(404).json({
message:
'接口不存在,请重启后端服务(node server.mjs 或 pnpm dev',
});
});
app.use('/admin-api', (_req, res) => {
res.status(404).json({
message:
'接口不存在,请重启后端服务(node server.mjs 或 pnpm dev',
});
});
app.use(
staticMiddleware(
pathApi.join(h5Root, 'dist'),
{ index: 'index.html' },
),
);
app.get('*', (_req, res) => {
res.sendFile(
pathApi.join(
h5Root,
'dist',
'index.html',
),
);
});
}