48hr.email/infrastructure/web/web.js
ClaraCrazy a7691ccf43
[Feat]: UX Debug mode
Adds UX debug mode while mocking the imap server and other critical parts of the service simply to test UI elements for faster development
2026-01-05 08:45:26 +01:00

213 lines
6.5 KiB
JavaScript

const path = require('path')
const http = require('http')
const debug = require('debug')('48hr-email:server')
const express = require('express')
const session = require('express-session')
const cookieParser = require('cookie-parser')
const logger = require('morgan')
const Twig = require('twig')
const compression = require('compression')
const helmet = require('helmet')
const socketio = require('socket.io')
const config = require('../../application/config')
const inboxRouter = require('./routes/inbox')
const loginRouter = require('./routes/login')
const errorRouter = require('./routes/error')
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')
// Utility function for consistent error handling in routes
const handleRouteError = (error, req, res, next, context = 'route') => {
debug(`Error in ${context}:`, error.message)
console.error(`Error in ${context}`, error)
next(error)
}
// Init express middleware
const app = express()
app.use(helmet())
app.use(compression())
app.set('config', config)
const server = http.createServer(app)
const io = socketio(server)
app.set('socketio', io)
// HTTP request logging - only enable with DEBUG environment variable
if (process.env.DEBUG && process.env.DEBUG.includes('48hr-email')) {
app.use(logger('dev'))
}
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
// Cookie parser for signed cookies (email verification)
app.use(cookieParser(config.http.sessionSecret))
// Session support (always enabled for forward verification and inbox locking)
app.use(session({
secret: config.http.sessionSecret,
resave: false,
saveUninitialized: false,
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
}))
// Clear lock session data when user goes Home (but preserve authentication)
app.get('/', (req, res, next) => {
if (req.session && req.session.lockedInbox) {
// Only clear lock-related data, preserve user authentication
delete req.session.lockedInbox
req.session.save(() => next())
} else {
next()
}
})
// Remove trailing slash middleware (except for root)
app.use((req, res, next) => {
if (req.path.length > 1 && req.path.endsWith('/')) {
const query = req.url.slice(req.path.length) // preserve query string
return res.redirect(301, req.path.slice(0, -1) + query)
}
next()
})
// View engine setup
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'twig')
app.set('twig options', {
autoescape: true
})
// Application code:
app.use(
express.static(path.join(__dirname, 'public'), {
immutable: true,
maxAge: '1h'
})
)
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
Twig.extendFilter('readablePurgeTime', readablePurgeTime)
// Middleware to expose user session to all templates
app.use((req, res, next) => {
res.locals.authEnabled = config.user.authEnabled
res.locals.config = config
res.locals.currentUser = null
res.locals.alertMessage = req.session ? req.session.alertMessage : null
// Clear alert after reading
if (req.session && req.session.alertMessage) {
delete req.session.alertMessage
}
if (req.session && req.session.userId && req.session.username && req.session.isAuthenticated) {
res.locals.currentUser = {
id: req.session.userId,
username: req.session.username
}
}
next()
})
// Middleware to expose mail count to all templates
app.use(async(req, res, next) => {
const mailProcessingService = req.app.get('mailProcessingService')
const imapService = req.app.get('imapService')
const Helper = require('../../application/helper')
const helper = new Helper()
if (mailProcessingService) {
const count = mailProcessingService.getCount()
let largestUid = null
if (imapService) {
try {
largestUid = await imapService.getLargestUid()
} catch (e) {
debug('Error getting largest UID:', e.message)
}
}
res.locals.mailCount = helper.mailCountBuilder(count, largestUid)
} else {
res.locals.mailCount = ''
}
next()
})
// Middleware to show loading page until IMAP is ready
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', templateContext.build(req, {
title: 'Loading...'
}))
}
next()
})
app.use('/', loginRouter)
if (config.user.authEnabled) {
app.use('/', authRouter)
app.use('/', accountRouter)
}
app.use('/inbox', inboxRouter)
app.use('/error', errorRouter)
app.use('/lock', lockRouter)
app.use('/stats', statsRouter)
// Catch 404 and forward to error handler
app.use((req, res, next) => {
next({ message: 'Page not found', status: 404 })
})
// Error handler
app.use(async(err, req, res, _next) => {
try {
debug('Error handler triggered:', err.message)
// Set locals, only providing error in development
res.locals.message = err.message
res.locals.error = req.app.get('env') === 'development' ? err : {}
// Render the error page
res.status(err.status || 500)
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)
// Fallback: send plain text error if rendering fails
res.status(500).send('Internal Server Error')
}
})
/**
* Get port from environment and store in Express.
*/
app.set('port', config.http.port)
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(config.http.port)
server.on('listening', () => {
const addr = server.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
debug('Listening on ' + bind)
// Emit event for app.js to display startup banner
server.emit('ready')
})
module.exports = { app, io, server }