diff --git a/src/app.js b/src/app.js index 4e31252..c7996f7 100644 --- a/src/app.js +++ b/src/app.js @@ -1,5 +1,5 @@ // src/app.js -console.log('CWD:', process.cwd()); +console.log("CWD:", process.cwd()); require("dotenv").config(); const setupMiddleware = require("./middleware"); @@ -7,6 +7,9 @@ const { manualLogger } = require("./utils/logging"); // const path = require("path"); +const { startTokenCleanup } = require("./utils/tokenCleanup"); +startTokenCleanup(); + app = setupMiddleware(); port = process.env.PORT || 3400; diff --git a/src/middleware/baseContext.js b/src/middleware/baseContext.js index 8676f74..a0d17a8 100644 --- a/src/middleware/baseContext.js +++ b/src/middleware/baseContext.js @@ -1,15 +1,12 @@ // src/middleware/baseContext.js const getBaseContext = require("../utils/baseContext"); +const { qualifyLink } = require("../utils/qualifyLinks"); +const { generateToken } = require("../utils/adminToken"); module.exports = async function baseContextMiddleware(req, res, next) { const isAuthenticated = req.isAuthenticated; - - const scheme = req.protocol; - const host = req.get("host"); - const requestUri = req.originalUrl; - const rd = `${scheme}://${host}${requestUri}`; - - const adminLoginUrl = `${process.env.AUTH_LOGIN}${encodeURIComponent(rd)}`; + const token = generateToken(); + const adminLoginUrl = qualifyLink(`/${token}`); const baseContext = await getBaseContext(isAuthenticated, { adminLoginUrl }); res.locals.baseContext = baseContext; diff --git a/src/routes/admin.js b/src/routes/admin.js new file mode 100644 index 0000000..0cac580 --- /dev/null +++ b/src/routes/admin.js @@ -0,0 +1,46 @@ +// src/routes/admin.js +const express = require("express"); +const { validateToken, cleanupTokens } = require("../utils/adminToken"); +const HttpError = require("../utils/HttpError"); +const router = express.Router(); + +// Middleware to cleanup expired tokens periodically +router.use((req, res, next) => { + // Clean up expired tokens on each request + if (Math.random() < 0.1) { + // 10% chance to cleanup on each request + cleanupTokens(); + } + next(); +}); + +router.get("/:token", (req, res, next) => { + const { token } = req.params; + if (!token) { + return next(); + } + + // Validate the token before proceeding + if (!validateToken(token)) { + const error = new HttpError("Invalid or expired token", 401, { token }); + req.log.warn({ err: error, token }, "Token validation failed"); + return next(); // fail silently + } + + const scheme = req.protocol; + const host = req.get("host"); + const referrer = req.get("Referer") || req.get("Referrer") || ""; + + const rd = referrer.startsWith("http") + ? referrer + : `${scheme}://${host}${referrer}`; + + const adminLoginUrl = `${process.env.AUTH_LOGIN}${rd}`; + res.set("Content-Type", "text/html"); + res + .status(301) + .set("Location", adminLoginUrl) + .render("pages/redirect", { layout: "layouts/redirect", adminLoginUrl }); +}); + +module.exports = router; diff --git a/src/routes/index.js b/src/routes/index.js index 6c97aa4..6902e98 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -7,6 +7,7 @@ const blog_index = require("./blog_index"); const csrfToken = require("../middleware/csrfToken"); const errorPage = require("./errorPage"); +const admin = require("./admin"); const contact = require("./contact"); const sitemap = require("./sitemap"); @@ -25,6 +26,7 @@ }); router.use(logs); +router.use(admin); router.post("/track", analytics); router.post("/analytics", analytics); diff --git a/src/utils/adminToken.js b/src/utils/adminToken.js new file mode 100644 index 0000000..b24f19c --- /dev/null +++ b/src/utils/adminToken.js @@ -0,0 +1,75 @@ +// src/utils/adminToken.js +const crypto = require("crypto"); + +const TOKEN_TTL_MS = 10 * 60 * 1000; // 10 minutes +const preAuthTokens = new Map(); // token -> expiry timestamp (ms) + +function generateToken() { + const token = crypto.randomBytes(24).toString("base64url"); + const expiry = Date.now() + TOKEN_TTL_MS; + preAuthTokens.set(token, expiry); + return token; +} + +function validateToken(token) { + const expiry = preAuthTokens.get(token); + if (!expiry || expiry < Date.now()) { + // Remove expired token + preAuthTokens.delete(token); + return false; + } + return true; +} + +function revokeToken(token) { + return preAuthTokens.delete(token); +} + +function cleanupTokens() { + const now = Date.now(); + for (const [token, expiry] of preAuthTokens.entries()) { + if (expiry < now) { + preAuthTokens.delete(token); + } + } +} + +// Optional: Get token info for debugging +function getTokenInfo(token) { + const expiry = preAuthTokens.get(token); + if (!expiry) return null; + + return { + token, + expiresAt: new Date(expiry), + isExpired: expiry < Date.now(), + ttlMs: expiry - Date.now(), + }; +} + +// Optional: Get all active tokens (for admin/debugging) +function getAllTokens() { + const now = Date.now(); + const tokens = []; + + for (const [token, expiry] of preAuthTokens.entries()) { + tokens.push({ + token, + expiresAt: new Date(expiry), + isExpired: expiry < now, + ttlMs: expiry - now, + }); + } + + return tokens; +} + +module.exports = { + generateToken, + validateToken, + revokeToken, + cleanupTokens, + getTokenInfo, + getAllTokens, + TOKEN_TTL_MS, +}; diff --git a/src/utils/tokenCleanup.js b/src/utils/tokenCleanup.js new file mode 100644 index 0000000..8bdd5a4 --- /dev/null +++ b/src/utils/tokenCleanup.js @@ -0,0 +1,14 @@ +// src/utils/tokenCleanup.js +const { cleanupTokens } = require("./adminToken"); + +// Set up periodic cleanup (run every 5 minutes) +const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes + +function startTokenCleanup() { + setInterval(() => { + cleanupTokens(); + console.log("Cleaned up expired pre-auth tokens"); + }, CLEANUP_INTERVAL); +} + +module.exports = { startTokenCleanup }; diff --git a/src/views/layouts/redirect.handlebars b/src/views/layouts/redirect.handlebars new file mode 100644 index 0000000..207ed41 --- /dev/null +++ b/src/views/layouts/redirect.handlebars @@ -0,0 +1,18 @@ + + + + + + + Redirecting... + + + + + {{{body}}} + + + + diff --git a/src/views/pages/redirect.handlebars b/src/views/pages/redirect.handlebars new file mode 100644 index 0000000..23bb188 --- /dev/null +++ b/src/views/pages/redirect.handlebars @@ -0,0 +1,5 @@ +
+

Redirecting...

+

Please wait while we redirect you to the authentication service.

+

If you are not redirected automatically, click here.

+