[Chore]: Misc changes around user merge

- Update lock removal timer and behaviour
- Redirect to previous path on sign-in and out
- Fix dashbaord UI and other UX elemets
- Lose sanity threlf times
This commit is contained in:
ClaraCrazy 2026-01-02 20:56:14 +01:00
parent 004d764238
commit 8ed7ccade8
No known key found for this signature in database
GPG key ID: EBBC896ACB497011
17 changed files with 127 additions and 78 deletions

View file

@ -43,17 +43,10 @@ HTTP_DISPLAY_SORT=2 # Domain display
HTTP_HIDE_OTHER=false # true = only show first domain, false = show all HTTP_HIDE_OTHER=false # true = only show first domain, false = show all
# --- USER AUTHENTICATION & INBOX LOCKING --- # --- USER AUTHENTICATION & INBOX LOCKING ---
# Authentication System
USER_AUTH_ENABLED=false # Enable user registration/login system (default: false) USER_AUTH_ENABLED=false # Enable user registration/login system (default: false)
# Session Secret (shared for both locking and user sessions)
USER_SESSION_SECRET="change-this-secret-in-production" # Secret for session encryption (used for auth & locking) USER_SESSION_SECRET="change-this-secret-in-production" # Secret for session encryption (used for auth & locking)
# Database Paths
USER_DATABASE_PATH="./db/data.db" # Path to application database (users, forwarding, locks) USER_DATABASE_PATH="./db/data.db" # Path to application database (users, forwarding, locks)
# Feature Limits
USER_MAX_FORWARD_EMAILS=5 # Maximum verified forwarding emails per user USER_MAX_FORWARD_EMAILS=5 # Maximum verified forwarding emails per user
USER_MAX_LOCKED_INBOXES=5 # Maximum locked inboxes per user USER_MAX_LOCKED_INBOXES=5 # Maximum locked inboxes per user
LOCK_RELEASE_HOURS=720 # Auto-release locked inboxes after X hours of inactivity (default: 720 = 30 days) LOCK_RELEASE_HOURS=168 # Auto-release locked inboxes after X hours without login (default: 168 = 7 days)

14
app.js
View file

@ -51,13 +51,19 @@ if (config.user.authEnabled) {
app.set('inboxLock', inboxLock) app.set('inboxLock', inboxLock)
debug('Inbox lock service initialized (user-based)') debug('Inbox lock service initialized (user-based)')
// Check for inactive locked inboxes // Check for inactive locked inboxes (users who haven't logged in for 7 days)
setInterval(() => { setInterval(() => {
const inactive = inboxLock.getInactive(config.user.lockReleaseHours) const inactive = inboxLock.getInactive(config.user.lockReleaseHours)
if (inactive.length > 0) { if (inactive.length > 0) {
debug(`Found ${inactive.length} inactive locked inbox(es)`) debug(`Auto-releasing ${inactive.length} locked inbox(es) due to user inactivity (${config.user.lockReleaseHours} hours without login)`)
// Note: Auto-release of user locks would require storing userId inactive.forEach(lock => {
// For now, inactive locks remain until user logs in try {
inboxLock.release(lock.userId, lock.address)
debug(`Released lock on ${lock.address} for inactive user ${lock.userId}`)
} catch (error) {
debug(`Failed to release lock on ${lock.address}: ${error.message}`)
}
})
} }
}, config.imap.refreshIntervalSeconds * 1000) }, config.imap.refreshIntervalSeconds * 1000)

View file

@ -86,7 +86,7 @@ const config = {
// Feature Limits // Feature Limits
maxForwardEmails: Number(process.env.USER_MAX_FORWARD_EMAILS) || 5, maxForwardEmails: Number(process.env.USER_MAX_FORWARD_EMAILS) || 5,
maxLockedInboxes: Number(process.env.USER_MAX_LOCKED_INBOXES) || 5, maxLockedInboxes: Number(process.env.USER_MAX_LOCKED_INBOXES) || 5,
lockReleaseHours: Number(process.env.LOCK_RELEASE_HOURS) || 720 // 30 days default lockReleaseHours: Number(process.env.LOCK_RELEASE_HOURS) || 168 // 7 days default
} }
}; };

View file

@ -121,17 +121,22 @@ class InboxLock {
} }
/** /**
* Get inactive locked inboxes (not accessed in X hours) * Get inactive locked inboxes (user hasn't logged in for X hours)
* @param {number} hoursThreshold - Hours of inactivity * @param {number} hoursThreshold - Hours of user inactivity (no login)
* @returns {Array<string>} - Array of inactive inbox addresses * @returns {Array<Object>} - Array of {userId, address} for inactive locks
*/ */
getInactive(hoursThreshold) { getInactive(hoursThreshold) {
const cutoff = Date.now() - (hoursThreshold * 60 * 60 * 1000) const cutoff = Date.now() - (hoursThreshold * 60 * 60 * 1000)
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT inbox_address FROM user_locked_inboxes SELECT ul.user_id, ul.inbox_address, u.last_login
WHERE last_accessed < ? FROM user_locked_inboxes ul
JOIN users u ON ul.user_id = u.id
WHERE u.last_login IS NULL OR u.last_login < ?
`) `)
return stmt.all(cutoff).map(row => row.inbox_address) return stmt.all(cutoff).map(row => ({
userId: row.user_id,
address: row.inbox_address
}))
} }
/** /**

View file

@ -1,8 +1,8 @@
function checkLockAccess(req, res, next) { function checkLockAccess(req, res, next) {
const inboxLock = req.app.get('inboxLock') const inboxLock = req.app.get('inboxLock')
const address = req.params.address const address = req.params.address
const userId = req.session ? .userId const userId = req.session && req.session.userId
const isAuthenticated = req.session ? .isAuthenticated const isAuthenticated = req.session && req.session.isAuthenticated
if (!address || !inboxLock) { if (!address || !inboxLock) {
return next() return next()
@ -14,7 +14,7 @@ function checkLockAccess(req, res, next) {
// Also allow session-based access for immediate unlock after locking // Also allow session-based access for immediate unlock after locking
const hasAccess = isAuthenticated && userId ? const hasAccess = isAuthenticated && userId ?
(inboxLock.isLockedByUser(address, userId) || req.session.lockedInbox === address.toLowerCase()) : (inboxLock.isLockedByUser(address, userId) || req.session.lockedInbox === address.toLowerCase()) :
(req.session ? .lockedInbox === address.toLowerCase()) (req.session && req.session.lockedInbox === address.toLowerCase())
// Block access to locked inbox without proper authentication // Block access to locked inbox without proper authentication
if (isLocked && !hasAccess) { if (isLocked && !hasAccess) {
@ -28,7 +28,7 @@ function checkLockAccess(req, res, next) {
count: count, count: count,
message: 'This inbox is locked by another user. Only the owner can access it.', message: 'This inbox is locked by another user. Only the owner can access it.',
branding: req.app.get('config').http.branding, branding: req.app.get('config').http.branding,
currentUser: req.session ? .username, currentUser: req.session && req.session.username,
authEnabled: req.app.get('config').user.authEnabled authEnabled: req.app.get('config').user.authEnabled
}) })
} }

View file

@ -741,7 +741,7 @@ text-muted {
.account-grid { .account-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); grid-template-columns: repeat(2, 1fr);
gap: 2rem; gap: 2rem;
margin-top: 2rem; margin-top: 2rem;
} }
@ -852,13 +852,19 @@ text-muted {
color: var(--color-text-gray); color: var(--color-text-gray);
} }
form {
margin: 0;
}
.inline-form { .inline-form {
display: inline; display: inline;
align-self: center;
} }
.button-small { .button-small {
padding: 0rem 1rem; padding: 0rem 1rem;
font-size: 0.85rem; font-size: 0.85rem;
margin: 0;
} }
.button-danger { .button-danger {

View file

@ -162,9 +162,10 @@ router.post('/login',
if (result.success) { if (result.success) {
debug(`User logged in successfully: ${username}`) debug(`User logged in successfully: ${username}`)
// Regenerate session to prevent fixation attacks // Store redirect URL before regenerating session
const redirectUrl = req.session.redirectAfterLogin || '/' const redirectUrl = req.session.redirectAfterLogin || '/'
// Regenerate session to prevent fixation attacks
req.session.regenerate((err) => { req.session.regenerate((err) => {
if (err) { if (err) {
debug(`Session regeneration error: ${err.message}`) debug(`Session regeneration error: ${err.message}`)
@ -185,7 +186,7 @@ router.post('/login',
return res.redirect('/auth') return res.redirect('/auth')
} }
debug(`Session created for user: ${username}`) debug(`Session created for user: ${username}, redirecting to: ${redirectUrl}`)
res.redirect(redirectUrl) res.redirect(redirectUrl)
}) })
}) })
@ -205,23 +206,30 @@ router.post('/login',
// GET /logout - Logout user // GET /logout - Logout user
router.get('/logout', (req, res) => { router.get('/logout', (req, res) => {
if (req.session) { // Store redirect URL before destroying session
const username = req.session.username const redirectUrl = req.query.redirect || req.get('Referer') || '/'
req.session.destroy((err) => {
if (err) {
debug(`Logout error: ${err.message}`)
console.error('Error during logout', err)
} else {
debug(`User logged out: ${username}`)
}
res.redirect('/')
})
} else {
res.redirect('/')
}
})
// GET /auth/check - JSON endpoint for checking auth status (AJAX) debug(`Logout requested with redirect: ${redirectUrl}`)
if (req.session) {
const username = req.session.username
req.session.destroy((err) => {
if (err) {
debug(`Logout error: ${err.message}`)
console.error('Error during logout', err)
return res.redirect('/')
}
debug(`User logged out: ${username}, redirecting to: ${redirectUrl}`)
// Clear cookie explicitly
res.clearCookie('connect.sid')
res.redirect(redirectUrl)
})
} else {
debug(`No session found, redirecting to: ${redirectUrl}`)
res.redirect(redirectUrl)
}
}) // GET /auth/check - JSON endpoint for checking auth status (AJAX)
router.get('/auth/check', (req, res) => { router.get('/auth/check', (req, res) => {
if (req.session && req.session.userId && req.session.isAuthenticated) { if (req.session && req.session.userId && req.session.isAuthenticated) {
res.json({ res.json({

View file

@ -113,13 +113,13 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, optiona
// Check lock status // Check lock status
const isLocked = inboxLock && inboxLock.isLocked(req.params.address) const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
const userId = req.session ? .userId const userId = req.session && req.session.userId
const isAuthenticated = req.session ? .isAuthenticated const isAuthenticated = req.session && req.session.isAuthenticated
// Check if user has access (either owns the lock or has session access) // Check if user has access (either owns the lock or has session access)
const hasAccess = isAuthenticated && userId && inboxLock ? const hasAccess = isAuthenticated && userId && inboxLock ?
(inboxLock.isLockedByUser(req.params.address, userId) || req.session.lockedInbox === req.params.address) : (inboxLock.isLockedByUser(req.params.address, userId) || req.session.lockedInbox === req.params.address) :
(req.session ? .lockedInbox === req.params.address) (req.session && req.session.lockedInbox === req.params.address)
// Get user's verified emails if logged in // Get user's verified emails if logged in
let userForwardEmails = [] let userForwardEmails = []
@ -211,13 +211,13 @@ router.get(
const inboxLock = req.app.get('inboxLock') const inboxLock = req.app.get('inboxLock')
const isLocked = inboxLock && inboxLock.isLocked(req.params.address) const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
const userId = req.session ? .userId const userId = req.session && req.session.userId
const isAuthenticated = req.session ? .isAuthenticated const isAuthenticated = req.session && req.session.isAuthenticated
// Check if user has access (either owns the lock or has session access) // Check if user has access (either owns the lock or has session access)
const hasAccess = isAuthenticated && userId && inboxLock ? const hasAccess = isAuthenticated && userId && inboxLock ?
(inboxLock.isLockedByUser(req.params.address, userId) || req.session.lockedInbox === req.params.address) : (inboxLock.isLockedByUser(req.params.address, userId) || req.session.lockedInbox === req.params.address) :
(req.session ? .lockedInbox === req.params.address) (req.session && req.session.lockedInbox === req.params.address)
// Get user's verified emails if logged in // Get user's verified emails if logged in
let userForwardEmails = [] let userForwardEmails = []

View file

@ -107,19 +107,7 @@ router.post('/unlock', requireAuth, async(req, res) => {
} }
}) })
router.get('/logout', (req, res) => { // Legacy logout route removed - handled by auth.js
const mailProcessingService = req.app.get('mailProcessingService')
// Clear cache before logout
if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) {
debug('Clearing lock cache for logout')
mailProcessingService.cachedFetchFullMail.clear()
}
debug('Clearing lockedInbox from session (lock logout)')
delete req.session.lockedInbox
res.redirect('/')
})
router.post('/remove', requireAuth, async(req, res) => { router.post('/remove', requireAuth, async(req, res) => {
const { address } = req.body const { address } = req.body

View file

@ -45,14 +45,7 @@ router.get('/inbox/random', (req, res, _next) => {
res.redirect(`/inbox/${inbox}`) res.redirect(`/inbox/${inbox}`)
}) })
router.get('/logout', (req, res, _next) => { // Legacy logout route removed - handled by auth.js
/**
* If we ever need a logout sequence, now we can have one!
*/
res.redirect('/')
})
router.post( router.post(
'/', [ '/', [
@ -91,4 +84,4 @@ router.post(
} }
) )
module.exports = router module.exports = router

View file

@ -87,7 +87,7 @@
<!-- Locked Inboxes Section --> <!-- Locked Inboxes Section -->
<div class="account-card"> <div class="account-card">
<h2>Locked Inboxes</h2> <h2>Locked Inboxes</h2>
<p class="card-description">Manage your locked inboxes. These are protected by your account and only accessible when logged in.</p> <p class="card-description">Manage your locked inboxes. These are protected by your account and only accessible when logged in. Locks auto-release after 7 days without login.</p>
{% if lockedInboxes|length > 0 %} {% if lockedInboxes|length > 0 %}
<ul class="inbox-list"> <ul class="inbox-list">

View file

@ -2,9 +2,23 @@
{% block header %} {% block header %}
<div class="action-links"> <div class="action-links">
{% if authEnabled and not currentUser %} {% if currentUser %}
<a href="/auth" aria-label="Login or Register">Account</a> <!-- Account Dropdown (logged in) -->
{% if authEnabled %}
<div class="action-dropdown">
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
<div class="dropdown-menu">
<a href="/account" aria-label="Account settings">Settings</a>
<a href="/logout?redirect=/" aria-label="Logout">Logout</a>
</div>
</div>
{% endif %}
{% else %}
{% if authEnabled %}
<a href="/auth" aria-label="Login or Register">Account</a>
{% endif %}
{% endif %} {% endif %}
<a href="/" aria-label="Return to home">Home</a> <a href="/" aria-label="Return to home">Home</a>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode"> <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">

View file

@ -25,7 +25,7 @@
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button> <button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a href="/account" aria-label="Account settings">Settings</a> <a href="/account" aria-label="Account settings">Settings</a>
<a href="/logout" aria-label="Logout">Logout</a> <a href="/logout?redirect={{ ('/inbox/' ~ address) | url_encode }}" aria-label="Logout">Logout</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@ -37,6 +37,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<a href="/" aria-label="Return to home">Home</a>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode"> <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>

View file

@ -114,7 +114,7 @@
<h3>✓ Account Benefits</h3> <h3>✓ Account Benefits</h3>
<div class="features-grid"> <div class="features-grid">
<div class="feature-item">Forward emails to verified addresses</div> <div class="feature-item">Forward emails to verified addresses</div>
<div class="feature-item">Lock up to 5 inboxes with passwords</div> <div class="feature-item">Lock up to 5 inboxes to your account</div>
<div class="feature-item">Manage multiple forwarding destinations</div> <div class="feature-item">Manage multiple forwarding destinations</div>
<div class="feature-item">Access your locked inboxes anywhere</div> <div class="feature-item">Access your locked inboxes anywhere</div>
</div> </div>

View file

@ -2,7 +2,24 @@
{% block header %} {% block header %}
<div class="action-links"> <div class="action-links">
<a href="/inbox/{{ example }}">Example Inbox</a> {% if currentUser %}
<!-- Account Dropdown (logged in) -->
{% if authEnabled %}
<div class="action-dropdown">
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
<div class="dropdown-menu">
<a href="/account" aria-label="Account settings">Settings</a>
<a href="/logout?redirect=/" aria-label="Logout">Logout</a>
</div>
</div>
{% endif %}
{% else %}
{% if authEnabled %}
<a href="/auth" aria-label="Login or Register">Account</a>
{% endif %}
{% endif %}
<a href="/inbox/{{ example }}" aria-label="View example inbox">Example Inbox</a>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode"> <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>

View file

@ -21,7 +21,7 @@
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button> <button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a href="/account" aria-label="Account settings">Settings</a> <a href="/account" aria-label="Account settings">Settings</a>
<a href="/" aria-label="Home">Home</a> <a href="/logout?redirect={{ ('/inbox/' ~ address ~ '/' ~ uid) | url_encode }}" aria-label="Logout">Logout</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@ -34,6 +34,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<a href="/" aria-label="Return to home">Home</a>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode"> <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>

View file

@ -2,7 +2,24 @@
{% block header %} {% block header %}
<div class="action-links"> <div class="action-links">
<a href="/" aria-label="Return to home">← Return to Home</a> {% if currentUser %}
<!-- Account Dropdown (logged in) -->
{% if authEnabled %}
<div class="action-dropdown">
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
<div class="dropdown-menu">
<a href="/account" aria-label="Account settings">Settings</a>
<a href="/logout?redirect=/" aria-label="Logout">Logout</a>
</div>
</div>
{% endif %}
{% else %}
{% if authEnabled %}
<a href="/auth" aria-label="Login or Register">Account</a>
{% endif %}
{% endif %}
<a href="/" aria-label="Return to home">Home</a>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode"> <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>