diff --git a/infrastructure/web/middleware/lock.js b/infrastructure/web/middleware/lock.js index a5b18eb..a8b5f2a 100644 --- a/infrastructure/web/middleware/lock.js +++ b/infrastructure/web/middleware/lock.js @@ -1,3 +1,5 @@ +const templateContext = require('../template-context') + function checkLockAccess(req, res, next) { const inboxLock = req.app.get('inboxLock') const address = req.params.address @@ -21,14 +23,10 @@ function checkLockAccess(req, res, next) { const unlockError = req.session ? req.session.unlockError : undefined if (req.session) delete req.session.unlockError - return res.render('error', { - purgeTime: require('../../../application/helper').prototype.purgeTimeElemetBuilder(), - address: address, - message: 'This inbox is locked by another user. Only the owner can access it.', - branding: req.app.get('config').http.branding, - currentUser: req.session && req.session.username, - authEnabled: req.app.get('config').user.authEnabled - }) + return res.render('error', templateContext.build(req, { + title: 'Access Denied', + message: 'This inbox is locked by another user. Only the owner can access it.' + })) } // Update last access if they have access and are authenticated diff --git a/infrastructure/web/routes/account.js b/infrastructure/web/routes/account.js index 141793d..7a30b62 100644 --- a/infrastructure/web/routes/account.js +++ b/infrastructure/web/routes/account.js @@ -3,6 +3,7 @@ const express = require('express') const router = express.Router() const { requireAuth } = require('../middleware/auth') const { body, validationResult } = require('express-validator') +const templateContext = require('../template-context') // GET /account - Account dashboard router.get('/account', requireAuth, async(req, res) => { @@ -26,25 +27,20 @@ router.get('/account', requireAuth, async(req, res) => { const config = req.app.get('config') const stats = userRepository.getUserStats(req.session.userId, config.user) - // Get purge time for footer - const purgeTime = helper.purgeTimeElemetBuilder() + const successMessage = req.session.accountSuccess + const errorMessage = req.session.accountError + delete req.session.accountSuccess + delete req.session.accountError - res.render('account', { + res.render('account', templateContext.build(req, { title: 'Account Dashboard', username: req.session.username, forwardEmails, lockedInboxes, stats, - branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'], - purgeTime: purgeTime, - smtpEnabled: config.email.features.smtp, - successMessage: req.session.accountSuccess, - errorMessage: req.session.accountError - }) - - // Clear flash messages - delete req.session.accountSuccess - delete req.session.accountError + successMessage, + errorMessage + })) } catch (error) { console.error('Account page error:', error) res.status(500).render('error', { diff --git a/infrastructure/web/routes/auth.js b/infrastructure/web/routes/auth.js index 7d55bf1..b7323a1 100644 --- a/infrastructure/web/routes/auth.js +++ b/infrastructure/web/routes/auth.js @@ -4,10 +4,7 @@ const { body, validationResult } = require('express-validator') const debug = require('debug')('48hr-email:auth-routes') const { redirectIfAuthenticated } = require('../middleware/auth') const config = require('../../../application/config') -const Helper = require('../../../application/helper') -const helper = new Helper() - -const purgeTime = helper.purgeTimeElemetBuilder() +const templateContext = require('../template-context') // Simple in-memory rate limiters for registration and login const registrationRateLimitStore = new Map() @@ -96,21 +93,13 @@ router.use((req, res, next) => { // GET /auth - Show unified auth page (login or register) router.get('/auth', redirectIfAuthenticated, (req, res) => { const config = req.app.get('config') - const errorMessage = req.session.errorMessage const successMessage = req.session.successMessage - - // Clear messages after reading - delete req.session.errorMessage delete req.session.successMessage - res.render('auth', { + res.render('auth', templateContext.build(req, { title: `Login or Register | ${(config.http.features.branding || ['48hr.email'])[0]}`, - branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'], - purgeTime: purgeTime, - smtpEnabled: config.email.features.smtp, - errorMessage, successMessage - }) + })) }) // POST /register - Process registration diff --git a/infrastructure/web/routes/error.js b/infrastructure/web/routes/error.js index 7fda3f4..8e567ad 100644 --- a/infrastructure/web/routes/error.js +++ b/infrastructure/web/routes/error.js @@ -1,13 +1,9 @@ const express = require('express') - const router = new express.Router() const config = require('../../../application/config') -const Helper = require('../../../application/helper') -const helper = new(Helper) +const templateContext = require('../template-context') const debug = require('debug')('48hr-email:routes') -const purgeTime = helper.purgeTimeElemetBuilder() - router.get('/:address/:errorCode', async(req, res, next) => { try { const mailProcessingService = req.app.get('mailProcessingService') @@ -21,14 +17,11 @@ router.get('/:address/:errorCode', async(req, res, next) => { debug(`Rendering error page ${errorCode} with message: ${message}`) const branding = config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'] res.status(errorCode) - res.render('error', { + res.render('error', templateContext.build(req, { title: `${branding[0]} | ${errorCode}`, - purgeTime: purgeTime, - address: req.params.address, message: message, - status: errorCode, - branding: branding - }) + status: errorCode + })) } catch (error) { debug('Error loading error page:', error.message) console.error('Error while loading error page', error) diff --git a/infrastructure/web/routes/inbox.js b/infrastructure/web/routes/inbox.js index faa61ce..61f68d0 100644 --- a/infrastructure/web/routes/inbox.js +++ b/infrastructure/web/routes/inbox.js @@ -6,6 +6,7 @@ const debug = require('debug')('48hr-email:routes') const config = require('../../../application/config') const Helper = require('../../../application/helper') const CryptoDetector = require('../../../application/crypto-detector') +const templateContext = require('../template-context') const helper = new(Helper) const cryptoDetector = new CryptoDetector() const { checkLockAccess } = require('../middleware/lock') @@ -105,68 +106,11 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, optiona throw new Error('Mail processing service not available') } debug(`Inbox request for ${req.params.address}`) - const inboxLock = req.app.get('inboxLock') - // Check lock status - const isLocked = inboxLock && inboxLock.isLocked(req.params.address) - const userId = req.session && req.session.userId - const isAuthenticated = req.session && req.session.isAuthenticated - - // Check if user has access (either owns the lock or has session access) - const hasAccess = isAuthenticated && userId && inboxLock ? - (inboxLock.isLockedByUser(req.params.address, userId) || req.session.lockedInbox === req.params.address) : - (req.session && req.session.lockedInbox === req.params.address) - - // Get user's verified emails if logged in - let userForwardEmails = [] - if (req.session && req.session.userId) { - const userRepository = req.app.get('userRepository') - if (userRepository) { - userForwardEmails = userRepository.getForwardEmails(req.session.userId) - } - } - - // Pull any lock error from session and clear it after reading - const lockError = req.session ? req.session.lockError : undefined - const unlockErrorSession = req.session ? req.session.unlockError : undefined - const errorMessage = req.session ? req.session.errorMessage : undefined - if (req.session) { - delete req.session.lockError - delete req.session.unlockError - delete req.session.errorMessage - } - - // Check for forward all success flag - const forwardAllSuccess = req.query.forwardedAll ? parseInt(req.query.forwardedAll) : null - - // Check for verification sent flag - const verificationSent = req.query.verificationSent === 'true' - const verificationEmail = req.query.email || '' - - res.render('inbox', { + res.render('inbox', templateContext.build(req, { title: `${(config.http.features.branding || ['48hr.email'])[0]} | ` + req.params.address, - purgeTime: purgeTime, - address: req.params.address, - mailSummaries: mailProcessingService.getMailSummaries(req.params.address), - branding: config.http.branding, - authEnabled: config.user.authEnabled, - smtpEnabled: config.email.features.smtp, - isAuthenticated: req.session && req.session.userId ? true : false, - userForwardEmails: userForwardEmails, - isLocked: isLocked, - hasAccess: hasAccess, - unlockError: unlockErrorSession, - locktimer: config.user.lockReleaseHours, - error: lockError, - redirectTo: req.originalUrl, - expiryTime: config.email.purgeTime.time, - expiryUnit: config.email.purgeTime.unit, - refreshInterval: config.imap.refreshIntervalSeconds, - errorMessage: errorMessage, - forwardAllSuccess: forwardAllSuccess, - verificationSent: verificationSent, - verificationEmail: verificationEmail - }) + mailSummaries: mailProcessingService.getMailSummaries(req.params.address) + })) } catch (error) { debug(`Error loading inbox for ${req.params.address}:`, error.message) console.error('Error while loading inbox', error) @@ -201,58 +145,13 @@ router.get( const cryptoAttachments = cryptoDetector.detectCryptoAttachments(mail.attachments) debug(`Found ${cryptoAttachments.length} cryptographic attachments`) - const inboxLock = req.app.get('inboxLock') - const isLocked = inboxLock && inboxLock.isLocked(req.params.address) - const userId = req.session && req.session.userId - const isAuthenticated = req.session && req.session.isAuthenticated - - // Check if user has access (either owns the lock or has session access) - const hasAccess = isAuthenticated && userId && inboxLock ? - (inboxLock.isLockedByUser(req.params.address, userId) || req.session.lockedInbox === req.params.address) : - (req.session && req.session.lockedInbox === req.params.address) - - // Get user's verified emails if logged in - let userForwardEmails = [] - if (req.session && req.session.userId) { - const userRepository = req.app.get('userRepository') - if (userRepository) { - userForwardEmails = userRepository.getForwardEmails(req.session.userId) - } - } - - // Pull error message from session and clear it - const errorMessage = req.session ? req.session.errorMessage : undefined - if (req.session) { - delete req.session.errorMessage - } - - // Check for forward success flag - const forwardSuccess = req.query.forwarded === 'true' - - // Check for verification sent flag - const verificationSent = req.query.verificationSent === 'true' - const verificationEmail = req.query.email || '' - debug(`Rendering email view for UID ${req.params.uid}`) - res.render('mail', { + res.render('mail', templateContext.build(req, { title: mail.subject + " | " + req.params.address, - purgeTime: purgeTime, - address: req.params.address, mail, cryptoAttachments: cryptoAttachments, - uid: req.params.uid, - branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'], - authEnabled: config.user.authEnabled, - smtpEnabled: config.email.features.smtp, - isAuthenticated: req.session && req.session.userId ? true : false, - userForwardEmails: userForwardEmails, - isLocked: isLocked, - hasAccess: hasAccess, - errorMessage: errorMessage, - forwardSuccess: forwardSuccess, - verificationSent: verificationSent, - verificationEmail: verificationEmail - }) + uid: req.params.uid + })) } else { debug(`Email ${req.params.uid} not found for ${req.params.address}`) req.session.errorMessage = 'This mail could not be found. It either does not exist or has been deleted from our servers!' @@ -424,11 +323,11 @@ router.get( // Emails are immutable, cache if found res.set('Cache-Control', 'private, max-age=600') debug(`Rendering raw email view for UID ${req.params.uid}`) - res.render('raw', { + res.render('raw', templateContext.build(req, { title: req.params.uid + " | raw | " + req.params.address, mail: rawMail, decoded: decodedMail - }) + })) } else { debug(`Raw email ${uid} not found for ${req.params.address}`) req.session.errorMessage = 'This mail could not be found. It either does not exist or has been deleted from our servers!' diff --git a/infrastructure/web/routes/stats.js b/infrastructure/web/routes/stats.js index 621e60a..ae1f0fb 100644 --- a/infrastructure/web/routes/stats.js +++ b/infrastructure/web/routes/stats.js @@ -1,6 +1,7 @@ const express = require('express') const router = new express.Router() const debug = require('debug')('48hr-email:stats-routes') +const templateContext = require('../template-context') // GET /stats - Statistics page with lazy loading router.get('/', async(req, res) => { @@ -16,10 +17,7 @@ router.get('/', async(req, res) => { return res.redirect(redirectUrl) } - const Helper = require('../../../application/helper') - const helper = new Helper() const branding = config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'] - const purgeTime = helper.purgeTimeElemetBuilder() // Return page with placeholder data immediately - real data loads via JS const placeholderStats = { @@ -48,15 +46,11 @@ router.get('/', async(req, res) => { debug(`Stats page requested - returning with lazy loading`) - res.render('stats', { + res.render('stats', templateContext.build(req, { title: `Statistics | ${branding[0]}`, - branding: branding, - purgeTime: purgeTime, stats: placeholderStats, - authEnabled: config.user.authEnabled, - currentUser: req.session && req.session.username, lazyLoad: true - }) + })) } catch (error) { debug(`Error loading stats page: ${error.message}`) console.error('Error while loading stats page', error) diff --git a/infrastructure/web/template-context.js b/infrastructure/web/template-context.js index d274c95..f492848 100644 --- a/infrastructure/web/template-context.js +++ b/infrastructure/web/template-context.js @@ -19,12 +19,36 @@ class TemplateContext { * @returns {Object} Base template context */ getBaseContext(req) { + const inboxLock = req.app.get('inboxLock') + const address = req.params && req.params.address + const userId = req.session && req.session.userId + const isAuthenticated = !!(req.session && req.session.userId) + + // Calculate lock status for current address + const isLocked = address && inboxLock ? inboxLock.isLocked(address) : false + const hasAccess = address && isAuthenticated && userId && inboxLock ? + (inboxLock.isLockedByUser(address, userId) || req.session.lockedInbox === address) : + (address && req.session && req.session.lockedInbox === address) + + // Get user's verified forward emails if logged in + let userForwardEmails = [] + if (isAuthenticated && userId) { + const userRepository = req.app.get('userRepository') + if (userRepository) { + userForwardEmails = userRepository.getForwardEmails(userId) + } + } + return { // Config values config: config, branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'], purgeTime: this.purgeTime, purgeTimeRaw: config.email.purgeTime, + expiryTime: config.email.purgeTime.time, + expiryUnit: config.email.purgeTime.unit, + refreshInterval: config.imap.refreshIntervalSeconds, + locktimer: config.user.lockReleaseHours, // Feature flags authEnabled: config.user.authEnabled, @@ -32,8 +56,29 @@ class TemplateContext { smtpEnabled: config.email.features.smtp, showInfoSection: config.http.features.infoSection, - // User session + // User session & authentication currentUser: req.session && req.session.username ? req.session.username : null, + isAuthenticated: isAuthenticated, + userForwardEmails: userForwardEmails, + + // Lock status + isLocked: isLocked, + hasAccess: hasAccess, + + // Session messages/errors (auto-clear after reading) + error: this._getAndClearSession(req, 'lockError'), + unlockError: this._getAndClearSession(req, 'unlockError'), + errorMessage: this._getAndClearSession(req, 'errorMessage'), + + // Query parameters + verificationSent: req.query && req.query.verificationSent === 'true', + verificationEmail: req.query && req.query.email || '', + forwardSuccess: req.query && req.query.forwarded === 'true', + forwardAllSuccess: req.query && req.query.forwardedAll ? parseInt(req.query.forwardedAll) : null, + + // Request info + redirectTo: req.originalUrl, + address: address, // Common data domains: this.cachedDomains, @@ -41,6 +86,17 @@ class TemplateContext { } } + /** + * Helper to get and clear session value + * @private + */ + _getAndClearSession(req, key) { + if (!req.session) return undefined + const value = req.session[key] + delete req.session[key] + return value + } + /** * Merge base context with page-specific data * @param {Object} req - Express request object diff --git a/infrastructure/web/views/layout.twig b/infrastructure/web/views/layout.twig index 645b738..9c70d4b 100644 --- a/infrastructure/web/views/layout.twig +++ b/infrastructure/web/views/layout.twig @@ -9,7 +9,7 @@ {% block metaTags %} - + @@ -20,7 +20,7 @@ - + diff --git a/infrastructure/web/views/twig-filters.js b/infrastructure/web/views/twig-filters.js index 14a8217..41e667a 100644 --- a/infrastructure/web/views/twig-filters.js +++ b/infrastructure/web/views/twig-filters.js @@ -71,12 +71,7 @@ function convertAndRound(time, unit) { */ exports.readablePurgeTime = function(purgeTime) { if (!purgeTime || !purgeTime.time || !purgeTime.unit) { - // Fallback to config if not provided - if (config.email.purgeTime) { - purgeTime = config.email.purgeTime - } else { - return '48 hours' - } + purgeTime = config.email.purgeTime } let result = `${purgeTime.time} ${purgeTime.unit}` diff --git a/infrastructure/web/web.js b/infrastructure/web/web.js index 8690c85..8319e7f 100644 --- a/infrastructure/web/web.js +++ b/infrastructure/web/web.js @@ -18,12 +18,9 @@ const lockRouter = require('./routes/lock') const authRouter = require('./routes/auth') const accountRouter = require('./routes/account') const statsRouter = require('./routes/stats') +const templateContext = require('./template-context') const { sanitizeHtmlTwigFilter, readablePurgeTime } = require('./views/twig-filters') -const Helper = require('../../application/helper') -const helper = new(Helper) -const purgeTime = helper.purgeTimeElemetBuilder() - // Utility function for consistent error handling in routes const handleRouteError = (error, req, res, next, context = 'route') => { debug(`Error in ${context}:`, error.message) @@ -143,7 +140,9 @@ app.use(async(req, res, next) => { app.use((req, res, next) => { const isImapReady = req.app.get('isImapReady') if (!isImapReady && !req.path.startsWith('/images') && !req.path.startsWith('/javascripts') && !req.path.startsWith('/stylesheets') && !req.path.startsWith('/dependencies')) { - return res.render('loading') + return res.render('loading', templateContext.build(req, { + title: 'Loading...' + })) } next() }) @@ -174,11 +173,11 @@ app.use(async(err, req, res, _next) => { // Render the error page res.status(err.status || 500) - res.render('error', { - purgeTime: purgeTime, - address: req.params && req.params.address, - branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'] - }) + res.render('error', templateContext.build(req, { + title: 'Error', + message: err.message, + status: err.status || 500 + })) } catch (renderError) { debug('Error in error handler:', renderError.message) console.error('Critical error in error handler', renderError)