← back to Stars of Design
server.js
116 lines
require('dotenv').config();
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const cookieParser = require('cookie-parser');
const publicRoutes = require('./routes/public');
const app = express();
const PORT = parseInt(process.env.PORT || '9928', 10);
const PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${PORT}`;
const IS_PROD = process.env.NODE_ENV === 'production';
app.set('trust proxy', 1);
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.disable('x-powered-by');
// gzip compression for all responses ≥ 1KB. Mirror asim — JSON-LD-heavy
// HTML pages drop significantly over the wire.
app.use(compression({ threshold: 1024 }));
// Security headers (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy)
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
// http://localhost:9935 is the Big Red widget host (lower-right avatar
// launcher on every page); its iframe + JS + fetch traffic must be
// allowed here. Drop the localhost:9935 entries when the named
// tunnel bigred.agentabrams.com goes live.
scriptSrc: ["'self'", "'unsafe-inline'", 'http://localhost:9935', 'https://www.googletagmanager.com', 'https://www.google-analytics.com'],
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
imgSrc: ["'self'", 'data:', 'https:', 'http://localhost:9935'],
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'data:'],
connectSrc: ["'self'", 'http://localhost:9935', 'ws://localhost:9935', 'https://www.google-analytics.com', 'https://*.analytics.google.com', 'https://*.googletagmanager.com'],
frameSrc: ["'self'", 'http://localhost:9935'],
frameAncestors: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
strictTransportSecurity: { maxAge: 31536000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
crossOriginEmbedderPolicy: false,
}));
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=(), payment=()');
next();
});
// Rate limit — 200 req/min/IP across all routes, defense in depth
app.use(rateLimit({
windowMs: 60_000, max: 200, standardHeaders: 'draft-7', legacyHeaders: false,
message: 'Too many requests, please slow down.',
}));
app.use(morgan(IS_PROD ? 'combined' : 'dev'));
app.use(express.json({ limit: '256kb' }));
app.use(express.urlencoded({ extended: true, limit: '256kb' }));
app.use(cookieParser());
// Static-bak guard — refuse to serve any URL path that looks like a snapshot
// or rollback artifact (foo.html.bak, foo.pre-refactor.js, etc.) even if
// someone accidentally leaves one in public/. Pair with the .gitignore rules
// for *.bak / *.bak.* / *.pre-*. Returns plain-text 404 (not 403) so an
// attacker can't confirm a snapshot exists by path-probing, and bypasses
// the EJS 404 template since the meta-locals it expects aren't wired yet
// at this point in the middleware stack.
app.use((req, res, next) => {
if (/\.bak(?:\.|$)|\.pre-/.test(req.path)) {
res.setHeader('Cache-Control', 'no-store');
return res.status(404).type('text/plain').send('Not found');
}
next();
});
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: '1h',
setHeaders(res, p) {
if (p.endsWith('.html')) res.setHeader('Cache-Control', 'no-store, must-revalidate');
},
}));
app.use((req, res, next) => {
res.locals.GA_ID = process.env.GA_ID || 'G-YCT0F2Q692';
res.locals.PUBLIC_URL = PUBLIC_URL;
next();
});
app.use('/', publicRoutes);
app.use((req, res) => {
res.status(404).render('public/404', { title: 'Not found — Stars of Design' });
});
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
console.error('[error]', err && err.stack ? err.stack : err);
// Always show generic message in prod — never leak internals
res.status(500).render('public/error', {
title: 'Server error — Stars of Design',
message: 'Something went wrong.',
});
});
app.listen(PORT, () => {
console.log(`starsofdesign listening on :${PORT} (${IS_PROD ? 'prod' : 'dev'})`);
});