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', ), ); }); }