diff --git a/content b/content index 8c6c5f4..ec739f0 160000 --- a/content +++ b/content @@ -1 +1 @@ -Subproject commit 8c6c5f4273c41f9fad4b5568841376d506a9de3c +Subproject commit ec739f0287a067b5f4defa5f5af2eca329f9e057 diff --git a/public/css/presentation.css b/public/css/presentation.css index 11fafdb..db455d1 100644 --- a/public/css/presentation.css +++ b/public/css/presentation.css @@ -1,115 +1,134 @@ - html { - font-size: 16px !important; - } - .reveal { - font-size: 1rem !important; - } +html { + font-size: 16px !important; +} +.reveal { + font-size: 1rem !important; +} - .reveal h1 { - font-size: 3.8rem !important; - line-height: 1.2 !important; - margin-bottom: 0.5em !important; - } +.reveal h1 { + font-size: 3.8rem !important; + line-height: 1.2 !important; + margin-bottom: 0.5em !important; +} - .reveal h2 { - font-size: 2.5rem !important; - line-height: 1.3 !important; - margin-bottom: 0.8em !important; - } +.reveal h2 { + font-size: 2.5rem !important; + line-height: 1.3 !important; + margin-bottom: 0.8em !important; +} - .reveal h3 { font-size: 1.8rem !important; } - .reveal h4 { font-size: 1.4rem !important; } +.reveal h3 { font-size: 1.8rem !important; } +.reveal h4 { font-size: 1.4rem !important; } - .reveal p, - .reveal ul li { - font-size: 1.15rem !important; - line-height: 1.6 !important; - margin-bottom: 0.8em !important; - text-align: left !important; - padding: 0 !important; - max-width: 900px !important; - margin-left: auto !important; - margin-right: auto !important; - } +.reveal p, +.reveal ul li { + font-size: 1.15rem !important; + line-height: 1.6 !important; + margin-bottom: 0.8em !important; + text-align: left !important; + padding: 0 !important; + max-width: 900px !important; + margin-left: auto !important; + margin-right: auto !important; +} - .slide-content-wrapper { - display: flex !important; - flex-direction: column !important; - justify-content: flex-start !important; - align-items: center !important; - width: 100% !important; - height: calc(100% - 140px) !important; - max-height: calc(100% - 140px) !important; - overflow-y: auto !important; - padding: 0 60px !important; - box-sizing: border-box !important; - } +.slide-content-wrapper { + display: flex !important; + flex-direction: column !important; + justify-content: flex-start !important; + align-items: center !important; + width: 100% !important; + height: calc(100% - 140px) !important; + max-height: calc(100% - 140px) !important; + overflow-y: auto !important; + padding: 0 60px !important; + box-sizing: border-box !important; +} - .content-block { - width: 100% !important; - max-width: 900px !important; - margin-bottom: 25px !important; - box-sizing: border-box !important; - } +.content-block { + width: 100% !important; + max-width: 900px !important; + margin-bottom: 25px !important; + box-sizing: border-box !important; +} - .content-list ul { - list-style-position: outside !important; - padding-left: 25px !important; - margin-left: 0 !important; - } - .content-list ul li { - margin-bottom: 0.5em !important; - } +.content-list ul { + list-style-position: outside !important; + padding-left: 25px !important; + margin-left: 0 !important; +} +.content-list ul li { + margin-bottom: 0.5em !important; +} - .slide-images { - display: flex !important; - flex-wrap: wrap !important; - justify-content: center !important; - align-items: center !important; - gap: 25px !important; - margin-top: 25px !important; - padding: 0 !important; - overflow: hidden !important; - } +.slide-images { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + align-items: center !important; + gap: 25px !important; + margin-top: 25px !important; + padding: 0 !important; + overflow: hidden !important; +} - .slide-images img { - width: auto !important; - height: auto !important; - max-width: 100% !important; - max-height: 100% !important; - object-fit: contain !important; - display: block !important; +.slide-images img { + width: auto !important; + height: auto !important; + max-width: 100% !important; + max-height: 100% !important; + object-fit: contain !important; + display: block !important; - flex-grow: 1 !important; - flex-shrink: 1 !important; - flex-basis: auto !important; + flex-grow: 1 !important; + flex-shrink: 1 !important; + flex-basis: auto !important; - min-width: 250px !important; - max-width: 48% !important; - max-height: 40vh !important; + min-width: 250px !important; + max-width: 48% !important; + max-height: 40vh !important; - border: 1px solid rgba(255, 255, 255, 0.2) !important; - box-shadow: 0 8px 16px rgba(0,0,0,0.4) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + box-shadow: 0 8px 16px rgba(0,0,0,0.4) !important; - cursor: zoom-in; - } + cursor: zoom-in; +} - .reveal img.reveal-lightbox-img { - cursor: pointer !important; - } +.reveal img.reveal-lightbox-img { + cursor: pointer !important; +} - .reveal section[data-state*="Showcase: Error"] .slide-images img, - .reveal section[data-state*="Showcase: Essential"] .slide-images img, - .reveal section[data-state*="Showcase: Documentation"] .slide-images img { - max-width: 31% !important; - max-height: 30vh !important; - } +.reveal section[data-state*="Showcase: Error"] .slide-images img, +.reveal section[data-state*="Showcase: Essential"] .slide-images img, +.reveal section[data-state*="Showcase: Documentation"] .slide-images img { + max-width: 31% !important; + max-height: 30vh !important; +} - .reveal .lightbox { - background: rgba(0, 0, 0, 0.9) !important; - } - .reveal .lightbox-image { - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6) !important; - max-width: 90vw !important; - max-height: 90vh !important; - } +.reveal .lightbox { + background: rgba(0, 0, 0, 0.9) !important; +} +.reveal .lightbox-image { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6) !important; + max-width: 90vw !important; + max-height: 90vh !important; +} +#returnButton { + position: absolute; + bottom: 2rem; + right: 2rem; + padding: 0.4rem 1rem; + background: transparent; + color: #fff; + border: 2px solid #888; + border-radius: 0.3rem; + font-size: 0.9rem; + font-family: inherit; + cursor: pointer; + transition: all 0.2s ease-in-out; +} + +#returnButton:hover { + background: #888; + color: #000; +} diff --git a/public/js/docs.js b/public/js/docs.js deleted file mode 100644 index 875f842..0000000 --- a/public/js/docs.js +++ /dev/null @@ -1,6 +0,0 @@ -Handlebars.registerHelper( - "isObject", - (v) => v && typeof v === "object" && !Array.isArray(v) -); -Handlebars.registerHelper("isArray", Array.isArray); -Handlebars.registerHelper("json", (ctx) => JSON.stringify(ctx, null, 2)); diff --git a/src/constants/securityConstants.js b/src/constants/securityConstants.js index 9583ece..79a078a 100644 --- a/src/constants/securityConstants.js +++ b/src/constants/securityConstants.js @@ -1,6 +1,8 @@ // config/securityConstants.js -module.exports = ({ nonce }) => ({ +const { baseUrl } = require("../utils/baseUrl"); + +module.exports = { LOCALHOST_HOSTNAMES: ["127.0.0.1", "localhost"], HEALTHCHECK_METHOD: "HEAD", HEALTHCHECK_PATH: "/health", @@ -8,22 +10,30 @@ FORBIDDEN_STATUS_CODE: 403, HSTS_MAX_AGE: 63072000, CSP_DIRECTIVES: { - defaultSrc: ["'self'"], - scriptSrc: ["'self'", "https://hcaptcha.com", "https://cdn.jsdelivr.net"], + defaultSrc: ["'self'", baseUrl], + scriptSrc: [ + "'self'", + "https://hcaptcha.com", + "https://cdn.jsdelivr.net", + "https://cdnjs.cloudflare.com", + // "'sha256-dMV9we3strWiwZYu55JT4zbPbIhmVvBssnieDrKQMKw='", + // "'sha256-dMV9we3strWiwZYu55JT4zbPbIhmVvBssnieDrKQMKw='", + ], styleSrc: [ "'self'", "https:", - "'sha256-huhqpKwGcFswbXjh5F/DueoxnLh3Yh/pg/lNbo+tnLE='", - `'${nonce}'`, + // "'sha256-huhqpKwGcFswbXjh5F/DueoxnLh3Yh/pg/lNbo+tnLE='", ], imgSrc: [ "'self'", "data:", "https://licensebuttons.net", "https://cdn.jsdelivr.net", + "https://licensebuttons.net", + "https://cdn.jsdelivr.net", ], frameSrc: ["'self'", "https://newassets.hcaptcha.com"], objectSrc: ["'none'"], upgradeInsecureRequests: [], }, -}); +}; diff --git a/src/middleware/applyProductionSecurity.js b/src/middleware/applyProductionSecurity.js index d9c64cd..4bae06f 100644 --- a/src/middleware/applyProductionSecurity.js +++ b/src/middleware/applyProductionSecurity.js @@ -37,24 +37,27 @@ return crypto.randomBytes(16).toString("base64"); } -const securityPolicy = (req, res, next) => { - const nonce = generateNonce(); - res.locals.nonce = nonce; // if templates need it +const securityPolicy = + (overrides = {}) => + (req, res, next) => { + const nonce = generateNonce(); + res.locals.nonce = nonce; - helmet.contentSecurityPolicy({ - directives: { + const mergedDirectives = { ...CSP_DIRECTIVES, - defaultSrc: ["'self'", baseUrl], + ...overrides, scriptSrc: [ - "'self'", + ...(overrides.scriptSrc || CSP_DIRECTIVES.scriptSrc), `'nonce-${nonce}'`, - "https://hcaptcha.com", - "https://cdn.jsdelivr.net", ], - }, - })(req, res, next); -}; + }; + return helmet.contentSecurityPolicy({ directives: mergedDirectives })( + req, + res, + next + ); + }; const applyProductionSecurity = [ disablePoweredBy, hpp(), @@ -62,7 +65,7 @@ // rateLimit middleware can be added here blockLocalhostAccess, helmet.hsts({ maxAge: HSTS_MAX_AGE }), - securityPolicy, + securityPolicy(), ]; -module.exports = applyProductionSecurity; +module.exports = { applyProductionSecurity, securityPolicy }; diff --git a/src/middleware/index.js b/src/middleware/index.js index 16bf3c0..7ab9603 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -6,7 +6,7 @@ const routes = require("../routes"); const formatHtml = require("./formatHtml"); const logEvent = require("./analytics.js"); -const applyProductionSecurity = require("./applyProductionSecurity"); +const { applyProductionSecurity } = require("./applyProductionSecurity"); const validateRequestIntegrity = require("./validateRequestIntegrity"); const errorHandler = require("./errorHandler"); const baseContext = require("./baseContext"); diff --git a/src/routes/index.js b/src/routes/index.js index 086be87..aaa7489 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -69,7 +69,7 @@ router.use(pages); router.use(rssFeed); router.use(tags); -router.use("/presentation", presentation); +router.use("/projects/website-presentation", presentation); router.use("/docs", docs); router.get("/blog/:year/:month/:name", post); diff --git a/src/routes/presentation.js b/src/routes/presentation.js index ba314c3..3a07f91 100644 --- a/src/routes/presentation.js +++ b/src/routes/presentation.js @@ -8,39 +8,75 @@ const HttpError = require("../utils/HttpError"); const { qualifyLink } = require("../utils/qualifyLinks"); const { baseUrl } = require("../utils/baseUrl"); +const { CSP_DIRECTIVES } = require("../constants/securityConstants"); +const { securityPolicy } = require("../middleware/applyProductionSecurity"); const yamlPath = path.resolve("content/presentation.yaml"); +function resolveReturnUrl(req, res, next) { + const myDomain = "jasonpoage.com"; + const fallbackUrl = baseUrl; + const referrer = req.body?.referrer; -router.get("/", async (req, res, next) => { + req.returnUrl = fallbackUrl; + + if (typeof referrer !== "string") return next(); + try { - const fileContent = await fs.readFile(yamlPath, "utf8"); - const data = yaml.load(fileContent); + const url = new URL(referrer); - // Wrap relative URLs with qualifyLink() - if (data.slides) { - for (const slide of data.slides) { - if (slide.images) { - slide.images = slide.images.map((img) => { - if (img.src && !img.src.match(/^https?:\/\//)) { - img.src = qualifyLink(img.src); - } - return img; - }); + const isSameDomain = url.hostname.endsWith(myDomain); + const isNotPresentation = !url.pathname.includes( + "/projects/website-presentation" + ); + + if (isSameDomain && isNotPresentation) { + req.returnUrl = referrer; + } + } catch { + // Invalid referrer, keep fallback + } + + next(); +} + +router.get( + "/", + resolveReturnUrl, + securityPolicy({ + scriptSrc: [...CSP_DIRECTIVES.scriptSrc, "'unsafe-eval'"], + styleSrc: [...CSP_DIRECTIVES.styleSrc, "'unsafe-inline'"], + }), + async (req, res, next) => { + try { + const fileContent = await fs.readFile(yamlPath, "utf8"); + const data = yaml.load(fileContent); + + if (data.slides) { + for (const slide of data.slides) { + if (slide.images) { + slide.images = slide.images.map((img) => { + if (img.src && !img.src.match(/^https?:\/\//)) { + img.src = qualifyLink(img.src); + } + return img; + }); + } } } - } - res.render("pages/presentation", { - layout: "presentation", - slides: data.slides, - title: data.title, - baseUrl, - nonce: res.locals.nonce, - }); - } catch (err) { - req.log.error(err.stack); - next(new HttpError("Failed to load presentation data", 500)); + res.render("pages/presentation", { + layout: "presentation", + slides: data.slides, + title: data.title, + baseUrl, + returnUrl: req.returnUrl, + nonce: res.locals.nonce, + }); + } catch (err) { + req.log.error(err.stack); + next(new HttpError("Failed to load presentation data", 500)); + } } -}); +); module.exports = router; diff --git a/src/views/layouts/docs.handlebars b/src/views/layouts/docs.handlebars index d638c6d..6a9d656 100644 --- a/src/views/layouts/docs.handlebars +++ b/src/views/layouts/docs.handlebars @@ -13,10 +13,7 @@ {{{_sections.styles}}}