diff --git a/src/app.js b/src/app.js index b9a9c12..072987b 100644 --- a/src/app.js +++ b/src/app.js @@ -1,40 +1,12 @@ // src/app.js -const express = require("express"); -const exphbs = require("express-handlebars"); + require("dotenv").config(); const setupMiddleware = require("./middleware"); -const { registerHelpers } = require("./utils/hbsHelpers"); const { manualLogger } = require("./utils/logging"); -const app = express(); // const path = require("path"); -const hbs = exphbs.create({ - layoutsDir: "src/views/layouts", - partialsDir: "src/views/partials", - defaultLayout: "main", - helpers: { - section: function (name, options) { - if (!this._sections) this._sections = {}; - this._sections[name] = options.fn(this); - return null; - }, - }, - defaultLayout: "main", - extname: ".handlebars", - runtimeOptions: { - allowProtoPropertiesByDefault: true, - allowProtoMethodsByDefault: true, - }, -}); - -registerHelpers(hbs); - -app.engine("handlebars", hbs.engine); -app.set("view engine", "handlebars"); -app.set("views", "./src/views"); - -app.use(setupMiddleware()); +app = setupMiddleware(); port = process.env.PORT || 3400; diff --git a/src/middleware/applyProductionSecurity.js b/src/middleware/applyProductionSecurity.js index c85b059..2f53ac0 100644 --- a/src/middleware/applyProductionSecurity.js +++ b/src/middleware/applyProductionSecurity.js @@ -1,10 +1,8 @@ -const express = require("express"); const helmet = require("helmet"); const hpp = require("hpp"); const xssSanitizer = require("./xssSanitizer"); -function setupMiddleware() { - const app = express(); +function applyProductionSecurity(app) { app.disable("x-powered-by"); app.set("trust proxy", true); @@ -52,7 +50,6 @@ }, }) ); // Sets secure HTTP headers. Prevents common attacks. - return app; } -module.exports = setupMiddleware; +module.exports = applyProductionSecurity; diff --git a/src/middleware/csrfToken.js b/src/middleware/csrfToken.js new file mode 100644 index 0000000..b500810 --- /dev/null +++ b/src/middleware/csrfToken.js @@ -0,0 +1,13 @@ +// src/csrfToken.js +const router = require("express").Router(); +const cookieParser = require("cookie-parser"); + +const csrf = require("csurf"); + +router.use(cookieParser()); +router.use(csrf({ cookie: true })); +router.use((req, res, next) => { + res.locals.csrfToken = req.csrfToken(); + next(); +}); +module.exports = router; diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js deleted file mode 100644 index dbb745c..0000000 --- a/src/middleware/errorHandler.js +++ /dev/null @@ -1,50 +0,0 @@ -const path = require("path"); -const getBaseContext = require("../utils/baseContext"); - -module.exports = async (err, req, res, next) => { - const statusCode = err.statusCode ?? 500; - const message = err.message ?? "Internal Server Error"; - - if (req?.log?.error) { - req.log.error( - JSON.stringify({ - message, - stack: err.stack || "No stack trace available", - method: req.method, - url: req.originalUrl || req.url, - statusCode, - code: err.code || null, - }) - ); - } else { - console.error(err); - } - const errorContextMap = { - EBADCSRFTOKEN: { - title: "Forbidden", - message: - "Invalid CSRF token. Your request was blocked for security reasons.", - statusCode: 403, - }, - 404: { - title: "Not Found", - message: "The requested resource was not found.", - statusCode: 404, - }, - }; - const errorKey = err.code || err.statusCode; - const errorContext = errorContextMap[errorKey] || { - title: "Error", - message, - statusCode, - }; - - const context = await getBaseContext({ - title: errorContext.title, - message: errorContext.message, - statusCode: errorContext.statusCode, - content: "", - }); - - res.status(errorContext.statusCode).render("pages/error", context); -}; diff --git a/src/middleware/index.js b/src/middleware/index.js index 3179db6..a109d9c 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -1,13 +1,14 @@ // src/setupMiddleware.js const express = require("express"); +const exphbs = require("express-handlebars"); const bodyParser = require("body-parser"); -const errorHandler = require("./errorHandler"); const compression = require("compression"); const routes = require("../routes"); const formatHtml = require("./formatHtml"); const logEvent = require("./analytics.js"); const applyProductionSecurity = require("./applyProductionSecurity"); const validateRequestIntegrity = require("./validateRequestIntegrity"); +const { registerHelpers } = require("../utils/hbsHelpers"); const { loggingMiddleware, @@ -18,39 +19,47 @@ function setupMiddleware() { const app = express(); + // Setup logging app.use(logEvent); app.use(morganInfo); app.use(morganWarn); app.use(morganError); app.use(loggingMiddleware); + // Setup view engine + + const hbs = exphbs.create({ + layoutsDir: "src/views/layouts", + partialsDir: "src/views/partials", + defaultLayout: "main", + helpers: { + section: function (name, options) { + if (!this._sections) this._sections = {}; + this._sections[name] = options.fn(this); + return null; + }, + }, + defaultLayout: "main", + extname: ".handlebars", + runtimeOptions: { + allowProtoPropertiesByDefault: true, + allowProtoMethodsByDefault: true, + }, + }); + registerHelpers(hbs); + app.engine("handlebars", hbs.engine); + app.set("view engine", "handlebars"); + app.set("views", "./src/views"); + if (process.env.NODE_ENV === "production") { - app.use(applyProductionSecurity()); + applyProductionSecurity(app); } app.use(express.json({ limit: "4kb" })); app.use(bodyParser.urlencoded({ extended: false, limit: "4kb" })); app.use(compression()); app.use(validateRequestIntegrity); - app.use( - "/static", - express.static("public", { - dotfiles: "deny", - index: false, - extensions: false, - fallthrough: false, - setHeaders: (res) => { - res.set("Cache-Control", "public, max-age=31536000, immutable"); - }, - }) - ); app.use(formatHtml); app.use(routes); - app.use((req, res, next) => { - const err = new Error("Not Found"); - err.statusCode = 404; - next(err); - }); - app.use(errorHandler); return app; } diff --git a/src/routes/errorHandler.js b/src/routes/errorHandler.js new file mode 100644 index 0000000..dbb745c --- /dev/null +++ b/src/routes/errorHandler.js @@ -0,0 +1,50 @@ +const path = require("path"); +const getBaseContext = require("../utils/baseContext"); + +module.exports = async (err, req, res, next) => { + const statusCode = err.statusCode ?? 500; + const message = err.message ?? "Internal Server Error"; + + if (req?.log?.error) { + req.log.error( + JSON.stringify({ + message, + stack: err.stack || "No stack trace available", + method: req.method, + url: req.originalUrl || req.url, + statusCode, + code: err.code || null, + }) + ); + } else { + console.error(err); + } + const errorContextMap = { + EBADCSRFTOKEN: { + title: "Forbidden", + message: + "Invalid CSRF token. Your request was blocked for security reasons.", + statusCode: 403, + }, + 404: { + title: "Not Found", + message: "The requested resource was not found.", + statusCode: 404, + }, + }; + const errorKey = err.code || err.statusCode; + const errorContext = errorContextMap[errorKey] || { + title: "Error", + message, + statusCode, + }; + + const context = await getBaseContext({ + title: errorContext.title, + message: errorContext.message, + statusCode: errorContext.statusCode, + content: "", + }); + + res.status(errorContext.statusCode).render("pages/error", context); +}; diff --git a/src/routes/index.js b/src/routes/index.js index ede4a3c..e39b258 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -2,13 +2,12 @@ const express = require("express"); const router = express.Router(); -const getBaseContext = require("../utils/baseContext"); +// const getBaseContext = require("../utils/baseContext"); const analytics = require("./analytics"); const robots = require("./robots"); const blog_index = require("./blog_index"); -const csrfToken = require("../utils/csrfToken"); - -router.post("/track", analytics); +const csrfToken = require("../middleware/csrfToken"); +const errorHandler = require("./errorHandler"); const contact = require("./contact"); const sitemap = require("./sitemap"); @@ -16,6 +15,20 @@ const pages = require("./pages"); const rssFeed = require("./rssFeed"); +router.use( + "/static", + express.static("public", { + dotfiles: "deny", + index: false, + extensions: false, + fallthrough: false, + setHeaders: (res) => { + res.set("Cache-Control", "public, max-age=31536000, immutable"); + }, + }) +); +router.post("/track", analytics); + router.use(blog_index); router.use(robots); router.use(contact, csrfToken); @@ -38,4 +51,12 @@ res.redirect(301, "/blog"); }); +router.use((req, res, next) => { + const err = new Error("Not Found"); + err.statusCode = 404; + next(err); +}); + +router.use(errorHandler); + module.exports = router; diff --git a/src/routes/pages.js b/src/routes/pages.js index ab1080b..f5c6edb 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -3,7 +3,7 @@ const router = express.Router(); const ConstructionRoutes = require("../utils/ConstructionRoutes"); const MarkdownRoutes = require("../utils/MarkdownRoutes"); -const csrfToken = require("../utils/csrfToken"); +const csrfToken = require("../middleware/csrfToken"); const construction = new ConstructionRoutes(); const markdown = new MarkdownRoutes(); diff --git a/src/utils/csrfToken.js b/src/utils/csrfToken.js deleted file mode 100644 index b500810..0000000 --- a/src/utils/csrfToken.js +++ /dev/null @@ -1,13 +0,0 @@ -// src/csrfToken.js -const router = require("express").Router(); -const cookieParser = require("cookie-parser"); - -const csrf = require("csurf"); - -router.use(cookieParser()); -router.use(csrf({ cookie: true })); -router.use((req, res, next) => { - res.locals.csrfToken = req.csrfToken(); - next(); -}); -module.exports = router;