310 lines
6.9 KiB
JavaScript
310 lines
6.9 KiB
JavaScript
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',
|
||
),
|
||
);
|
||
});
|
||
}
|