The 1h max-age set in Phase A caused a cache-vs-deploy mismatch when Phase B refactored config-manager.js to lazy-load admin-overview.js et al. The new index.html no longer eager-loads those scripts, but browsers with the cached (pre-Phase-B) config-manager.js didn't do the lazy-load either — so AdminOverview / AdminSystem / etc. were undefined and the admin tools rendered 'failed to load' errors. 60s is the right balance: rapid in-session clicks skip the network round-trip, but a deploy is visible within a minute. ETag-based 304s still keep the per-request cost tiny when nothing changed. Signed-off-by: librelad <librelad@digitalangels.vip>
80 lines
3.2 KiB
JavaScript
Executable File
80 lines
3.2 KiB
JavaScript
Executable File
const express = require('express');
|
|
const path = require('path');
|
|
const cookieParser = require('cookie-parser');
|
|
const config = require('./config.js');
|
|
const { verifyToken } = require('./auth.js');
|
|
|
|
// compression is a new dependency (added to package.json). The Docker image
|
|
// bakes node_modules at build time and routes/utils/server.js are bind-mounted
|
|
// in compose.yml — but node_modules is NOT bind-mounted, so a "quick" deploy
|
|
// (cp + restart) hits the old image without compression installed. We require
|
|
// it defensively: present after the next image rebuild → ~70 % wire-size
|
|
// reduction; absent → degrade silently to the previous uncompressed behaviour.
|
|
let compression = null;
|
|
try { compression = require('compression'); } catch (_) {}
|
|
|
|
function requireAuth(req, res, next) {
|
|
const token = req.cookies?.libreportal_token;
|
|
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
|
const payload = verifyToken(token);
|
|
if (!payload) return res.status(401).json({ error: 'Token expired or invalid' });
|
|
req.user = payload;
|
|
next();
|
|
}
|
|
|
|
// Prevent the browser from caching authenticated /data/* responses so they're
|
|
// not retained after logout or persisted to disk caches.
|
|
function noStore(req, res, next) {
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
next();
|
|
}
|
|
|
|
// Static-asset options:
|
|
// - 60s maxAge + ETag on JS/CSS/icons. Long enough that rapid in-session
|
|
// clicks skip the network round-trip, short enough that a deploy is
|
|
// visible within a minute. Originally tried 1h but that caused stale
|
|
// cached JS to reference things the new HTML no longer loaded (the
|
|
// Phase-B lazy-load refactor changed who loads which script).
|
|
// - HTML files get Cache-Control: no-cache (always revalidates via ETag,
|
|
// so new deploys land immediately — the SPA shell changes most often).
|
|
// - dotfiles='ignore' so .auth.json is never served.
|
|
const staticOptions = {
|
|
maxAge: '60s',
|
|
etag: true,
|
|
dotfiles: 'ignore',
|
|
setHeaders: (res, filePath) => {
|
|
if (filePath.endsWith('.html')) {
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
}
|
|
}
|
|
};
|
|
|
|
function setup(app) {
|
|
// Gzip-compress responses. JS/CSS/HTML/JSON typically shrink ~70 %, so the
|
|
// 1.7 MB of static assets the SPA loads on a cold cache drop to ~500 KB on
|
|
// the wire. compression defaults skip already-compressed types (images,
|
|
// gzipped tarballs) and small responses (<1 KB). Defensive — no-op if the
|
|
// module isn't installed (image not yet rebuilt with the new dep).
|
|
if (compression) app.use(compression());
|
|
|
|
app.use(express.json());
|
|
app.use(cookieParser());
|
|
|
|
// Block MIME sniffing on every response.
|
|
app.use((req, res, next) => {
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
next();
|
|
});
|
|
|
|
// /data/* requires auth. express.static doesn't generate directory listings,
|
|
// so the only way to read anything is to know an exact path. noStore wins
|
|
// over staticOptions' maxAge for this prefix — auth-sensitive content
|
|
// should never be cached.
|
|
app.use('/data', requireAuth, noStore, express.static(path.join(config.FRONTEND_PATH, 'data')));
|
|
|
|
// All other static assets (js, css, icons, html partials, index.html) remain public.
|
|
app.use(express.static(config.FRONTEND_PATH, staticOptions));
|
|
}
|
|
|
|
module.exports = { setup, requireAuth };
|