diff --git a/.gitignore b/.gitignore index c618026..da15b0f 100755 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ .coverage/ *.sqlite3 *.sqlite +/logs /logs/*/*.json /logs/*/*/*.json *.gz diff --git a/content b/content index b06e407..b09f15a 160000 --- a/content +++ b/content @@ -1 +1 @@ -Subproject commit b06e40705ee75e462bb82b090b01de225f80284e +Subproject commit b09f15adaeea68955ce64ec568ca86cc12261772 diff --git a/src/app.js b/src/app.js index 6b31d21..8b3c592 100644 --- a/src/app.js +++ b/src/app.js @@ -3,25 +3,21 @@ const net = require("net"); const setupMiddleware = require("./middleware"); -const { winstonLogger } = require("./utils/logging"); +const { + winstonLogger, + handleUncaughtException, + handleUnhandledRejection, +} = require("./utils/logging"); const { startTokenCleanup } = require("./utils/tokenCleanup"); +const { cleanupOldSessions } = require("./utils/logManager"); const SERVER_PORT = process.env.TEST_PORT || process.env.SERVER_PORT || 3400; const SERVER_LISTEN_LOG = (port) => `Server listening on http://localhost:${port}`; const NODE_ENV_LOG = `NODE_ENV: ${process.env.NODE_ENV}`; -const UNCUGHT_EXCEPTION_MSG = "Uncaught Exception:"; -const UNHANDLED_REJECTION_MSG = "Unhandled Rejection:"; -function handleUncaughtException(err) { - winstonLogger.error(UNCUGHT_EXCEPTION_MSG, err.stack || err); -} - -function handleUnhandledRejection(reason) { - winstonLogger.error(UNHANDLED_REJECTION_MSG, reason?.stack || reason); -} - +cleanupOldSessions(); startTokenCleanup(); const app = setupMiddleware(); diff --git a/src/middleware/baseContext.js b/src/middleware/baseContext.js index f64ca1d..36533a4 100644 --- a/src/middleware/baseContext.js +++ b/src/middleware/baseContext.js @@ -1,14 +1,62 @@ // 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) { +// src/utils/baseContext.js +const path = require("path"); +const getPostsMenu = require("../services/postsMenuService"); +const { formatMonth } = require("../utils/formatMonth"); +const { qualifyNavLinks } = require("../utils/qualifyLinks.js"); +const { baseUrl } = require("../utils/baseUrl.js"); +const navLinks = require(path.join(__dirname, "../../content/navLinks.json")); +const processMenuLinks = require("../utils/processMenuLinks"); + +const getSiteTitle = (owner) => `${owner}'s Software Blog`; + +const POSTS_DIR = path.join(__dirname, "../../content/posts"); +const DEFAULT_CONTEXT = { + showSidebar: true, + showFooter: true, +}; + +module.exports.attachBaseContextGetter = async (req, res, next) => { + req.getBaseContext = async (isAuthenticated, overrides = {}) => { + const filteredNavLinks = processMenuLinks( + navLinks, + isAuthenticated, + req.path + ); + const qualifiedNavLinks = qualifyNavLinks(filteredNavLinks); + const menu = await getPostsMenu(POSTS_DIR); + const siteOwner = process.env.SITE_OWNER; + + const context = { + title: getSiteTitle(siteOwner), + siteOwner, + originCountry: process.env.COUNTRY, + hCaptchaKey: process.env.HCAPTCHA_KEY, + navLinks: qualifiedNavLinks, + years: menu, + formatMonth, + baseUrl, + isAuthenticated, + ...DEFAULT_CONTEXT, + ...overrides, + }; + + return context; + }; + next(); +}; + +module.exports.buildBaseContext = async (req, res, next) => { const isAuthenticated = req.isAuthenticated; const token = generateToken(); const adminLoginUrl = qualifyLink(`/${token}`); - const baseContext = await getBaseContext(isAuthenticated, { adminLoginUrl }); + const baseContext = await req.getBaseContext(isAuthenticated, { + adminLoginUrl, + }); res.locals.baseContext = baseContext; res.renderWithBaseContext = (template, overrides = {}) => { diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index 42365c6..4488c90 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -1,6 +1,5 @@ // src/middleware/errorHandler const crypto = require("crypto"); -const getBaseContext = require("../utils/baseContext"); const { getErrorContext } = require("../utils/errorContext"); const { buildErrorRenderContext } = require("../utils/buildErrorRenderContext"); const { isDev } = require("../utils/env"); @@ -74,7 +73,10 @@ metadata: err.metadata, }); - const errorPageContext = await getBaseContext(req?.isAuthenticated, context); + const errorPageContext = await req.getBaseContext( + req?.isAuthenticated, + context + ); res.status(errorContext.statusCode); res.renderGenericMessage(errorPageContext); }; diff --git a/src/middleware/index.js b/src/middleware/index.js index 50b3740..1767be6 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -9,7 +9,7 @@ const { applyProductionSecurity } = require("./applyProductionSecurity"); const validateRequestIntegrity = require("./validateRequestIntegrity"); const errorHandler = require("./errorHandler"); -const baseContext = require("./baseContext"); +const { attachBaseContextGetter, buildBaseContext } = require("./baseContext"); const hbs = require("./hbs"); const authCheck = require("./authCheck"); const { redirectMiddleware } = require("./redirect"); @@ -52,7 +52,7 @@ winstonLogger.error("JSON parsing error:", err.message); return next(err); } - winstonLogger.debug("Parsed JSON body:", req.body); + // winstonLogger.debug("Parsed JSON body:", req.body); next(); }); } else if (contentType.includes("application/x-www-form-urlencoded")) { @@ -62,7 +62,7 @@ winstonLogger.error("Form parsing error:", err.message); return next(err); } - winstonLogger.debug("Parsed form body:", req.body); + // winstonLogger.debug("Parsed form body:", req.body); next(); }); } else if (contentType.includes("multipart/form-data")) { @@ -87,11 +87,11 @@ }); return next(jsonErr); } - winstonLogger.warn("Parsed JSON body (fallback):", req.body); + // winstonLogger.warn("Parsed JSON body (fallback):", req.body); next(); }); } else { - winstonLogger.debug("Parsed form body (default):", req.body); + // winstonLogger.debug("Parsed form body (default):", req.body); next(); } }); @@ -106,7 +106,7 @@ app.use(authCheck); // Setup handlebars - app.use(baseContext); + app.use(attachBaseContextGetter, buildBaseContext); // Setup production environment if ( diff --git a/src/routes/contact.js b/src/routes/contact.js index feab0a0..16ac487 100644 --- a/src/routes/contact.js +++ b/src/routes/contact.js @@ -1,49 +1,8 @@ -// // src/routes/contact.js -// const express = require("express"); -// const router = express.Router(); -// const sendContactMail = require("../utils/sendContactMail"); -// const getBaseContext = require("../utils/baseContext"); -// const formLimiter = require("../utils/formLimiter"); -// const verifyHCaptcha = require("../utils/verifyHCaptcha"); - -// router.post("/contact", formLimiter, async (req, res, next) => { -// try { -// const { name, email, message, hcaptchaToken } = req.body; -// if (!hcaptchaToken) { -// return res.status(400).send("Captcha token missing"); -// } -// const valid = await verifyHCaptcha(hcaptchaToken); -// if (!valid) { -// return res.status(400).send("Captcha verification failed"); -// } -// await sendContactMail({ name, email, message }); -// res.redirect("/contact/thankyou"); -// } catch (err) { -// next(err); -// } -// }); - -// router.get("/contact", async (req, res) => { -// const context = await getBaseContext({ -// csrfToken: res.locals.csrfToken, -// title: "Contact", -// }); -// res.render("pages/contact.handlebars", context); -// }); - -// router.get("/contact/thankyou", async (req, res) => { -// const context = await getBaseContext({ -// title: "Thank You", -// }); -// res.render("pages/thankyou.handlebars", context); -// }); - -// module.exports = router; // src/routes/contact.js const express = require("express"); const router = express.Router(); const sendContactMail = require("../utils/sendContactMail"); -// const getBaseContext = require("../utils/baseContext"); + const formLimiter = require("../utils/formLimiter"); const verifyHCaptcha = require("../utils/verifyHCaptcha"); const crypto = require("crypto"); diff --git a/src/utils/baseContext.js b/src/utils/baseContext.js deleted file mode 100644 index ca43b5a..0000000 --- a/src/utils/baseContext.js +++ /dev/null @@ -1,42 +0,0 @@ -// src/utils/baseContext.js -const path = require("path"); -const getPostsMenu = require("../services/postsMenuService"); -const { formatMonth } = require("./formatMonth"); -const { qualifyNavLinks } = require("./qualifyLinks.js"); -const { baseUrl } = require("./baseUrl.js"); -const navLinks = require(path.join(__dirname, "../../content/navLinks.json")); -const filterSecureLinks = require("../utils/filterSecureLinks"); - -const getSiteTitle = (owner) => `${owner}'s Software Blog`; - -const POSTS_DIR = path.join(__dirname, "../../content/posts"); -const DEFAULT_CONTEXT = { - showSidebar: true, - showFooter: true, -}; - -module.exports = async function getBaseContext( - isAuthenticated, - overrides = {} -) { - const filteredNavLinks = filterSecureLinks(navLinks, isAuthenticated); - const qualifiedNavLinks = qualifyNavLinks(filteredNavLinks); - const menu = await getPostsMenu(POSTS_DIR); - const siteOwner = process.env.SITE_OWNER; - - const context = { - title: getSiteTitle(siteOwner), - siteOwner, - originCountry: process.env.COUNTRY, - hCaptchaKey: process.env.HCAPTCHA_KEY, - navLinks: qualifiedNavLinks, - years: menu, - formatMonth, - baseUrl, - isAuthenticated, - ...DEFAULT_CONTEXT, - ...overrides, - }; - - return context; -}; diff --git a/src/utils/docsContext.js b/src/utils/docsContext.js index db407dc..e61c095 100644 --- a/src/utils/docsContext.js +++ b/src/utils/docsContext.js @@ -7,7 +7,7 @@ const { baseUrl } = require("./baseUrl"); const generateDocsMenuModel = require("./generateDocsMenuModel"); const navLinks = require(path.join(__dirname, "../../content/navLinks.json")); -const filterSecureLinks = require("../utils/filterSecureLinks"); +const processMenuLinks = require("../utils/processMenuLinks"); const getSiteTitle = (owner) => `${owner}'s Software Blog`; @@ -40,7 +40,7 @@ isAuthenticated, overrides = {} ) { - const filteredNavLinks = filterSecureLinks(navLinks, isAuthenticated); + const filteredNavLinks = processMenuLinks(navLinks, isAuthenticated); const qualifiedNavLinks = qualifyNavLinks(filteredNavLinks); const siteOwner = process.env.SITE_OWNER; diff --git a/src/utils/filterSecureLinks.js b/src/utils/filterSecureLinks.js deleted file mode 100644 index cc1a50d..0000000 --- a/src/utils/filterSecureLinks.js +++ /dev/null @@ -1,14 +0,0 @@ -// src/utils/filterSecureLinks.js -function filterSecureLinks(links, isAuthenticated) { - return links - .filter(link => isAuthenticated || !link.secure) - .map(link => { - const item = { ...link }; - if (item.submenu) { - item.submenu = filterSecureLinks(item.submenu, isAuthenticated); - if (!item.submenu.length) delete item.submenu; - } - return item; - }); -} -module.exports = filterSecureLinks diff --git a/src/utils/logManager.js b/src/utils/logManager.js new file mode 100644 index 0000000..b03a0ae --- /dev/null +++ b/src/utils/logManager.js @@ -0,0 +1,359 @@ +const fs = require("fs"); +const path = require("path"); +const { winstonLogger } = require("./logging"); + +const logDir = path.join(__dirname, "../../logs"); + +class LogManager { + constructor(logDir, options = {}) { + this.logDir = logDir; + this.serverStart = Date.now(); + this.isDevelopment = process.env.NODE_ENV !== "production"; + + // Configurable thresholds + this.config = { + development: { + maxSessionCount: 25, // Fewer sessions in dev + sessionRetentionHours: 1, // Very short retention + maxTotalSizeMB: 50, // Small disk footprint + maxDiskUsagePercent: 85, // Panic threshold + cleanupIntervalMinutes: 15, // Frequent cleanup + emergencyCleanupRatio: 0.7, // Keep only 30% newest in emergency + }, + production: { + maxSessionCount: 100, + sessionRetentionHours: 24, + maxTotalSizeMB: 200, + maxDiskUsagePercent: 90, + cleanupIntervalMinutes: 60, + emergencyCleanupRatio: 0.5, + }, + ...options, // Allow override + }; + + this.currentConfig = this.isDevelopment + ? this.config.development + : this.config.production; + this.lastCleanupFile = path.join(logDir, ".last-cleanup"); + this.metricsFile = path.join(logDir, ".cleanup-metrics"); + + winstonLogger.info( + `LogManager initialized for ${this.isDevelopment ? "development" : "production"}` + ); + winstonLogger.info(`Config:`, this.currentConfig); + } + + // Main cleanup orchestrator + cleanup(force = false) { + try { + const metrics = this.getMetrics(); + winstonLogger.info(`\n=== Log Cleanup Started ===`); + winstonLogger.info(`Current sessions: ${metrics.sessionCount}`); + winstonLogger.info(`Total size: ${metrics.totalSizeMB.toFixed(2)}MB`); + winstonLogger.info(`Disk usage: ${metrics.diskUsagePercent.toFixed(1)}%`); + + // Emergency cleanup if disk is critically full + if (metrics.diskUsagePercent > this.currentConfig.maxDiskUsagePercent) { + winstonLogger.info(`🚨 EMERGENCY CLEANUP: Disk usage critical!`); + return this.emergencyCleanup(); + } + + // Size-based cleanup if total size exceeded + if (metrics.totalSizeMB > this.currentConfig.maxTotalSizeMB) { + winstonLogger.info(`📦 SIZE-BASED CLEANUP: Total size exceeded`); + return this.sizeLimitedCleanup(); + } + + // Regular cleanup if time-based or forced + if (force || this.shouldRunRegularCleanup()) { + winstonLogger.info(`🕒 REGULAR CLEANUP: Time-based maintenance`); + return this.regularCleanup(); + } + + winstonLogger.info(`✅ No cleanup needed`); + return { cleaned: false, reason: "thresholds not met" }; + } catch (error) { + winstonLogger.error(`❌ Cleanup failed:`, error); + return { cleaned: false, error: error.message }; + } + } + + // Get current metrics + getMetrics() { + const sessionsDir = path.join(this.logDir, "sessions"); + if (!fs.existsSync(sessionsDir)) { + return { + sessionCount: 0, + totalSizeMB: 0, + diskUsagePercent: 0, + sessions: [], + }; + } + + const sessions = this.getSessionsWithMetadata(); + const totalSize = this.calculateDirectorySize(sessionsDir); + const diskUsage = this.getDiskUsage(); + + return { + sessionCount: sessions.length, + totalSizeMB: totalSize / (1024 * 1024), + diskUsagePercent: diskUsage, + sessions: sessions, + }; + } + + // Get sessions with rich metadata + getSessionsWithMetadata() { + const sessionsDir = path.join(this.logDir, "sessions"); + if (!fs.existsSync(sessionsDir)) return []; + + return fs + .readdirSync(sessionsDir) + .map((sessionFolder) => { + const sessionPath = path.join(sessionsDir, sessionFolder); + try { + if (!fs.statSync(sessionPath).isDirectory()) return null; + + const files = fs.readdirSync(sessionPath); + let latestMtime = 0; + let totalSize = 0; + + files.forEach((file) => { + const filePath = path.join(sessionPath, file); + const stat = fs.statSync(filePath); + latestMtime = Math.max(latestMtime, stat.mtimeMs); + totalSize += stat.size; + }); + + const ageHours = (Date.now() - latestMtime) / (1000 * 60 * 60); + + return { + folder: sessionFolder, + path: sessionPath, + latestMtime, + ageHours, + sizeMB: totalSize / (1024 * 1024), + fileCount: files.length, + isCurrentSession: latestMtime >= this.serverStart, + isStale: ageHours > this.currentConfig.sessionRetentionHours, + }; + } catch (error) { + winstonLogger.warn( + `Error processing session ${sessionFolder}:`, + error.message + ); + return null; + } + }) + .filter(Boolean) + .sort((a, b) => b.latestMtime - a.latestMtime); // Newest first + } + + // Emergency cleanup - keep only newest sessions + emergencyCleanup() { + const sessions = this.getSessionsWithMetadata(); + const keepCount = Math.floor( + sessions.length * this.currentConfig.emergencyCleanupRatio + ); + const sessionsToDelete = sessions.slice(keepCount); + + winstonLogger.info( + `Keeping ${keepCount} newest sessions, deleting ${sessionsToDelete.length}` + ); + + let deletedCount = 0; + let freedMB = 0; + + sessionsToDelete.forEach((session) => { + if (this.deleteSession(session)) { + deletedCount++; + freedMB += session.sizeMB; + } + }); + + this.updateMetrics({ deletedCount, freedMB, type: "emergency" }); + return { cleaned: true, type: "emergency", deletedCount, freedMB }; + } + + // Size-limited cleanup - delete until under threshold + sizeLimitedCleanup() { + const sessions = this.getSessionsWithMetadata(); + const targetSizeMB = this.currentConfig.maxTotalSizeMB * 0.8; // Clean to 80% of limit + + let currentSizeMB = sessions.reduce((sum, s) => sum + s.sizeMB, 0); + let deletedCount = 0; + let freedMB = 0; + + // Delete oldest first until we're under the target + for ( + let i = sessions.length - 1; + i >= 0 && currentSizeMB > targetSizeMB; + i-- + ) { + const session = sessions[i]; + if (!session.isCurrentSession) { + if (this.deleteSession(session)) { + deletedCount++; + freedMB += session.sizeMB; + currentSizeMB -= session.sizeMB; + } + } + } + + this.updateMetrics({ deletedCount, freedMB, type: "size-limited" }); + return { cleaned: true, type: "size-limited", deletedCount, freedMB }; + } + + // Regular cleanup - age and count based + regularCleanup() { + const sessions = this.getSessionsWithMetadata(); + let deletedCount = 0; + let freedMB = 0; + + sessions.forEach((session, index) => { + const shouldDeleteByAge = !session.isCurrentSession && session.isStale; + const shouldDeleteByCount = index >= this.currentConfig.maxSessionCount; + + if (shouldDeleteByAge || shouldDeleteByCount) { + if (this.deleteSession(session)) { + deletedCount++; + freedMB += session.sizeMB; + + const reason = shouldDeleteByAge + ? `age (${session.ageHours.toFixed(1)}h)` + : "count limit"; + winstonLogger.info( + ` Deleted ${session.folder} - ${reason} - ${session.sizeMB.toFixed(2)}MB` + ); + } + } + }); + + this.updateLastCleanup(); + this.updateMetrics({ deletedCount, freedMB, type: "regular" }); + return { cleaned: true, type: "regular", deletedCount, freedMB }; + } + + // Helper methods + deleteSession(session) { + try { + fs.rmSync(session.path, { recursive: true, force: true }); + return true; + } catch (error) { + winstonLogger.error(`Failed to delete ${session.path}:`, error.message); + return false; + } + } + + shouldRunRegularCleanup() { + try { + const lastCleanup = fs.readFileSync(this.lastCleanupFile, "utf8"); + const timeSinceLastCleanup = Date.now() - parseInt(lastCleanup); + const cleanupInterval = + this.currentConfig.cleanupIntervalMinutes * 60 * 1000; + return timeSinceLastCleanup > cleanupInterval; + } catch { + return true; // First run + } + } + + updateLastCleanup() { + fs.writeFileSync(this.lastCleanupFile, Date.now().toString()); + } + + updateMetrics(data) { + const metrics = { + timestamp: new Date().toISOString(), + environment: this.isDevelopment ? "development" : "production", + ...data, + }; + + // Append to metrics log + const logLine = JSON.stringify(metrics) + "\n"; + fs.appendFileSync(this.metricsFile, logLine); + } + + calculateDirectorySize(dirPath) { + let totalSize = 0; + + const calculateSize = (itemPath) => { + try { + const stat = fs.statSync(itemPath); + if (stat.isDirectory()) { + fs.readdirSync(itemPath).forEach((item) => + calculateSize(path.join(itemPath, item)) + ); + } else { + totalSize += stat.size; + } + } catch (error) { + // Skip inaccessible files + } + }; + + if (fs.existsSync(dirPath)) { + calculateSize(dirPath); + } + + return totalSize; + } + + getDiskUsage() { + try { + const stats = fs.statSync(this.logDir); + // This is a simplified version - in production you might want to use a library + // like 'diskusage' or 'statvfs' for accurate disk space calculation + return 0; // Placeholder - implement based on your system + } catch { + return 0; + } + } + + // Utility method to show current status + status() { + const metrics = this.getMetrics(); + winstonLogger.info(`\n=== Log Manager Status ===`); + winstonLogger.info( + `Environment: ${this.isDevelopment ? "development" : "production"}` + ); + winstonLogger.info( + `Sessions: ${metrics.sessionCount} (max: ${this.currentConfig.maxSessionCount})` + ); + winstonLogger.info( + `Total size: ${metrics.totalSizeMB.toFixed(2)}MB (max: ${this.currentConfig.maxTotalSizeMB}MB)` + ); + winstonLogger.info( + `Retention: ${this.currentConfig.sessionRetentionHours}h` + ); + + if (metrics.sessions.length > 0) { + const oldest = metrics.sessions[metrics.sessions.length - 1]; + const newest = metrics.sessions[0]; + winstonLogger.info( + `Oldest session: ${oldest.ageHours.toFixed(1)}h old (${oldest.sizeMB.toFixed(2)}MB)` + ); + winstonLogger.info( + `Newest session: ${newest.ageHours.toFixed(1)}h old (${newest.sizeMB.toFixed(2)}MB)` + ); + } + + return metrics; + } +} + +// Usage: +const logManager = new LogManager(logDir, { + // Optional custom config overrides + development: { + maxSessionCount: 15, // Even more aggressive for your dev setup + sessionRetentionHours: 0.5, // 30 minutes + }, +}); + +// Call this on server start +function cleanupOldSessions() { + return logManager.cleanup(); +} + +// Export for use in other modules +module.exports = { LogManager, cleanupOldSessions }; diff --git a/src/utils/logging.js b/src/utils/logging.js index b9e6d8a..1bc8004 100644 --- a/src/utils/logging.js +++ b/src/utils/logging.js @@ -18,6 +18,8 @@ debug: "blue", }, }; +const UNCUGHT_EXCEPTION_MSG = "Uncaught Exception:"; +const UNHANDLED_REJECTION_MSG = "Unhandled Rejection:"; const fs = require("fs"); const path = require("path"); @@ -216,12 +218,25 @@ let stack = meta.stack || ""; if (stack) delete meta.stack; - let metaString = ""; - if (Object.keys(meta).length > 0) { - metaString = JSON.stringify(meta, null, 2); + // Safely stringify message + let outputMsg; + if (typeof message === "string") { + outputMsg = message; + } else { + try { + outputMsg = JSON.stringify(message, null, 2); + } catch { + outputMsg = util.inspect(message, { depth: null, colors: false }); + } } - return `[${timestamp}] [${level}] ${message}\n${stack}\n${metaString}`; + // Handle meta + let metaString = ""; + if (Object.keys(meta).length > 0) { + metaString = util.inspect(meta, { depth: null, colors: false }); + } + + return `[${timestamp}] [${level}] ${outputMsg}\n${stack}\n${metaString}`; }) ), }), @@ -229,26 +244,13 @@ ], }); -// Clean up old session directories (optional) -function cleanupOldSessions() { - const sessionsDir = path.join(logDir, "sessions"); - if (fs.existsSync(sessionsDir)) { - const sessions = fs.readdirSync(sessionsDir); - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - 30); // Keep 30 days of sessions - - sessions.forEach((sessionFolder) => { - const sessionPath = path.join(sessionsDir, sessionFolder); - const stats = fs.statSync(sessionPath); - if (stats.isDirectory() && stats.mtime < cutoffDate) { - fs.rmSync(sessionPath, { recursive: true, force: true }); - } - }); - } +function handleUncaughtException(err) { + winstonLogger.error(UNCUGHT_EXCEPTION_MSG, err.stack || err); } -// Run cleanup on startup -cleanupOldSessions(); +function handleUnhandledRejection(reason) { + winstonLogger.error(UNHANDLED_REJECTION_MSG, reason?.stack || reason); +} if ( process.env.NODE_ENV !== "production" && @@ -260,4 +262,6 @@ module.exports = { manualLogger, winstonLogger, + handleUncaughtException, + handleUnhandledRejection, }; diff --git a/src/utils/processMenuLinks.js b/src/utils/processMenuLinks.js new file mode 100644 index 0000000..92386ad --- /dev/null +++ b/src/utils/processMenuLinks.js @@ -0,0 +1,23 @@ +// src/utils/processMenuLinks.js +function processMenuLinks(links, isAuthenticated, currentPath) { + return links + .filter((link) => isAuthenticated || !link.secure) + .map((link) => { + const item = { ...link }; + if (item.appendCurrentPath && typeof item.href === "string") { + if (currentPath !== "/" && !item.href.endsWith(currentPath)) { + item.href = item.href + currentPath; + } + } + if (item.submenu) { + item.submenu = processMenuLinks( + item.submenu, + isAuthenticated, + currentPath + ); + if (!item.submenu.length) delete item.submenu; + } + return item; + }); +} +module.exports = processMenuLinks; diff --git a/src/utils/structuredLogger.js b/src/utils/structuredLogger.js index 62a7b9e..956d8ec 100644 --- a/src/utils/structuredLogger.js +++ b/src/utils/structuredLogger.js @@ -1,8 +1,6 @@ const { winstonLogger } = require("./logging"); module.exports = (level) => (req, res, next) => { - const start = process.hrtime(); - res.on("finish", () => { const { method, url, headers, query, body, ip, connection } = req; const { statusCode } = res; diff --git a/src/views/partials/copyright_notice.handlebars b/src/views/partials/copyright_notice.handlebars new file mode 100644 index 0000000..07d5d3a --- /dev/null +++ b/src/views/partials/copyright_notice.handlebars @@ -0,0 +1,12 @@ +

+ To the extent possible under law, {{siteOwner}} has waived all copyright and related or neighboring rights to this + work + + {{#if adminLoginUrl}} + . + {{else}} + . + {{/if}} + + {{> creative_commons }} +

diff --git a/src/views/partials/creative_commons.handlebars b/src/views/partials/creative_commons.handlebars new file mode 100644 index 0000000..d2cae5d --- /dev/null +++ b/src/views/partials/creative_commons.handlebars @@ -0,0 +1,3 @@ + + CC0 + diff --git a/src/views/partials/footer.handlebars b/src/views/partials/footer.handlebars index b860df0..6e4bc7d 100644 --- a/src/views/partials/footer.handlebars +++ b/src/views/partials/footer.handlebars @@ -1,45 +1,9 @@
-

- To the extent possible under law, {{siteOwner}} has waived all copyright and related or neighboring rights to this - work - - {{#if adminLoginUrl}} - . - {{else}} - . - {{/if}} - - - CC0 - -

+ {{> copyright_notice }} - + {{> w3c }} -
-

- Link to LinkedIn Profile - -

-

- Link to GitHub Profile - - - - -

-
+ {{> footer_navigation }} + + {{> social_icons }}
diff --git a/src/views/partials/footer_navigation.handlebars b/src/views/partials/footer_navigation.handlebars new file mode 100644 index 0000000..72f2123 --- /dev/null +++ b/src/views/partials/footer_navigation.handlebars @@ -0,0 +1,10 @@ + diff --git a/src/views/partials/site_headers.handlebars b/src/views/partials/site_headers.handlebars index 646c817..4cf5e00 100644 --- a/src/views/partials/site_headers.handlebars +++ b/src/views/partials/site_headers.handlebars @@ -1,8 +1,8 @@ - - + + - - + + {{{_sections.styles}}} diff --git a/src/views/partials/social_icons.handlebars b/src/views/partials/social_icons.handlebars new file mode 100644 index 0000000..fa5b620 --- /dev/null +++ b/src/views/partials/social_icons.handlebars @@ -0,0 +1,17 @@ +
+

+ Link to LinkedIn Profile + +

+

+ Link to GitHub Profile + + + + +

+
diff --git a/src/views/partials/w3c.handlebars b/src/views/partials/w3c.handlebars new file mode 100644 index 0000000..a055bea --- /dev/null +++ b/src/views/partials/w3c.handlebars @@ -0,0 +1,6 @@ +

+ + Valid CSS! + +