← back to Lifestyle Asset Intel
server.js
106 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 apiRoutes = require('./routes/api');
const consoleRoutes = require('./routes/console');
const app = express();
const PORT = parseInt(process.env.PORT || '9820', 10);
const PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${PORT}`;
const IS_PROD = process.env.NODE_ENV === 'production';
const HTTPS_PUBLIC = PUBLIC_URL.startsWith('https://');
// TODO (v0.1): once a brand domain is chosen, set CONTACT_EMAIL=info@<domain>
// in .env and provision the mailbox at Purelymail per Steve's standing rule.
const CONTACT_EMAIL = process.env.CONTACT_EMAIL || '';
const BRAND_DOMAIN = process.env.BRAND_DOMAIN || '';
app.set('trust proxy', 1);
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
baseUri: ["'self'"]
}
},
hsts: HTTPS_PUBLIC ? { maxAge: 63072000, includeSubDomains: true, preload: true } : false,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
crossOriginEmbedderPolicy: false
}));
app.use(compression());
const morganFormat = IS_PROD
? ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'
: ':method :url :status :response-time ms - :res[content-length]';
app.use(morgan(morganFormat));
app.use(express.json({ limit: '512kb' }));
app.use(express.urlencoded({ extended: true, limit: '512kb' }));
// Hard 404 guard for editor/snapshot backups so that even if a stray
// `.bak`, `.bak.*`, `.pre-*`, `.orig`, `.rej`, `.swp`, or `*~` file
// lands under public/ it can never be served. Runs BEFORE express.static.
app.use((req, res, next) => {
if (/\.(bak|orig|rej|swp)(\.|$)|\.pre-|~$/i.test(req.path)) {
return res.status(404).json({ error: 'not_found' });
}
next();
});
app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h' }));
app.use((req, res, next) => {
res.locals.publicUrl = PUBLIC_URL;
res.locals.path = req.path;
res.locals.contactEmail = CONTACT_EMAIL;
res.locals.brandDomain = BRAND_DOMAIN;
next();
});
app.use('/api', apiRoutes);
app.use('/', consoleRoutes);
app.use((req, res) => {
res.status(404);
if (req.accepts('html')) {
return res.render('error', { title: 'Not Found', message: `No route for ${req.path}`, path: req.path });
}
res.json({ error: 'not_found', path: req.path });
});
app.use((err, req, res, next) => {
console.error('[error]', err);
res.status(err.status || 500);
if (req.accepts('html')) {
return res.render('error', {
title: 'Something went wrong',
message: IS_PROD ? 'Internal error' : err.message,
path: req.path
});
}
res.json({ error: err.message || 'internal_error' });
});
if (require.main === module) {
app.listen(PORT, () => {
console.log(`[lai] listening on :${PORT} (${process.env.NODE_ENV || 'development'})`);
});
}
module.exports = app;