mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +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
|
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
14
app.js
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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 = []
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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"/>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"/>
|
||||||
|
|
|
||||||
|
|
@ -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"/>
|
||||||
|
|
|
||||||
|
|
@ -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"/>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue