mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-02-12 08:19:35 +01:00
[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:
parent
004d764238
commit
8ed7ccade8
17 changed files with 127 additions and 78 deletions
|
|
@ -43,17 +43,10 @@ HTTP_DISPLAY_SORT=2 # Domain display
|
|||
HTTP_HIDE_OTHER=false # true = only show first domain, false = show all
|
||||
|
||||
# --- USER AUTHENTICATION & INBOX LOCKING ---
|
||||
# Authentication System
|
||||
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)
|
||||
|
||||
# Database Paths
|
||||
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_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
14
app.js
|
|
@ -51,13 +51,19 @@ if (config.user.authEnabled) {
|
|||
app.set('inboxLock', inboxLock)
|
||||
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(() => {
|
||||
const inactive = inboxLock.getInactive(config.user.lockReleaseHours)
|
||||
if (inactive.length > 0) {
|
||||
debug(`Found ${inactive.length} inactive locked inbox(es)`)
|
||||
// Note: Auto-release of user locks would require storing userId
|
||||
// For now, inactive locks remain until user logs in
|
||||
debug(`Auto-releasing ${inactive.length} locked inbox(es) due to user inactivity (${config.user.lockReleaseHours} hours without login)`)
|
||||
inactive.forEach(lock => {
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const config = {
|
|||
// Feature Limits
|
||||
maxForwardEmails: Number(process.env.USER_MAX_FORWARD_EMAILS) || 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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -121,17 +121,22 @@ class InboxLock {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get inactive locked inboxes (not accessed in X hours)
|
||||
* @param {number} hoursThreshold - Hours of inactivity
|
||||
* @returns {Array<string>} - Array of inactive inbox addresses
|
||||
* Get inactive locked inboxes (user hasn't logged in for X hours)
|
||||
* @param {number} hoursThreshold - Hours of user inactivity (no login)
|
||||
* @returns {Array<Object>} - Array of {userId, address} for inactive locks
|
||||
*/
|
||||
getInactive(hoursThreshold) {
|
||||
const cutoff = Date.now() - (hoursThreshold * 60 * 60 * 1000)
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT inbox_address FROM user_locked_inboxes
|
||||
WHERE last_accessed < ?
|
||||
SELECT ul.user_id, ul.inbox_address, u.last_login
|
||||
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
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
function checkLockAccess(req, res, next) {
|
||||
const inboxLock = req.app.get('inboxLock')
|
||||
const address = req.params.address
|
||||
const userId = req.session ? .userId
|
||||
const isAuthenticated = req.session ? .isAuthenticated
|
||||
const userId = req.session && req.session.userId
|
||||
const isAuthenticated = req.session && req.session.isAuthenticated
|
||||
|
||||
if (!address || !inboxLock) {
|
||||
return next()
|
||||
|
|
@ -14,7 +14,7 @@ function checkLockAccess(req, res, next) {
|
|||
// Also allow session-based access for immediate unlock after locking
|
||||
const hasAccess = isAuthenticated && userId ?
|
||||
(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
|
||||
if (isLocked && !hasAccess) {
|
||||
|
|
@ -28,7 +28,7 @@ function checkLockAccess(req, res, next) {
|
|||
count: count,
|
||||
message: 'This inbox is locked by another user. Only the owner can access it.',
|
||||
branding: req.app.get('config').http.branding,
|
||||
currentUser: req.session ? .username,
|
||||
currentUser: req.session && req.session.username,
|
||||
authEnabled: req.app.get('config').user.authEnabled
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -741,7 +741,7 @@ text-muted {
|
|||
|
||||
.account-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
|
@ -852,13 +852,19 @@ text-muted {
|
|||
color: var(--color-text-gray);
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: inline;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.button-small {
|
||||
padding: 0rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button-danger {
|
||||
|
|
|
|||
|
|
@ -162,9 +162,10 @@ router.post('/login',
|
|||
if (result.success) {
|
||||
debug(`User logged in successfully: ${username}`)
|
||||
|
||||
// Regenerate session to prevent fixation attacks
|
||||
// Store redirect URL before regenerating session
|
||||
const redirectUrl = req.session.redirectAfterLogin || '/'
|
||||
|
||||
// Regenerate session to prevent fixation attacks
|
||||
req.session.regenerate((err) => {
|
||||
if (err) {
|
||||
debug(`Session regeneration error: ${err.message}`)
|
||||
|
|
@ -185,7 +186,7 @@ router.post('/login',
|
|||
return res.redirect('/auth')
|
||||
}
|
||||
|
||||
debug(`Session created for user: ${username}`)
|
||||
debug(`Session created for user: ${username}, redirecting to: ${redirectUrl}`)
|
||||
res.redirect(redirectUrl)
|
||||
})
|
||||
})
|
||||
|
|
@ -205,23 +206,30 @@ router.post('/login',
|
|||
|
||||
// GET /logout - Logout user
|
||||
router.get('/logout', (req, res) => {
|
||||
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)
|
||||
} else {
|
||||
debug(`User logged out: ${username}`)
|
||||
}
|
||||
res.redirect('/')
|
||||
})
|
||||
} else {
|
||||
res.redirect('/')
|
||||
}
|
||||
})
|
||||
// Store redirect URL before destroying session
|
||||
const redirectUrl = req.query.redirect || req.get('Referer') || '/'
|
||||
|
||||
// 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) => {
|
||||
if (req.session && req.session.userId && req.session.isAuthenticated) {
|
||||
res.json({
|
||||
|
|
|
|||
|
|
@ -113,13 +113,13 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, optiona
|
|||
|
||||
// Check lock status
|
||||
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
|
||||
const userId = req.session ? .userId
|
||||
const isAuthenticated = req.session ? .isAuthenticated
|
||||
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 ? .lockedInbox === req.params.address)
|
||||
(req.session && req.session.lockedInbox === req.params.address)
|
||||
|
||||
// Get user's verified emails if logged in
|
||||
let userForwardEmails = []
|
||||
|
|
@ -211,13 +211,13 @@ router.get(
|
|||
|
||||
const inboxLock = req.app.get('inboxLock')
|
||||
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
|
||||
const userId = req.session ? .userId
|
||||
const isAuthenticated = req.session ? .isAuthenticated
|
||||
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 ? .lockedInbox === req.params.address)
|
||||
(req.session && req.session.lockedInbox === req.params.address)
|
||||
|
||||
// Get user's verified emails if logged in
|
||||
let userForwardEmails = []
|
||||
|
|
|
|||
|
|
@ -107,19 +107,7 @@ router.post('/unlock', requireAuth, async(req, res) => {
|
|||
}
|
||||
})
|
||||
|
||||
router.get('/logout', (req, res) => {
|
||||
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('/')
|
||||
})
|
||||
// Legacy logout route removed - handled by auth.js
|
||||
|
||||
router.post('/remove', requireAuth, async(req, res) => {
|
||||
const { address } = req.body
|
||||
|
|
|
|||
|
|
@ -45,14 +45,7 @@ router.get('/inbox/random', (req, res, _next) => {
|
|||
res.redirect(`/inbox/${inbox}`)
|
||||
})
|
||||
|
||||
router.get('/logout', (req, res, _next) => {
|
||||
|
||||
/**
|
||||
* If we ever need a logout sequence, now we can have one!
|
||||
*/
|
||||
|
||||
res.redirect('/')
|
||||
})
|
||||
// Legacy logout route removed - handled by auth.js
|
||||
|
||||
router.post(
|
||||
'/', [
|
||||
|
|
@ -91,4 +84,4 @@ router.post(
|
|||
}
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
module.exports = router
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
<!-- Locked Inboxes Section -->
|
||||
<div class="account-card">
|
||||
<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 %}
|
||||
<ul class="inbox-list">
|
||||
|
|
|
|||
|
|
@ -2,9 +2,23 @@
|
|||
|
||||
{% block header %}
|
||||
<div class="action-links">
|
||||
{% if authEnabled and not currentUser %}
|
||||
<a href="/auth" aria-label="Login or Register">Account</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">
|
||||
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<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" aria-label="Logout">Logout</a>
|
||||
<a href="/logout?redirect={{ ('/inbox/' ~ address) | url_encode }}" aria-label="Logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<a href="/" aria-label="Return to home">Home</a>
|
||||
<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">
|
||||
<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"/>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
<h3>✓ Account Benefits</h3>
|
||||
<div class="features-grid">
|
||||
<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">Access your locked inboxes anywhere</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,24 @@
|
|||
|
||||
{% block header %}
|
||||
<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">
|
||||
<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"/>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<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="/" aria-label="Home">Home</a>
|
||||
<a href="/logout?redirect={{ ('/inbox/' ~ address ~ '/' ~ uid) | url_encode }}" aria-label="Logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<a href="/" aria-label="Return to home">Home</a>
|
||||
<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">
|
||||
<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"/>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,24 @@
|
|||
|
||||
{% block header %}
|
||||
<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">
|
||||
<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"/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue