mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
common/ had become the backend's library: 30 of its 41 modules were imported by nothing in the frontend, so the name no longer said what belonged there. It now holds only what the frontend or the build imports too; backend-only code moved to server/, flat except for datasets/ (the dataset engine and its readers). The *-db/ dataset directories stay in common/: self-hosting docs and Docker volumes mount those paths. frontend/utils/ keeps its app-wide modules at the root and gains ip/ (IP / ASN shaping and validation), report/ (the report pipeline) and features/ (the pure layer of a single feature). Paths only — no logic changes. The Dockerfile copies server/, and the AGENTS.md files say where a new module goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
4.5 KiB
JavaScript
92 lines
4.5 KiB
JavaScript
// frontend-server.js
|
|
import dotenv from 'dotenv';
|
|
import express from 'express';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
import logger from './server/logger.js';
|
|
|
|
dotenv.config({ quiet: true });
|
|
|
|
const frontendApp = express();
|
|
const backEndPort = parseInt(process.env.BACKEND_PORT || 11966, 10);
|
|
const frontEndPort = parseInt(process.env.FRONTEND_PORT || 18966, 10);
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// API Proxy to backend server
|
|
frontendApp.use('/api', createProxyMiddleware({
|
|
target: `http://localhost:${backEndPort}/api`,
|
|
changeOrigin: true
|
|
}));
|
|
|
|
// Set static file directory.
|
|
// Cache-Control is set per-asset class so the static layer behaves well
|
|
// even when no CDN sits in front of it (CF in production sets its own
|
|
// Browser TTL on top of these, so the longer values are upper bounds):
|
|
// - dist/assets/** Vite-hashed JS/CSS/images — content-addressed → 1y immutable
|
|
// - dist/fonts/** non-hashed but essentially never change → 1y immutable
|
|
// - favicons/** connectivity-target icons → 30d; not hashed,
|
|
// but a changed icon ships under a new member id
|
|
// (= new filename), so long caching is safe
|
|
// - non-hashed images favicon / logos / achievements / … → 7d
|
|
// (any depth; not content-hashed, so changing one
|
|
// of these images requires renaming the file)
|
|
// - HTML + manifest + 24h at the edge (s-maxage), zero in browsers — the
|
|
// SEO surfaces build's postbuild purge evicts them on deploy, so
|
|
// the TTL only caps drift between deploys and no
|
|
// browser copy outlives a purge; manifest references
|
|
// only stable /logos/* paths, so a stale copy never
|
|
// points at dead assets
|
|
// - everything else (ads.txt, …) → 1h
|
|
const distDir = path.join(__dirname, './dist');
|
|
|
|
// Root-level SEO / AI-engine surfaces
|
|
const SEO_SURFACES = new Set(['llms.txt', 'llms-full.txt', 'sitemap.xml', 'robots.txt']);
|
|
|
|
function setStaticHeaders(res, filePath) {
|
|
const rel = path.relative(distDir, filePath).replaceAll(path.sep, '/');
|
|
|
|
if (rel.startsWith('assets/') || rel.startsWith('fonts/')) {
|
|
res.setHeader('Cache-Control', `public, max-age=${24 * 60 * 60 * 365}, immutable`);
|
|
} else if (rel.startsWith('favicons/')) {
|
|
res.setHeader('Cache-Control', `public, max-age=${30 * 24 * 60 * 60}`);
|
|
} else if (/\.(png|jpg|jpeg|webp|svg|ico)$/i.test(rel)) {
|
|
res.setHeader('Cache-Control', `public, max-age=${7 * 24 * 60 * 60}`);
|
|
} else if (rel.endsWith('.html') || rel === 'manifest.webmanifest' || SEO_SURFACES.has(rel)) {
|
|
res.setHeader('Cache-Control', `public, max-age=0, s-maxage=${24 * 60 * 60}, must-revalidate`);
|
|
} else {
|
|
res.setHeader('Cache-Control', `public, max-age=${60 * 60}`);
|
|
}
|
|
}
|
|
|
|
frontendApp.use(express.static(distDir, { setHeaders: setStaticHeaders }));
|
|
|
|
// SPA history fallback. The app uses HTML5 history routing (e.g. /tools/whois),
|
|
// so a hard load / new-tab of a client route reaches this server as a real
|
|
// path with no matching file. Serve index.html for navigation requests and let
|
|
// vue-router resolve the route. Mounted after the /api proxy and the static
|
|
// layer, so real assets and API calls are untouched.
|
|
// - GET only; other methods fall through.
|
|
// - `req.accepts('html')` gates on navigations: browsers asking for a page
|
|
// send `Accept: text/html`.
|
|
// - A path whose final segment has a file extension (e.g. /assets/x.js) is an
|
|
// asset request, not a client route — let it 404 rather than return an HTML
|
|
// body, which would break a missing JS/CSS chunk's importer after a deploy
|
|
// (`*/*` requests otherwise match `accepts('html')`).
|
|
// - Client-route loads stay uncached (unlike / and /index.html on the
|
|
// static layer): only those two URLs live at the edge, so a deploy-time
|
|
// purge stays a two-URL operation.
|
|
frontendApp.use((req, res, next) => {
|
|
if (req.method !== 'GET' || !req.accepts('html')) return next();
|
|
if (req.path.split('/').pop().includes('.')) return next();
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
res.sendFile(path.join(distDir, 'index.html'));
|
|
});
|
|
|
|
// Start static file server
|
|
frontendApp.listen(frontEndPort, () => {
|
|
logger.info(`🚀 Static file server ready on http://localhost:${frontEndPort}`);
|
|
});
|