mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
[Feat]; Add user functionality
Add dashboard and update routes to use the new User object. Merge forwarding and locking to be user-only methods and remove old routes that no longer exist
This commit is contained in:
parent
598cea9b9c
commit
004d764238
22 changed files with 1598 additions and 502 deletions
|
|
@ -30,7 +30,6 @@ SMTP_PORT=465 # SMTP port (587
|
|||
SMTP_SECURE=true # Use SSL (true for port 465, false for other ports)
|
||||
SMTP_USER="noreply@48hr.email" # SMTP authentication username (also used as from address)
|
||||
SMTP_PASSWORD="password" # SMTP authentication password
|
||||
SMTP_FROM_NAME="48hr Email Service" # Display name for forwarded emails
|
||||
|
||||
# --- HTTP / WEB CONFIGURATION ---
|
||||
HTTP_PORT=3000 # Port
|
||||
|
|
@ -51,8 +50,7 @@ USER_AUTH_ENABLED=false # Enable user re
|
|||
USER_SESSION_SECRET="change-this-secret-in-production" # Secret for session encryption (used for auth & locking)
|
||||
|
||||
# Database Paths
|
||||
USER_DATABASE_PATH="./db/users.db" # Path to user database
|
||||
LOCK_DATABASE_PATH="./db/locked-inboxes.db" # Path to lock database
|
||||
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
|
||||
|
|
|
|||
45
app.js
45
app.js
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
const config = require('./application/config')
|
||||
const debug = require('debug')('48hr-email:app')
|
||||
const Helper = require('./application/helper')
|
||||
|
||||
const { app, io, server } = require('./infrastructure/web/web')
|
||||
const ClientNotification = require('./infrastructure/web/client-notification')
|
||||
|
|
@ -20,31 +21,23 @@ const clientNotification = new ClientNotification()
|
|||
debug('Client notification service initialized')
|
||||
clientNotification.use(io)
|
||||
|
||||
// Initialize inbox locking (always available for registered users)
|
||||
const inboxLock = new InboxLock(config.user.lockDbPath)
|
||||
app.set('inboxLock', inboxLock)
|
||||
debug('Inbox lock service initialized')
|
||||
// Check for inactive locked inboxes
|
||||
setInterval(() => {
|
||||
const inactive = inboxLock.getInactive(config.user.lockReleaseHours)
|
||||
if (inactive.length > 0) {
|
||||
debug(`Releasing ${inactive.length} inactive locked inbox(es)`)
|
||||
inactive.forEach(address => inboxLock.release(address))
|
||||
}
|
||||
}, config.imap.refreshIntervalSeconds * 1000)
|
||||
|
||||
const imapService = new ImapService(config, inboxLock)
|
||||
debug('IMAP service initialized')
|
||||
|
||||
const smtpService = new SmtpService(config)
|
||||
debug('SMTP service initialized')
|
||||
app.set('smtpService', smtpService)
|
||||
|
||||
const verificationStore = new VerificationStore()
|
||||
debug('Verification store initialized')
|
||||
app.set('verificationStore', verificationStore)
|
||||
|
||||
// Set config in app for route access
|
||||
app.set('config', config)
|
||||
|
||||
// Initialize user repository and auth service (if enabled)
|
||||
let inboxLock = null
|
||||
if (config.user.authEnabled) {
|
||||
// Migrate legacy database files for backwards compatibility
|
||||
Helper.migrateDatabase(config.user.databasePath)
|
||||
|
||||
const userRepository = new UserRepository(config.user.databasePath)
|
||||
debug('User repository initialized')
|
||||
app.set('userRepository', userRepository)
|
||||
|
|
@ -52,13 +45,33 @@ if (config.user.authEnabled) {
|
|||
const authService = new AuthService(userRepository, config)
|
||||
debug('Auth service initialized')
|
||||
app.set('authService', authService)
|
||||
|
||||
// Initialize inbox locking with user repository
|
||||
inboxLock = new InboxLock(userRepository)
|
||||
app.set('inboxLock', inboxLock)
|
||||
debug('Inbox lock service initialized (user-based)')
|
||||
|
||||
// Check for inactive locked inboxes
|
||||
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
|
||||
}
|
||||
}, config.imap.refreshIntervalSeconds * 1000)
|
||||
|
||||
console.log('User authentication system enabled')
|
||||
} else {
|
||||
app.set('userRepository', null)
|
||||
app.set('authService', null)
|
||||
app.set('inboxLock', null)
|
||||
debug('User authentication system disabled')
|
||||
}
|
||||
|
||||
const imapService = new ImapService(config, inboxLock)
|
||||
debug('IMAP service initialized')
|
||||
|
||||
const mailProcessingService = new MailProcessingService(
|
||||
new MailRepository(),
|
||||
imapService,
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ const config = {
|
|||
port: Number(process.env.SMTP_PORT) || 465,
|
||||
secure: parseBool(process.env.SMTP_SECURE) || true,
|
||||
user: parseValue(process.env.SMTP_USER),
|
||||
password: parseValue(process.env.SMTP_PASSWORD),
|
||||
fromName: parseValue(process.env.SMTP_FROM_NAME) || '48hr.email Forwarding'
|
||||
password: parseValue(process.env.SMTP_PASSWORD)
|
||||
},
|
||||
|
||||
http: {
|
||||
|
|
@ -79,8 +78,7 @@ const config = {
|
|||
authEnabled: parseBool(process.env.USER_AUTH_ENABLED) || false,
|
||||
|
||||
// Database
|
||||
databasePath: parseValue(process.env.USER_DATABASE_PATH) || './db/users.db',
|
||||
lockDbPath: parseValue(process.env.LOCK_DATABASE_PATH) || './db/locked-inboxes.db',
|
||||
databasePath: parseValue(process.env.USER_DATABASE_PATH) || './db/data.db',
|
||||
|
||||
// Session & Auth
|
||||
sessionSecret: parseValue(process.env.USER_SESSION_SECRET) || 'change-me-in-production',
|
||||
|
|
|
|||
|
|
@ -232,6 +232,36 @@ class Helper {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate legacy database files for backwards compatibility
|
||||
* - Renames users.db to data.db if it exists
|
||||
* - Logs if locked-inboxes.db exists (no longer needed)
|
||||
* @param {string} dbPath - Path to the current database (data.db)
|
||||
*/
|
||||
static migrateDatabase(dbPath) {
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const dbDir = path.dirname(dbPath)
|
||||
const legacyUsersDb = path.join(dbDir, 'users.db')
|
||||
const legacyLockedInboxesDb = path.join(dbDir, 'locked-inboxes.db')
|
||||
|
||||
// Migrate users.db to data.db
|
||||
if (fs.existsSync(legacyUsersDb) && !fs.existsSync(dbPath)) {
|
||||
console.log(`Migrating ${legacyUsersDb} → ${dbPath}`)
|
||||
fs.renameSync(legacyUsersDb, dbPath)
|
||||
debug(`Database migrated: users.db → data.db`)
|
||||
}
|
||||
|
||||
// Warn about old locked-inboxes.db
|
||||
if (fs.existsSync(legacyLockedInboxesDb)) {
|
||||
console.log(`⚠️ Found legacy ${legacyLockedInboxesDb}`)
|
||||
console.log(` This database is no longer used. Locks are now stored in ${path.basename(dbPath)}.`)
|
||||
console.log(` You can safely delete ${legacyLockedInboxesDb} after verifying your locks are working.`)
|
||||
debug('Legacy locked-inboxes.db detected but not migrated (data already in user_locked_inboxes table)')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Helper
|
||||
|
|
|
|||
|
|
@ -261,7 +261,8 @@ class MailProcessingService extends EventEmitter {
|
|||
|
||||
// Forward via SMTP service
|
||||
debug(`Forwarding email to ${destinationEmail}`)
|
||||
const result = await this.smtpService.forwardMail(fullMail, destinationEmail)
|
||||
const branding = this.config.http.branding[0] || '48hr.email'
|
||||
const result = await this.smtpService.forwardMail(fullMail, destinationEmail, branding)
|
||||
|
||||
if (result.success) {
|
||||
debug(`Email forwarded successfully. MessageId: ${result.messageId}`)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class SmtpService {
|
|||
* @param {string} destinationEmail - Email address to forward to
|
||||
* @returns {Promise<{success: boolean, error?: string, messageId?: string}>}
|
||||
*/
|
||||
async forwardMail(mail, destinationEmail) {
|
||||
async forwardMail(mail, destinationEmail, branding = '48hr.email') {
|
||||
if (!this.transporter) {
|
||||
return {
|
||||
success: false,
|
||||
|
|
@ -83,7 +83,7 @@ class SmtpService {
|
|||
try {
|
||||
debug(`Forwarding email (Subject: "${mail.subject}") to ${destinationEmail}`)
|
||||
|
||||
const forwardMessage = this._buildForwardMessage(mail, destinationEmail)
|
||||
const forwardMessage = this._buildForwardMessage(mail, destinationEmail, branding)
|
||||
|
||||
const info = await this.transporter.sendMail(forwardMessage)
|
||||
|
||||
|
|
@ -106,10 +106,11 @@ class SmtpService {
|
|||
* Build the forward message structure
|
||||
* @param {Object} mail - Parsed email object
|
||||
* @param {string} destinationEmail - Destination address
|
||||
* @param {string} branding - Service branding name
|
||||
* @returns {Object} - Nodemailer message object
|
||||
* @private
|
||||
*/
|
||||
_buildForwardMessage(mail, destinationEmail) {
|
||||
_buildForwardMessage(mail, destinationEmail, branding = '48hr.email') {
|
||||
// Extract original sender info
|
||||
const originalFrom = (mail.from && mail.from.text) || 'Unknown Sender'
|
||||
const originalTo = (mail.to && mail.to.text) || 'Unknown Recipient'
|
||||
|
|
@ -138,7 +139,7 @@ To: ${originalTo}
|
|||
// Build the message object
|
||||
const message = {
|
||||
from: {
|
||||
name: this.config.smtp.fromName,
|
||||
name: branding,
|
||||
address: this.config.smtp.user
|
||||
},
|
||||
to: destinationEmail,
|
||||
|
|
@ -224,9 +225,10 @@ ${mail.html}
|
|||
* @param {string} token - Verification token
|
||||
* @param {string} baseUrl - Base URL for verification link
|
||||
* @param {string} branding - Service branding name
|
||||
* @param {string} verifyPath - Verification path (default: /inbox/verify)
|
||||
* @returns {Promise<{success: boolean, error?: string, messageId?: string}>}
|
||||
*/
|
||||
async sendVerificationEmail(destinationEmail, token, baseUrl, branding = '48hr.email') {
|
||||
async sendVerificationEmail(destinationEmail, token, baseUrl, branding = '48hr.email', verifyPath = '/inbox/verify') {
|
||||
if (!this.transporter) {
|
||||
return {
|
||||
success: false,
|
||||
|
|
@ -234,7 +236,7 @@ ${mail.html}
|
|||
}
|
||||
}
|
||||
|
||||
const verificationLink = `${baseUrl}/inbox/verify?token=${token}`
|
||||
const verificationLink = `${baseUrl}${verifyPath}?token=${token}`
|
||||
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
|
|
|
|||
|
|
@ -1,95 +1,222 @@
|
|||
const Database = require('better-sqlite3')
|
||||
const bcrypt = require('bcrypt')
|
||||
const path = require('path')
|
||||
const debug = require('debug')('48hr-email:inbox-lock')
|
||||
|
||||
/**
|
||||
* InboxLock - Manages inbox locking for registered users
|
||||
* Uses user_locked_inboxes table from the users database
|
||||
*/
|
||||
class InboxLock {
|
||||
constructor(dbPath = './db/locked-inboxes.db') {
|
||||
// Ensure data directory exists
|
||||
const fs = require('fs')
|
||||
const dir = path.dirname(dbPath)
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
this.db = new Database(dbPath)
|
||||
this.db.pragma('journal_mode = WAL')
|
||||
this._initTable()
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository
|
||||
this.db = userRepository.db
|
||||
debug('InboxLock initialized with user database')
|
||||
}
|
||||
|
||||
_initTable() {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS locked_inboxes (
|
||||
address TEXT PRIMARY KEY,
|
||||
password_hash TEXT NOT NULL,
|
||||
locked_at INTEGER NOT NULL,
|
||||
last_access INTEGER NOT NULL
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
async lock(address, password) {
|
||||
const passwordHash = await bcrypt.hash(password, 10)
|
||||
const now = Date.now()
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO locked_inboxes (address, password_hash, locked_at, last_access)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
/**
|
||||
* Lock an inbox for a user (no separate password needed - uses account ownership)
|
||||
* @param {number} userId - User ID
|
||||
* @param {string} address - Inbox address to lock
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
async lock(userId, address) {
|
||||
try {
|
||||
stmt.run(address.toLowerCase(), passwordHash, now, now)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
// Check if user can lock more inboxes (5 max)
|
||||
if (!this.canLockMore(userId)) {
|
||||
throw new Error('You have reached the maximum of 5 locked inboxes')
|
||||
}
|
||||
|
||||
// Check if inbox is already locked
|
||||
if (this.isLocked(address)) {
|
||||
throw new Error('This inbox is already locked')
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO user_locked_inboxes (user_id, inbox_address, password_hash, locked_at, last_accessed)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
// Use empty password hash since we rely on user authentication
|
||||
stmt.run(userId, address.toLowerCase(), '', now, now)
|
||||
debug(`Inbox ${address} locked by user ${userId}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
debug(`Failed to lock inbox ${address}:`, error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async unlock(address, password) {
|
||||
const stmt = this.db.prepare('SELECT * FROM locked_inboxes WHERE address = ?')
|
||||
const inbox = stmt.get(address.toLowerCase())
|
||||
/**
|
||||
* Unlock an inbox (verify user owns the lock)
|
||||
* @param {number} userId - User ID attempting to unlock
|
||||
* @param {string} address - Inbox address to unlock
|
||||
* @returns {Promise<Object|null>} - Lock info if successful, null if failed
|
||||
*/
|
||||
async unlock(userId, address) {
|
||||
try {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM user_locked_inboxes
|
||||
WHERE user_id = ? AND inbox_address = ?
|
||||
`)
|
||||
const lock = stmt.get(userId, address.toLowerCase())
|
||||
|
||||
if (!inbox) {
|
||||
if (!lock) {
|
||||
debug(`No lock found for user ${userId} on inbox ${address}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Update last access
|
||||
this.updateAccess(userId, address)
|
||||
debug(`Inbox ${address} unlocked by user ${userId}`)
|
||||
return lock
|
||||
} catch (error) {
|
||||
debug(`Error unlocking inbox ${address}:`, error.message)
|
||||
return null
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, inbox.password_hash)
|
||||
if (!valid) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Update last access
|
||||
this.updateAccess(address)
|
||||
return inbox
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an inbox is locked by any user
|
||||
* @param {string} address - Inbox address
|
||||
* @returns {boolean} - True if locked
|
||||
*/
|
||||
isLocked(address) {
|
||||
const stmt = this.db.prepare('SELECT address FROM locked_inboxes WHERE address = ?')
|
||||
return stmt.get(address.toLowerCase()) !== undefined
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT inbox_address FROM user_locked_inboxes
|
||||
WHERE inbox_address = ?
|
||||
`)
|
||||
const result = stmt.get(address.toLowerCase())
|
||||
return result !== undefined
|
||||
}
|
||||
|
||||
updateAccess(address) {
|
||||
const stmt = this.db.prepare('UPDATE locked_inboxes SET last_access = ? WHERE address = ?')
|
||||
stmt.run(Date.now(), address.toLowerCase())
|
||||
/**
|
||||
* Check if an inbox is locked by a specific user
|
||||
* @param {string} address - Inbox address
|
||||
* @param {number} userId - User ID
|
||||
* @returns {boolean} - True if locked by this user
|
||||
*/
|
||||
isLockedByUser(address, userId) {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT inbox_address FROM user_locked_inboxes
|
||||
WHERE inbox_address = ? AND user_id = ?
|
||||
`)
|
||||
const result = stmt.get(address.toLowerCase(), userId)
|
||||
return result !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last access timestamp for a locked inbox
|
||||
* @param {number} userId - User ID
|
||||
* @param {string} address - Inbox address
|
||||
*/
|
||||
updateAccess(userId, address) {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE user_locked_inboxes
|
||||
SET last_accessed = ?
|
||||
WHERE user_id = ? AND inbox_address = ?
|
||||
`)
|
||||
stmt.run(Date.now(), userId, address.toLowerCase())
|
||||
debug(`Updated last access for inbox ${address} by user ${userId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inactive locked inboxes (not accessed in X hours)
|
||||
* @param {number} hoursThreshold - Hours of inactivity
|
||||
* @returns {Array<string>} - Array of inactive inbox addresses
|
||||
*/
|
||||
getInactive(hoursThreshold) {
|
||||
const cutoff = Date.now() - (hoursThreshold * 60 * 60 * 1000)
|
||||
const stmt = this.db.prepare('SELECT address FROM locked_inboxes WHERE last_access < ?')
|
||||
return stmt.all(cutoff).map(row => row.address)
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT inbox_address FROM user_locked_inboxes
|
||||
WHERE last_accessed < ?
|
||||
`)
|
||||
return stmt.all(cutoff).map(row => row.inbox_address)
|
||||
}
|
||||
|
||||
release(address) {
|
||||
const stmt = this.db.prepare('DELETE FROM locked_inboxes WHERE address = ?')
|
||||
stmt.run(address.toLowerCase())
|
||||
/**
|
||||
* Release (unlock) an inbox
|
||||
* @param {number} userId - User ID
|
||||
* @param {string} address - Inbox address to release
|
||||
*/
|
||||
release(userId, address) {
|
||||
const stmt = this.db.prepare(`
|
||||
DELETE FROM user_locked_inboxes
|
||||
WHERE user_id = ? AND inbox_address = ?
|
||||
`)
|
||||
stmt.run(userId, address.toLowerCase())
|
||||
debug(`Released lock on inbox ${address} by user ${userId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all locked inboxes (for admin/debugging)
|
||||
* @returns {Array<string>} - Array of all locked inbox addresses
|
||||
*/
|
||||
getAllLocked() {
|
||||
const stmt = this.db.prepare('SELECT address FROM locked_inboxes')
|
||||
return stmt.all().map(row => row.address)
|
||||
const stmt = this.db.prepare('SELECT inbox_address FROM user_locked_inboxes')
|
||||
return stmt.all().map(row => row.inbox_address)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all locked inboxes for a specific user
|
||||
* @param {number} userId - User ID
|
||||
* @returns {Array<Object>} - Array of locked inbox objects with metadata
|
||||
*/
|
||||
getUserLockedInboxes(userId) {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT inbox_address, locked_at, last_accessed
|
||||
FROM user_locked_inboxes
|
||||
WHERE user_id = ?
|
||||
ORDER BY locked_at DESC
|
||||
`)
|
||||
const inboxes = stmt.all(userId)
|
||||
|
||||
return inboxes.map(inbox => ({
|
||||
address: inbox.inbox_address,
|
||||
lockedAt: inbox.locked_at,
|
||||
lastAccess: inbox.last_accessed,
|
||||
lastAccessedAgo: this._formatTimeAgo(inbox.last_accessed)
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can lock more inboxes (5 max)
|
||||
* @param {number} userId - User ID
|
||||
* @returns {boolean} - True if user can lock more
|
||||
*/
|
||||
canLockMore(userId) {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM user_locked_inboxes
|
||||
WHERE user_id = ?
|
||||
`)
|
||||
const result = stmt.get(userId)
|
||||
return result.count < 5
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of locked inboxes for a user
|
||||
* @param {number} userId - User ID
|
||||
* @returns {number} - Number of locked inboxes
|
||||
*/
|
||||
getUserLockedCount(userId) {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM user_locked_inboxes
|
||||
WHERE user_id = ?
|
||||
`)
|
||||
const result = stmt.get(userId)
|
||||
return result.count
|
||||
}
|
||||
|
||||
_formatTimeAgo(timestamp) {
|
||||
const seconds = Math.floor((Date.now() - timestamp) / 1000)
|
||||
|
||||
if (seconds < 60) return 'just now'
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`
|
||||
return `${Math.floor(seconds / 86400)} days ago`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InboxLock
|
||||
module.exports = InboxLock
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ class UserRepository {
|
|||
/**
|
||||
* Get all verified forwarding emails for a user
|
||||
* @param {number} userId
|
||||
* @returns {Array} - Array of email objects
|
||||
* @returns {Array} - Array of email objects with formatted timestamps
|
||||
*/
|
||||
getForwardEmails(userId) {
|
||||
try {
|
||||
|
|
@ -197,14 +197,37 @@ class UserRepository {
|
|||
ORDER BY created_at DESC
|
||||
`)
|
||||
const emails = stmt.all(userId)
|
||||
|
||||
// Add formatted timestamp
|
||||
const formatted = emails.map(email => ({
|
||||
...email,
|
||||
verifiedAgo: this._formatTimeAgo(email.verified_at)
|
||||
}))
|
||||
|
||||
debug(`Found ${emails.length} forward emails for user ${userId}`)
|
||||
return emails
|
||||
return formatted
|
||||
} catch (error) {
|
||||
debug(`Error getting forward emails: ${error.message}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp to relative time
|
||||
* @param {number} timestamp - Unix timestamp in milliseconds
|
||||
* @returns {string} - Formatted time ago string
|
||||
* @private
|
||||
*/
|
||||
_formatTimeAgo(timestamp) {
|
||||
const seconds = Math.floor((Date.now() - timestamp) / 1000)
|
||||
|
||||
if (seconds < 60) return 'just now'
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`
|
||||
if (seconds < 2592000) return `${Math.floor(seconds / 86400)} days ago`
|
||||
return `${Math.floor(seconds / 2592000)} months ago`
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a specific forwarding email
|
||||
* @param {number} userId
|
||||
|
|
@ -276,9 +299,10 @@ class UserRepository {
|
|||
/**
|
||||
* Get user statistics
|
||||
* @param {number} userId
|
||||
* @returns {Object} - {lockedInboxesCount, forwardEmailsCount, accountAge}
|
||||
* @param {Object} config - Application configuration
|
||||
* @returns {Object} - {lockedInboxesCount, forwardEmailsCount, accountAge, maxLockedInboxes, maxForwardEmails, lockReleaseHours}
|
||||
*/
|
||||
getUserStats(userId) {
|
||||
getUserStats(userId, config = {}) {
|
||||
try {
|
||||
const user = this.getUserById(userId)
|
||||
if (!user) {
|
||||
|
|
@ -294,7 +318,8 @@ class UserRepository {
|
|||
|
||||
const lockedInboxesCount = lockedInboxesStmt.get(userId).count
|
||||
const forwardEmailsCount = forwardEmailsStmt.get(userId).count
|
||||
const accountAge = Date.now() - user.created_at
|
||||
const accountAgeMs = Date.now() - user.created_at
|
||||
const accountAge = this._formatAccountAge(accountAgeMs)
|
||||
|
||||
debug(`Stats for user ${userId}: ${lockedInboxesCount} locked inboxes, ${forwardEmailsCount} forward emails`)
|
||||
|
||||
|
|
@ -303,7 +328,10 @@ class UserRepository {
|
|||
forwardEmailsCount,
|
||||
accountAge,
|
||||
createdAt: user.created_at,
|
||||
lastLogin: user.last_login
|
||||
lastLogin: user.last_login,
|
||||
maxLockedInboxes: config.maxLockedInboxes || 5,
|
||||
maxForwardEmails: config.maxForwardEmails || 5,
|
||||
lockReleaseHours: config.lockReleaseHours || 720
|
||||
}
|
||||
} catch (error) {
|
||||
debug(`Error getting user stats: ${error.message}`)
|
||||
|
|
@ -311,6 +339,21 @@ class UserRepository {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format account age in human-readable format
|
||||
* @param {number} ms - Milliseconds since account creation
|
||||
* @returns {string} - Formatted age
|
||||
* @private
|
||||
*/
|
||||
_formatAccountAge(ms) {
|
||||
const days = Math.floor(ms / (1000 * 60 * 60 * 24))
|
||||
if (days === 0) return 'Today'
|
||||
if (days === 1) return '1 day'
|
||||
if (days < 30) return `${days} days`
|
||||
if (days < 365) return `${Math.floor(days / 30)} months`
|
||||
return `${Math.floor(days / 365)} years`
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ function requireAuth(req, res, next) {
|
|||
}
|
||||
|
||||
// User is not authenticated
|
||||
debug('Unauthenticated request, redirecting to login')
|
||||
debug('Unauthenticated request, redirecting to auth page')
|
||||
|
||||
// Store the original URL to redirect back after login
|
||||
req.session.redirectAfterLogin = req.originalUrl
|
||||
|
||||
// Redirect to login
|
||||
return res.redirect('/login')
|
||||
// Redirect to auth page
|
||||
return res.redirect('/auth')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
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
|
||||
|
||||
if (!address || !inboxLock) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const isLocked = inboxLock.isLocked(address)
|
||||
const hasAccess = req.session && req.session.lockedInbox === address.toLowerCase()
|
||||
|
||||
// For authenticated users, check database ownership
|
||||
// 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())
|
||||
|
||||
// Block access to locked inbox without proper authentication
|
||||
if (isLocked && !hasAccess) {
|
||||
|
|
@ -19,20 +26,19 @@ function checkLockAccess(req, res, next) {
|
|||
purgeTime: require('../../../application/helper').prototype.purgeTimeElemetBuilder(),
|
||||
address: address,
|
||||
count: count,
|
||||
message: 'This inbox is locked. Please unlock it to access.',
|
||||
message: 'This inbox is locked by another user. Only the owner can access it.',
|
||||
branding: req.app.get('config').http.branding,
|
||||
showUnlockButton: true,
|
||||
unlockError: unlockError,
|
||||
redirectTo: req.originalUrl
|
||||
currentUser: req.session ? .username,
|
||||
authEnabled: req.app.get('config').user.authEnabled
|
||||
})
|
||||
}
|
||||
|
||||
// Update last access if they have access
|
||||
if (isLocked && hasAccess) {
|
||||
inboxLock.updateAccess(address)
|
||||
// Update last access if they have access and are authenticated
|
||||
if (isLocked && hasAccess && isAuthenticated && userId) {
|
||||
inboxLock.updateAccess(userId, address)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = { checkLockAccess }
|
||||
module.exports = { checkLockAccess }
|
||||
|
|
|
|||
|
|
@ -114,10 +114,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const closeLock = document.getElementById('closeLock');
|
||||
const lockForm = document.querySelector('#lockModal form');
|
||||
|
||||
const unlockModal = document.getElementById('unlockModal');
|
||||
const unlockBtn = document.getElementById('unlockBtn');
|
||||
const closeUnlock = document.getElementById('closeUnlock');
|
||||
|
||||
const removeLockModal = document.getElementById('removeLockModal');
|
||||
const removeLockBtn = document.getElementById('removeLockBtn');
|
||||
const closeRemoveLock = document.getElementById('closeRemoveLock');
|
||||
|
|
@ -136,35 +132,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
closeLock.onclick = function() { closeModal(lockModal); };
|
||||
}
|
||||
|
||||
if (lockForm) {
|
||||
lockForm.addEventListener('submit', function(e) {
|
||||
const pwElement = document.getElementById('lockPassword');
|
||||
const cfElement = document.getElementById('lockConfirm');
|
||||
const pw = pwElement ? pwElement.value : '';
|
||||
const cf = cfElement ? cfElement.value : '';
|
||||
const err = document.getElementById('lockErrorInline');
|
||||
const serverErr = document.getElementById('lockServerError');
|
||||
if (serverErr) serverErr.style.display = 'none';
|
||||
|
||||
if (pw !== cf) {
|
||||
e.preventDefault();
|
||||
if (err) {
|
||||
err.textContent = 'Passwords do not match.';
|
||||
err.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pw.length < 8) {
|
||||
e.preventDefault();
|
||||
if (err) {
|
||||
err.textContent = 'Password must be at least 8 characters.';
|
||||
err.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (err) err.style.display = 'none';
|
||||
});
|
||||
}
|
||||
// Lock form no longer needs password validation - authentication-based locking
|
||||
|
||||
if (lockModal) {
|
||||
const lockErrorValue = (lockModal.dataset.lockError || '').trim();
|
||||
|
|
@ -176,8 +144,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
if (err) {
|
||||
if (lockErrorValue === 'locking_disabled_for_example') {
|
||||
err.textContent = 'Locking is disabled for the example inbox.';
|
||||
} else if (lockErrorValue === 'invalid_password') {
|
||||
err.textContent = 'Please provide a valid password.';
|
||||
} else if (lockErrorValue === 'max_locked_inboxes') {
|
||||
err.textContent = 'You have reached the maximum of 5 locked inboxes.';
|
||||
} else if (lockErrorValue === 'already_locked') {
|
||||
err.textContent = 'This inbox is already locked by another user.';
|
||||
} else if (lockErrorValue === 'server_error') {
|
||||
err.textContent = 'A server error occurred. Please try again.';
|
||||
} else if (lockErrorValue === 'remove_failed') {
|
||||
|
|
@ -190,20 +160,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (unlockBtn) {
|
||||
unlockBtn.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
openModal(unlockModal);
|
||||
};
|
||||
}
|
||||
if (closeUnlock) {
|
||||
closeUnlock.onclick = function() { closeModal(unlockModal); };
|
||||
}
|
||||
if (unlockModal) {
|
||||
const unlockErrorValue = (unlockModal.dataset.unlockError || '').trim();
|
||||
if (unlockErrorValue) { openModal(unlockModal); }
|
||||
}
|
||||
|
||||
if (removeLockBtn) {
|
||||
removeLockBtn.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
|
|
@ -219,7 +175,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
window.onclick = function(e) {
|
||||
if (e.target === lockModal) closeModal(lockModal);
|
||||
if (e.target === unlockModal) closeModal(unlockModal);
|
||||
if (e.target === removeLockModal) closeModal(removeLockModal);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,6 +297,122 @@ text-muted {
|
|||
}
|
||||
|
||||
|
||||
/* Action Dropdowns */
|
||||
|
||||
.action-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
/* Invisible hover area to keep dropdown open */
|
||||
|
||||
.action-dropdown::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: calc(100% + 8px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.action-dropdown:hover::after {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
height: 42px;
|
||||
padding: 0px 24px;
|
||||
border-radius: 15px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid var(--overlay-white-15);
|
||||
background: transparent;
|
||||
color: var(--color-text-light);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-toggle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, var(--overlay-white-10), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.dropdown-toggle:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.dropdown-toggle:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px var(--overlay-purple-40);
|
||||
border-color: var(--overlay-purple-30);
|
||||
color: var(--color-text-white);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 180px;
|
||||
background: var(--color-bg-dark);
|
||||
border: 1px solid var(--overlay-purple-30);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 25px var(--overlay-purple-40);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.action-dropdown:hover .dropdown-menu,
|
||||
.action-dropdown:focus-within .dropdown-menu,
|
||||
.dropdown-menu:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-menu a {
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
color: var(--color-text-light);
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
height: auto;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-menu a::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-menu a:hover {
|
||||
background: var(--overlay-purple-30);
|
||||
color: var(--color-text-white);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dropdown-menu a:not(:last-child) {
|
||||
border-bottom: 1px solid var(--overlay-white-10);
|
||||
}
|
||||
|
||||
|
||||
/* Auth pages */
|
||||
|
||||
.auth-container {
|
||||
|
|
@ -366,7 +482,6 @@ text-muted {
|
|||
background: var(--color-accent-purple);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
|
@ -431,6 +546,95 @@ text-muted {
|
|||
}
|
||||
|
||||
|
||||
/* Unified auth page (side-by-side login/register) */
|
||||
|
||||
.auth-unified-container {
|
||||
max-width: 1100px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.auth-intro {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.auth-intro h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-accent-purple);
|
||||
}
|
||||
|
||||
.auth-forms-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.auth-forms-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-card h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-accent-purple);
|
||||
}
|
||||
|
||||
.auth-card-subtitle {
|
||||
color: var(--color-text-gray);
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.auth-features-unified {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: var(--color-bg-dark);
|
||||
border: 1px solid var(--color-border-dark);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.auth-features-unified h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--color-accent-purple);
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
padding: 1rem;
|
||||
background: var(--color-bg-medium);
|
||||
border-radius: 4px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.guest-note {
|
||||
color: var(--color-text-gray);
|
||||
font-size: 1.3rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.guest-note a {
|
||||
color: var(--color-accent-purple);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/* Verification success page */
|
||||
|
||||
.verification-success-container {
|
||||
|
|
@ -522,6 +726,237 @@ text-muted {
|
|||
}
|
||||
|
||||
|
||||
/* Account dashboard page */
|
||||
|
||||
.account-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.account-subtitle {
|
||||
color: var(--color-text-gray);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.account-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.account-card {
|
||||
background: var(--color-bg-dark);
|
||||
border: 1px solid var(--color-border-dark);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.account-card h2 {
|
||||
color: var(--color-accent-purple);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: var(--color-text-gray);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.account-stats {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: var(--overlay-purple-08);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--overlay-purple-15);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-accent-purple);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--color-text-gray);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.email-list,
|
||||
.inbox-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.email-item,
|
||||
.inbox-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--overlay-white-03);
|
||||
border: 1px solid var(--color-border-dark);
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.email-item:hover,
|
||||
.inbox-item:hover {
|
||||
background: var(--overlay-white-05);
|
||||
}
|
||||
|
||||
.email-info,
|
||||
.inbox-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.email-address {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-light);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.inbox-address {
|
||||
font-weight: 600;
|
||||
color: var(--color-accent-purple);
|
||||
text-decoration: none;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.inbox-address:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.email-meta,
|
||||
.inbox-meta {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-gray);
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.button-small {
|
||||
padding: 0rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.button-danger {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button-danger:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: var(--color-text-gray);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--color-text-gray);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.limit-reached {
|
||||
color: var(--color-warning);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.account-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.email-item,
|
||||
.inbox-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
.inline-form {
|
||||
width: 100%;
|
||||
}
|
||||
.button-small {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Forward modal auth prompts */
|
||||
|
||||
.auth-required {
|
||||
background: var(--overlay-warning-10);
|
||||
border-left: 3px solid var(--color-warning);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.auth-prompt {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.auth-prompt .button {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
color: var(--color-text-gray);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.form-hint a {
|
||||
color: var(--color-accent-purple);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.form-hint a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal-info {
|
||||
color: var(--color-text-gray);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
|
||||
/* Reset apple form styles */
|
||||
|
||||
input,
|
||||
|
|
@ -1105,7 +1540,7 @@ label {
|
|||
|
||||
.attachment-link {
|
||||
color: var(--color-accent-purple-light);
|
||||
padding: 12px 16px;
|
||||
padding: 0px 16px;
|
||||
background: var(--overlay-purple-10);
|
||||
border: 1px solid var(--overlay-purple-20);
|
||||
transition: all 0.3s ease;
|
||||
|
|
@ -1311,7 +1746,7 @@ label {
|
|||
}
|
||||
|
||||
.raw-tab-button {
|
||||
padding: 8px 14px;
|
||||
padding: 0px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--overlay-white-12);
|
||||
background: var(--overlay-white-05);
|
||||
|
|
|
|||
228
infrastructure/web/routes/account.js
Normal file
228
infrastructure/web/routes/account.js
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
// Account management routes for registered users
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const { requireAuth } = require('../middleware/auth')
|
||||
const { body, validationResult } = require('express-validator')
|
||||
|
||||
// GET /account - Account dashboard
|
||||
router.get('/account', requireAuth, async(req, res) => {
|
||||
try {
|
||||
const userRepository = req.app.get('userRepository')
|
||||
const inboxLock = req.app.get('inboxLock')
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
const Helper = require('../../../application/helper')
|
||||
const helper = new Helper()
|
||||
|
||||
// Get user's verified forwarding emails
|
||||
const forwardEmails = userRepository.getForwardEmails(req.session.userId)
|
||||
|
||||
// Get user's locked inboxes (if locking is available)
|
||||
let lockedInboxes = []
|
||||
if (inboxLock) {
|
||||
lockedInboxes = inboxLock.getUserLockedInboxes(req.session.userId)
|
||||
}
|
||||
|
||||
// Get user stats
|
||||
const config = req.app.get('config')
|
||||
const stats = userRepository.getUserStats(req.session.userId, config.user)
|
||||
|
||||
// Get mail count for footer
|
||||
const count = await mailProcessingService.getCount()
|
||||
const imapService = req.app.locals.imapService
|
||||
const largestUid = await imapService.getLargestUid()
|
||||
const totalcount = helper.countElementBuilder(count, largestUid)
|
||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||
|
||||
res.render('account', {
|
||||
title: 'Account Dashboard',
|
||||
username: req.session.username,
|
||||
forwardEmails,
|
||||
lockedInboxes,
|
||||
stats,
|
||||
branding: config.http.branding,
|
||||
purgeTime: purgeTime,
|
||||
totalcount: totalcount,
|
||||
successMessage: req.session.accountSuccess,
|
||||
errorMessage: req.session.accountError
|
||||
})
|
||||
|
||||
// Clear flash messages
|
||||
delete req.session.accountSuccess
|
||||
delete req.session.accountError
|
||||
} catch (error) {
|
||||
console.error('Account page error:', error)
|
||||
res.status(500).render('error', {
|
||||
message: 'Failed to load account page',
|
||||
error: error
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// POST /account/forward-email/add - Add forwarding email (triggers verification)
|
||||
router.post('/account/forward-email/add',
|
||||
requireAuth, [
|
||||
body('email').isEmail().normalizeEmail().withMessage('Invalid email address')
|
||||
],
|
||||
async(req, res) => {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
req.session.accountError = errors.array()[0].msg
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
try {
|
||||
const userRepository = req.app.get('userRepository')
|
||||
const smtpService = req.app.get('smtpService')
|
||||
const verificationStore = req.app.get('verificationStore')
|
||||
const config = req.app.get('config')
|
||||
const crypto = require('crypto')
|
||||
const { email } = req.body
|
||||
|
||||
// Check if already verified
|
||||
if (userRepository.hasForwardEmail(req.session.userId, email)) {
|
||||
req.session.accountError = 'This email is already verified on your account'
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
// Check limit
|
||||
const emailCount = userRepository.getForwardEmailCount(req.session.userId)
|
||||
if (emailCount >= config.user.maxForwardEmails) {
|
||||
req.session.accountError = `Maximum ${config.user.maxForwardEmails} forwarding emails allowed`
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
// Generate verification token
|
||||
const token = crypto.randomBytes(32).toString('hex')
|
||||
verificationStore.createVerification(token, email, {
|
||||
userId: req.session.userId
|
||||
})
|
||||
|
||||
// Send verification email
|
||||
const baseUrl = config.http.baseUrl || 'http://localhost:3000'
|
||||
const branding = config.http.branding[0] || '48hr.email'
|
||||
|
||||
await smtpService.sendVerificationEmail(
|
||||
email,
|
||||
token,
|
||||
baseUrl,
|
||||
branding,
|
||||
'/account/verify'
|
||||
)
|
||||
|
||||
req.session.accountSuccess = `Verification email sent to ${email}. Check your inbox!`
|
||||
res.redirect('/account')
|
||||
} catch (error) {
|
||||
console.error('Add forward email error:', error)
|
||||
req.session.accountError = 'Failed to send verification email. Please try again.'
|
||||
res.redirect('/account')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// GET /account/verify - Verify forwarding email
|
||||
router.get('/account/verify', requireAuth, async(req, res) => {
|
||||
const { token } = req.query
|
||||
|
||||
if (!token) {
|
||||
req.session.accountError = 'Invalid verification link'
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
try {
|
||||
const verificationStore = req.app.get('verificationStore')
|
||||
const userRepository = req.app.get('userRepository')
|
||||
|
||||
const verification = verificationStore.verifyToken(token)
|
||||
|
||||
if (!verification) {
|
||||
req.session.accountError = 'Verification link expired or invalid'
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
// Check if token belongs to this user
|
||||
if (verification.metadata.userId !== req.session.userId) {
|
||||
req.session.accountError = 'This verification link belongs to another account'
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
// Add email to user's verified emails
|
||||
userRepository.addForwardEmail(req.session.userId, verification.destinationEmail)
|
||||
|
||||
req.session.accountSuccess = `Successfully verified ${verification.destinationEmail}!`
|
||||
res.redirect('/account')
|
||||
} catch (error) {
|
||||
console.error('Email verification error:', error)
|
||||
req.session.accountError = 'Failed to verify email. Please try again.'
|
||||
res.redirect('/account')
|
||||
}
|
||||
})
|
||||
|
||||
// POST /account/forward-email/remove - Remove forwarding email
|
||||
router.post('/account/forward-email/remove',
|
||||
requireAuth, [
|
||||
body('email').isEmail().normalizeEmail().withMessage('Invalid email address')
|
||||
],
|
||||
async(req, res) => {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
req.session.accountError = errors.array()[0].msg
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
try {
|
||||
const userRepository = req.app.get('userRepository')
|
||||
const { email } = req.body
|
||||
|
||||
userRepository.removeForwardEmail(req.session.userId, email)
|
||||
|
||||
req.session.accountSuccess = `Removed ${email} from your account`
|
||||
res.redirect('/account')
|
||||
} catch (error) {
|
||||
console.error('Remove forward email error:', error)
|
||||
req.session.accountError = 'Failed to remove email. Please try again.'
|
||||
res.redirect('/account')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// POST /account/locked-inbox/release - Release a locked inbox
|
||||
router.post('/account/locked-inbox/release',
|
||||
requireAuth, [
|
||||
body('address').notEmpty().withMessage('Inbox address is required')
|
||||
],
|
||||
async(req, res) => {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
req.session.accountError = errors.array()[0].msg
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
try {
|
||||
const inboxLock = req.app.get('inboxLock')
|
||||
const { address } = req.body
|
||||
|
||||
if (!inboxLock) {
|
||||
req.session.accountError = 'Inbox locking is not available'
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
// Check if user owns this locked inbox
|
||||
if (!inboxLock.isLockedByUser(address, req.session.userId)) {
|
||||
req.session.accountError = 'You do not own this locked inbox'
|
||||
return res.redirect('/account')
|
||||
}
|
||||
|
||||
// Release the lock
|
||||
inboxLock.release(req.session.userId, address)
|
||||
|
||||
req.session.accountSuccess = `Released lock on ${address}`
|
||||
res.redirect('/account')
|
||||
} catch (error) {
|
||||
console.error('Release inbox error:', error)
|
||||
req.session.accountError = 'Failed to release inbox. Please try again.'
|
||||
res.redirect('/account')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
|
|
@ -74,8 +74,8 @@ const loginRateLimiter = (req, res, next) => {
|
|||
next()
|
||||
}
|
||||
|
||||
// GET /register - Show registration form
|
||||
router.get('/register', redirectIfAuthenticated, (req, res) => {
|
||||
// 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
|
||||
|
|
@ -84,8 +84,8 @@ router.get('/register', redirectIfAuthenticated, (req, res) => {
|
|||
delete req.session.errorMessage
|
||||
delete req.session.successMessage
|
||||
|
||||
res.render('register', {
|
||||
title: `Register | ${config.http.branding[0]}`,
|
||||
res.render('login-auth', {
|
||||
title: `Login or Register | ${config.http.branding[0]}`,
|
||||
branding: config.http.branding,
|
||||
errorMessage,
|
||||
successMessage
|
||||
|
|
@ -106,7 +106,7 @@ router.post('/register',
|
|||
const firstError = errors.array()[0].msg
|
||||
debug(`Registration validation failed: ${firstError}`)
|
||||
req.session.errorMessage = firstError
|
||||
return res.redirect('/register')
|
||||
return res.redirect('/auth')
|
||||
}
|
||||
|
||||
const { username, password, confirmPassword } = req.body
|
||||
|
|
@ -115,7 +115,7 @@ router.post('/register',
|
|||
if (password !== confirmPassword) {
|
||||
debug('Registration failed: Passwords do not match')
|
||||
req.session.errorMessage = 'Passwords do not match'
|
||||
return res.redirect('/register')
|
||||
return res.redirect('/auth')
|
||||
}
|
||||
|
||||
const authService = req.app.get('authService')
|
||||
|
|
@ -124,39 +124,21 @@ router.post('/register',
|
|||
if (result.success) {
|
||||
debug(`User registered successfully: ${username}`)
|
||||
req.session.successMessage = 'Registration successful! Please log in.'
|
||||
return res.redirect('/login')
|
||||
return res.redirect('/auth')
|
||||
} else {
|
||||
debug(`Registration failed: ${result.error}`)
|
||||
req.session.errorMessage = result.error
|
||||
return res.redirect('/register')
|
||||
return res.redirect('/auth')
|
||||
}
|
||||
} catch (error) {
|
||||
debug(`Registration error: ${error.message}`)
|
||||
console.error('Error during registration', error)
|
||||
req.session.errorMessage = 'An unexpected error occurred. Please try again.'
|
||||
res.redirect('/register')
|
||||
res.redirect('/auth')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// GET /login - Show login form
|
||||
router.get('/login', 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('login-auth', {
|
||||
title: `Login | ${config.http.branding[0]}`,
|
||||
branding: config.http.branding,
|
||||
errorMessage,
|
||||
successMessage
|
||||
})
|
||||
})
|
||||
|
||||
// POST /login - Process login
|
||||
router.post('/login',
|
||||
redirectIfAuthenticated,
|
||||
|
|
@ -170,7 +152,7 @@ router.post('/login',
|
|||
const firstError = errors.array()[0].msg
|
||||
debug(`Login validation failed: ${firstError}`)
|
||||
req.session.errorMessage = firstError
|
||||
return res.redirect('/login')
|
||||
return res.redirect('/auth')
|
||||
}
|
||||
|
||||
const { username, password } = req.body
|
||||
|
|
@ -187,7 +169,7 @@ router.post('/login',
|
|||
if (err) {
|
||||
debug(`Session regeneration error: ${err.message}`)
|
||||
req.session.errorMessage = 'Login failed. Please try again.'
|
||||
return res.redirect('/login')
|
||||
return res.redirect('/auth')
|
||||
}
|
||||
|
||||
// Set session data
|
||||
|
|
@ -200,7 +182,7 @@ router.post('/login',
|
|||
if (err) {
|
||||
debug(`Session save error: ${err.message}`)
|
||||
req.session.errorMessage = 'Login failed. Please try again.'
|
||||
return res.redirect('/login')
|
||||
return res.redirect('/auth')
|
||||
}
|
||||
|
||||
debug(`Session created for user: ${username}`)
|
||||
|
|
@ -210,13 +192,13 @@ router.post('/login',
|
|||
} else {
|
||||
debug(`Login failed: ${result.error}`)
|
||||
req.session.errorMessage = result.error
|
||||
return res.redirect('/login')
|
||||
return res.redirect('/auth')
|
||||
}
|
||||
} catch (error) {
|
||||
debug(`Login error: ${error.message}`)
|
||||
console.error('Error during login', error)
|
||||
req.session.errorMessage = 'An unexpected error occurred. Please try again.'
|
||||
res.redirect('/login')
|
||||
res.redirect('/auth')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const CryptoDetector = require('../../../application/crypto-detector')
|
|||
const helper = new(Helper)
|
||||
const cryptoDetector = new CryptoDetector()
|
||||
const { checkLockAccess } = require('../middleware/lock')
|
||||
const { requireAuth, optionalAuth } = require('../middleware/auth')
|
||||
|
||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||
|
||||
|
|
@ -97,7 +98,7 @@ const validateForwardRequest = [
|
|||
})
|
||||
]
|
||||
|
||||
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLockAccess, async(req, res, next) => {
|
||||
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, optionalAuth, checkLockAccess, async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
if (!mailProcessingService) {
|
||||
|
|
@ -109,8 +110,25 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLo
|
|||
const largestUid = await req.app.locals.imapService.getLargestUid()
|
||||
const totalcount = helper.countElementBuilder(count, largestUid)
|
||||
debug(`Rendering inbox with ${count} total mails`)
|
||||
|
||||
// Check lock status
|
||||
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
|
||||
const hasAccess = req.session && req.session.lockedInbox === req.params.address
|
||||
const userId = req.session ? .userId
|
||||
const isAuthenticated = 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)
|
||||
|
||||
// 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
|
||||
|
|
@ -138,6 +156,8 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLo
|
|||
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
|
||||
branding: config.http.branding,
|
||||
authEnabled: config.user.authEnabled,
|
||||
isAuthenticated: req.session && req.session.userId ? true : false,
|
||||
userForwardEmails: userForwardEmails,
|
||||
isLocked: isLocked,
|
||||
hasAccess: hasAccess,
|
||||
unlockError: unlockErrorSession,
|
||||
|
|
@ -163,6 +183,7 @@ router.get(
|
|||
'^/:address/:uid([0-9]+)',
|
||||
sanitizeAddress,
|
||||
validateDomain,
|
||||
optionalAuth,
|
||||
checkLockAccess,
|
||||
async(req, res, next) => {
|
||||
try {
|
||||
|
|
@ -190,7 +211,22 @@ router.get(
|
|||
|
||||
const inboxLock = req.app.get('inboxLock')
|
||||
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
|
||||
const hasAccess = req.session && req.session.lockedInbox === req.params.address
|
||||
const userId = req.session ? .userId
|
||||
const isAuthenticated = 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)
|
||||
|
||||
// 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
|
||||
|
|
@ -217,6 +253,8 @@ router.get(
|
|||
uid: req.params.uid,
|
||||
branding: config.http.branding,
|
||||
authEnabled: config.user.authEnabled,
|
||||
isAuthenticated: req.session && req.session.userId ? true : false,
|
||||
userForwardEmails: userForwardEmails,
|
||||
isLocked: isLocked,
|
||||
hasAccess: hasAccess,
|
||||
errorMessage: errorMessage,
|
||||
|
|
@ -418,9 +456,10 @@ router.get(
|
|||
}
|
||||
)
|
||||
|
||||
// POST route for forwarding a single email
|
||||
// POST route for forwarding a single email (requires authentication)
|
||||
router.post(
|
||||
'^/:address/:uid/forward',
|
||||
requireAuth,
|
||||
forwardLimiter,
|
||||
validateDomain,
|
||||
checkLockAccess,
|
||||
|
|
@ -436,51 +475,36 @@ router.post(
|
|||
}
|
||||
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
const userRepository = req.app.get('userRepository')
|
||||
const { destinationEmail } = req.body
|
||||
const uid = parseInt(req.params.uid, 10)
|
||||
|
||||
// Check if destination email is verified via signed cookie
|
||||
const verifiedEmail = req.signedCookies.verified_email
|
||||
// Check if destination email is in user's verified emails
|
||||
const userEmails = userRepository.getForwardEmails(req.session.userId)
|
||||
const isVerified = userEmails.some(e => e.email.toLowerCase() === destinationEmail.toLowerCase())
|
||||
|
||||
if (verifiedEmail && verifiedEmail.toLowerCase() === destinationEmail.toLowerCase()) {
|
||||
// Email is verified, proceed with forwarding
|
||||
debug(`Forwarding email ${uid} from ${req.params.address} to ${destinationEmail} (verified)`)
|
||||
if (!isVerified) {
|
||||
debug(`Email ${destinationEmail} not in user's verified emails`)
|
||||
req.session.errorMessage = 'Please select a verified email address from your account'
|
||||
return res.redirect(`/inbox/${req.params.address}/${req.params.uid}`)
|
||||
}
|
||||
|
||||
const result = await mailProcessingService.forwardEmail(
|
||||
req.params.address,
|
||||
uid,
|
||||
destinationEmail
|
||||
)
|
||||
// Email is verified, proceed with forwarding
|
||||
debug(`Forwarding email ${uid} from ${req.params.address} to ${destinationEmail} (user verified)`)
|
||||
|
||||
if (result.success) {
|
||||
debug(`Email ${uid} forwarded successfully to ${destinationEmail}`)
|
||||
return res.redirect(`/inbox/${req.params.address}/${uid}?forwarded=true`)
|
||||
} else {
|
||||
debug(`Failed to forward email ${uid}: ${result.error}`)
|
||||
req.session.errorMessage = result.error
|
||||
return res.redirect(`/inbox/${req.params.address}/${uid}`)
|
||||
}
|
||||
const result = await mailProcessingService.forwardEmail(
|
||||
req.params.address,
|
||||
uid,
|
||||
destinationEmail
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
debug(`Email ${uid} forwarded successfully to ${destinationEmail}`)
|
||||
return res.redirect(`/inbox/${req.params.address}/${uid}?forwarded=true`)
|
||||
} else {
|
||||
// Email not verified, initiate verification flow
|
||||
debug(`Email ${destinationEmail} not verified, initiating verification`)
|
||||
|
||||
const verificationResult = await mailProcessingService.initiateForwardVerification(
|
||||
req.params.address,
|
||||
destinationEmail, [uid]
|
||||
)
|
||||
|
||||
if (verificationResult.success) {
|
||||
debug(`Verification email sent to ${destinationEmail}`)
|
||||
return res.redirect(`/inbox/${req.params.address}/${uid}?verificationSent=true&email=${encodeURIComponent(destinationEmail)}`)
|
||||
} else if (verificationResult.cooldownSeconds) {
|
||||
debug(`Verification rate limited for ${destinationEmail}`)
|
||||
req.session.errorMessage = verificationResult.error
|
||||
return res.redirect(`/inbox/${req.params.address}/${uid}`)
|
||||
} else {
|
||||
debug(`Failed to send verification email: ${verificationResult.error}`)
|
||||
req.session.errorMessage = verificationResult.error || 'Failed to send verification email'
|
||||
return res.redirect(`/inbox/${req.params.address}/${uid}`)
|
||||
}
|
||||
debug(`Failed to forward email ${uid}: ${result.error}`)
|
||||
req.session.errorMessage = result.error
|
||||
return res.redirect(`/inbox/${req.params.address}/${uid}`)
|
||||
}
|
||||
} catch (error) {
|
||||
debug(`Error forwarding email ${req.params.uid}: ${error.message}`)
|
||||
|
|
@ -491,9 +515,10 @@ router.post(
|
|||
}
|
||||
)
|
||||
|
||||
// POST route for forwarding all emails in an inbox
|
||||
// POST route for forwarding all emails in an inbox (requires authentication)
|
||||
router.post(
|
||||
'^/:address/forward-all',
|
||||
requireAuth,
|
||||
forwardLimiter,
|
||||
validateDomain,
|
||||
checkLockAccess,
|
||||
|
|
@ -509,40 +534,21 @@ router.post(
|
|||
}
|
||||
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
const userRepository = req.app.get('userRepository')
|
||||
const { destinationEmail } = req.body
|
||||
|
||||
// Check if destination email is verified via signed cookie
|
||||
const verifiedEmail = req.signedCookies.verified_email
|
||||
// Check if destination email is in user's verified emails
|
||||
const userEmails = userRepository.getForwardEmails(req.session.userId)
|
||||
const isVerified = userEmails.some(e => e.email.toLowerCase() === destinationEmail.toLowerCase())
|
||||
|
||||
if (!verifiedEmail || verifiedEmail.toLowerCase() !== destinationEmail.toLowerCase()) {
|
||||
// Email not verified, initiate verification flow
|
||||
debug(`Email ${destinationEmail} not verified, initiating verification for forward-all`)
|
||||
|
||||
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
|
||||
const uids = mailSummaries.map(m => m.uid)
|
||||
|
||||
const verificationResult = await mailProcessingService.initiateForwardVerification(
|
||||
req.params.address,
|
||||
destinationEmail,
|
||||
uids
|
||||
)
|
||||
|
||||
if (verificationResult.success) {
|
||||
debug(`Verification email sent to ${destinationEmail}`)
|
||||
return res.redirect(`/inbox/${req.params.address}?verificationSent=true&email=${encodeURIComponent(destinationEmail)}`)
|
||||
} else if (verificationResult.cooldownSeconds) {
|
||||
debug(`Verification rate limited for ${destinationEmail}`)
|
||||
req.session.errorMessage = verificationResult.error
|
||||
return res.redirect(`/inbox/${req.params.address}`)
|
||||
} else {
|
||||
debug(`Failed to send verification email: ${verificationResult.error}`)
|
||||
req.session.errorMessage = verificationResult.error || 'Failed to send verification email'
|
||||
return res.redirect(`/inbox/${req.params.address}`)
|
||||
}
|
||||
if (!isVerified) {
|
||||
debug(`Email ${destinationEmail} not in user's verified emails`)
|
||||
req.session.errorMessage = 'Please select a verified email address from your account'
|
||||
return res.redirect(`/inbox/${req.params.address}`)
|
||||
}
|
||||
|
||||
// Email is verified, proceed with bulk forwarding
|
||||
debug(`Forwarding all emails from ${req.params.address} to ${destinationEmail} (verified)`)
|
||||
debug(`Forwarding all emails from ${req.params.address} to ${destinationEmail} (user verified)`)
|
||||
|
||||
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const debug = require('debug')('48hr-email:lock')
|
||||
const { requireAuth } = require('../middleware/auth')
|
||||
|
||||
router.post('/lock', async(req, res) => {
|
||||
const { address, password } = req.body
|
||||
debug(`Lock attempt for inbox: ${address}`);
|
||||
router.post('/lock', requireAuth, async(req, res) => {
|
||||
const { address } = req.body
|
||||
const userId = req.session.userId
|
||||
debug(`Lock attempt for inbox: ${address} by user ${userId}`)
|
||||
|
||||
if (!address || !password || password.length < 8) {
|
||||
debug(`Lock error for ${address}: invalid input`);
|
||||
if (!address) {
|
||||
debug(`Lock error for ${address}: missing address`)
|
||||
if (req.session) req.session.lockError = 'invalid'
|
||||
return res.redirect(`/inbox/${address}`)
|
||||
}
|
||||
|
|
@ -17,58 +19,88 @@ router.post('/lock', async(req, res) => {
|
|||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
const config = req.app.get('config')
|
||||
|
||||
// Prevent locking the example inbox; allow UI but block DB insert
|
||||
if (!inboxLock) {
|
||||
debug('Lock error: inboxLock service not available')
|
||||
if (req.session) req.session.lockError = 'service_unavailable'
|
||||
return res.redirect(`/inbox/${address}`)
|
||||
}
|
||||
|
||||
// Prevent locking the example inbox
|
||||
if (config && config.email && config.email.examples && config.email.examples.account && address.toLowerCase() === config.email.examples.account.toLowerCase()) {
|
||||
debug(`Lock error for ${address}: locking disabled for example inbox`);
|
||||
debug(`Lock error for ${address}: locking disabled for example inbox`)
|
||||
if (req.session) req.session.lockError = 'locking_disabled_for_example'
|
||||
return res.redirect(`/inbox/${address}`)
|
||||
}
|
||||
|
||||
await inboxLock.lock(address, password)
|
||||
debug(`Inbox locked: ${address}`);
|
||||
// Check if user can lock more inboxes (5 max)
|
||||
if (!inboxLock.canLockMore(userId)) {
|
||||
debug(`Lock error for ${address}: user ${userId} has reached 5-inbox limit`)
|
||||
if (req.session) req.session.lockError = 'max_locked_inboxes'
|
||||
return res.redirect(`/inbox/${address}`)
|
||||
}
|
||||
|
||||
await inboxLock.lock(userId, address)
|
||||
debug(`Inbox locked: ${address} by user ${userId}`)
|
||||
|
||||
// Clear cache for this inbox
|
||||
if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) {
|
||||
debug(`Clearing lock cache for: ${address}`);
|
||||
debug(`Clearing lock cache for: ${address}`)
|
||||
mailProcessingService.cachedFetchFullMail.clear()
|
||||
}
|
||||
|
||||
// Store in session for immediate access
|
||||
req.session.lockedInbox = address
|
||||
res.redirect(`/inbox/${address}`)
|
||||
} catch (error) {
|
||||
debug(`Lock error for ${address}: ${error.message}`);
|
||||
debug(`Lock error for ${address}: ${error.message}`)
|
||||
console.error('Lock error:', error)
|
||||
if (req.session) req.session.lockError = 'server_error'
|
||||
if (req.session) {
|
||||
if (error.message.includes('already locked')) {
|
||||
req.session.lockError = 'already_locked'
|
||||
} else if (error.message.includes('maximum')) {
|
||||
req.session.lockError = 'max_locked_inboxes'
|
||||
} else {
|
||||
req.session.lockError = 'server_error'
|
||||
}
|
||||
}
|
||||
res.redirect(`/inbox/${address}`)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/unlock', async(req, res) => {
|
||||
const { address, password, redirectTo } = req.body
|
||||
router.post('/unlock', requireAuth, async(req, res) => {
|
||||
const { address, redirectTo } = req.body
|
||||
const userId = req.session.userId
|
||||
const destination = redirectTo && redirectTo.startsWith('/') ? redirectTo : `/inbox/${address}`
|
||||
debug(`Unlock attempt for inbox: ${address}`);
|
||||
debug(`Unlock attempt for inbox: ${address} by user ${userId}`)
|
||||
|
||||
if (!address || !password) {
|
||||
debug(`Unlock error for ${address}: missing fields`);
|
||||
if (!address) {
|
||||
debug(`Unlock error for ${address}: missing address`)
|
||||
if (req.session) req.session.unlockError = 'missing_fields'
|
||||
return res.redirect(destination)
|
||||
}
|
||||
|
||||
try {
|
||||
const inboxLock = req.app.get('inboxLock')
|
||||
const inbox = await inboxLock.unlock(address, password)
|
||||
|
||||
if (!inbox) {
|
||||
debug(`Unlock error for ${address}: invalid password`);
|
||||
if (req.session) req.session.unlockError = 'invalid_password'
|
||||
if (!inboxLock) {
|
||||
debug('Unlock error: inboxLock service not available')
|
||||
if (req.session) req.session.unlockError = 'service_unavailable'
|
||||
return res.redirect(destination)
|
||||
}
|
||||
|
||||
debug(`Inbox unlocked: ${address}`);
|
||||
const inbox = await inboxLock.unlock(userId, address)
|
||||
|
||||
if (!inbox) {
|
||||
debug(`Unlock error for ${address}: not owned by user ${userId}`)
|
||||
if (req.session) req.session.unlockError = 'not_your_lock'
|
||||
return res.redirect(destination)
|
||||
}
|
||||
|
||||
debug(`Inbox ${address} unlocked by user ${userId}`)
|
||||
req.session.lockedInbox = address
|
||||
res.redirect(destination)
|
||||
} catch (error) {
|
||||
debug(`Unlock error for ${address}: ${error.message}`);
|
||||
debug(`Unlock error for ${address}: ${error.message}`)
|
||||
console.error('Unlock error:', error)
|
||||
if (req.session) req.session.unlockError = 'server_error'
|
||||
res.redirect(destination)
|
||||
|
|
@ -80,55 +112,62 @@ router.get('/logout', (req, res) => {
|
|||
|
||||
// Clear cache before logout
|
||||
if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) {
|
||||
debug('Clearing lock cache for logout');
|
||||
debug('Clearing lock cache for logout')
|
||||
mailProcessingService.cachedFetchFullMail.clear()
|
||||
}
|
||||
|
||||
debug('Lock session destroyed (logout)');
|
||||
req.session.destroy()
|
||||
debug('Clearing lockedInbox from session (lock logout)')
|
||||
delete req.session.lockedInbox
|
||||
res.redirect('/')
|
||||
})
|
||||
|
||||
router.post('/remove', async(req, res) => {
|
||||
router.post('/remove', requireAuth, async(req, res) => {
|
||||
const { address } = req.body
|
||||
debug(`Remove lock attempt for inbox: ${address}`);
|
||||
const userId = req.session.userId
|
||||
debug(`Remove lock attempt for inbox: ${address} by user ${userId}`)
|
||||
|
||||
if (!address) {
|
||||
debug('Remove lock error: missing address');
|
||||
debug('Remove lock error: missing address')
|
||||
return res.redirect('/')
|
||||
}
|
||||
|
||||
// Check if user has access to this locked inbox
|
||||
const hasAccess = req.session && req.session.lockedInbox === address.toLowerCase()
|
||||
debug(`Lock middleware: ${address} - hasAccess: ${hasAccess}`);
|
||||
|
||||
if (!hasAccess) {
|
||||
debug(`Remove lock error: no access for ${address}`);
|
||||
return res.redirect(`/inbox/${address}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const inboxLock = req.app.get('inboxLock')
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
|
||||
await inboxLock.release(address)
|
||||
debug(`Lock removed for inbox: ${address}`);
|
||||
if (!inboxLock) {
|
||||
debug('Remove lock error: inboxLock service not available')
|
||||
return res.redirect(`/inbox/${address}`)
|
||||
}
|
||||
|
||||
// Verify user owns this lock
|
||||
if (!inboxLock.isLockedByUser(address, userId)) {
|
||||
debug(`Remove lock error: inbox ${address} not owned by user ${userId}`)
|
||||
if (req.session) req.session.lockError = 'not_your_lock'
|
||||
return res.redirect(`/inbox/${address}`)
|
||||
}
|
||||
|
||||
await inboxLock.release(userId, address)
|
||||
debug(`Lock removed for inbox: ${address} by user ${userId}`)
|
||||
|
||||
// Clear cache when removing lock
|
||||
if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) {
|
||||
debug(`Clearing lock cache for: ${address}`);
|
||||
debug(`Clearing lock cache for: ${address}`)
|
||||
mailProcessingService.cachedFetchFullMail.clear()
|
||||
}
|
||||
|
||||
debug('Lock session destroyed (remove)');
|
||||
req.session.destroy()
|
||||
// Clear from session
|
||||
if (req.session.lockedInbox === address.toLowerCase()) {
|
||||
delete req.session.lockedInbox
|
||||
}
|
||||
|
||||
res.redirect(`/inbox/${address}`)
|
||||
} catch (error) {
|
||||
debug(`Remove lock error for ${address}: ${error.message}`);
|
||||
debug(`Remove lock error for ${address}: ${error.message}`)
|
||||
console.error('Remove lock error:', error)
|
||||
if (req.session) req.session.lockError = 'remove_failed'
|
||||
res.redirect(`/inbox/${address}`)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
module.exports = router
|
||||
|
|
|
|||
170
infrastructure/web/views/account.twig
Normal file
170
infrastructure/web/views/account.twig
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block header %}
|
||||
<div class="action-links">
|
||||
<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"/>
|
||||
</svg>
|
||||
<svg class="theme-icon theme-icon-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="account" class="account-container">
|
||||
<h1>Account Dashboard</h1>
|
||||
<p class="account-subtitle">Welcome back, <strong>{{ username }}</strong></p>
|
||||
|
||||
{% if successMessage %}
|
||||
<div class="success-message">
|
||||
{{ successMessage }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if errorMessage %}
|
||||
<div class="unlock-error">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="account-grid">
|
||||
<!-- Account Stats -->
|
||||
<div class="account-card account-stats">
|
||||
<h2>Account Overview</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.forwardEmailsCount }}/{{ stats.maxForwardEmails }}</div>
|
||||
<div class="stat-label">Forward Emails</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.lockedInboxesCount }}/{{ stats.maxLockedInboxes }}</div>
|
||||
<div class="stat-label">Locked Inboxes</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.accountAge }}</div>
|
||||
<div class="stat-label">Account Age</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Forwarding Emails Section -->
|
||||
<div class="account-card">
|
||||
<h2>Forwarding Emails</h2>
|
||||
<p class="card-description">Add verified emails to forward messages to. Each email must be verified before use.</p>
|
||||
|
||||
{% if forwardEmails|length > 0 %}
|
||||
<ul class="email-list">
|
||||
{% for email in forwardEmails %}
|
||||
<li class="email-item">
|
||||
<div class="email-info">
|
||||
<span class="email-address">{{ email.email }}</span>
|
||||
<span class="email-meta">Verified {{ email.verifiedAgo }}</span>
|
||||
</div>
|
||||
<form method="POST" action="/account/forward-email/remove" class="inline-form">
|
||||
<input type="hidden" name="email" value="{{ email.email }}">
|
||||
<button type="submit" class="button button-small button-danger" onclick="return confirm('Remove {{ email.email }}?')">
|
||||
Remove
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="empty-state">No verified forwarding emails yet.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if stats.forwardEmailsCount < stats.maxForwardEmails %}
|
||||
<button class="button button-primary" id="addEmailBtn">Add Email</button>
|
||||
{% else %}
|
||||
<p class="limit-reached">Maximum {{ stats.maxForwardEmails }} emails reached</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
{% if lockedInboxes|length > 0 %}
|
||||
<ul class="inbox-list">
|
||||
{% for inbox in lockedInboxes %}
|
||||
<li class="inbox-item">
|
||||
<div class="inbox-info">
|
||||
<a href="/inbox/{{ inbox.address }}" class="inbox-address">{{ inbox.address }}</a>
|
||||
<span class="inbox-meta">Last accessed {{ inbox.lastAccessedAgo }}</span>
|
||||
</div>
|
||||
<form method="POST" action="/account/locked-inbox/release" class="inline-form">
|
||||
<input type="hidden" name="address" value="{{ inbox.address }}">
|
||||
<button type="submit" class="button button-small button-danger" onclick="return confirm('Release lock on {{ inbox.address }}?')">
|
||||
Release
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="empty-state">No locked inboxes yet. Lock an inbox to protect it with your account.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if stats.lockedInboxesCount < stats.maxLockedInboxes %}
|
||||
<p class="hint">You can lock up to {{ stats.maxLockedInboxes }} inboxes total.</p>
|
||||
{% else %}
|
||||
<p class="limit-reached">Maximum {{ stats.maxLockedInboxes }} inboxes locked</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Email Modal -->
|
||||
<div id="addEmailModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" id="closeAddEmail">×</span>
|
||||
<h3>Add Forwarding Email</h3>
|
||||
<p class="modal-description">Enter an email address to verify. We'll send you a verification link.</p>
|
||||
|
||||
<form method="POST" action="/account/forward-email/add">
|
||||
<fieldset>
|
||||
<label for="forwardEmail">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="forwardEmail"
|
||||
name="email"
|
||||
placeholder="your-email@example.com"
|
||||
required
|
||||
class="modal-input"
|
||||
>
|
||||
<button type="submit" class="button-primary modal-button">Send Verification</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add Email Modal
|
||||
const addEmailBtn = document.getElementById('addEmailBtn');
|
||||
const addEmailModal = document.getElementById('addEmailModal');
|
||||
const closeAddEmail = document.getElementById('closeAddEmail');
|
||||
|
||||
if (addEmailBtn) {
|
||||
addEmailBtn.onclick = function() {
|
||||
addEmailModal.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
if (closeAddEmail) {
|
||||
closeAddEmail.onclick = function() {
|
||||
addEmailModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target == addEmailModal) {
|
||||
addEmailModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
{% block header %}
|
||||
<div class="action-links">
|
||||
{% if showUnlockButton %}
|
||||
<a href="#" id="unlockBtn" aria-label="Unlock inbox">Unlock</a>
|
||||
{% if authEnabled and not currentUser %}
|
||||
<a href="/auth" aria-label="Login or Register">Account</a>
|
||||
{% endif %}
|
||||
<a href="/" aria-label="Return to home">Logout</a>
|
||||
<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"/>
|
||||
|
|
@ -21,48 +21,4 @@
|
|||
<h1>{{message}}</h1>
|
||||
<h2>{{error.status}}</h2>
|
||||
<pre>{{error.stack}}</pre>
|
||||
|
||||
{% if showUnlockButton %}
|
||||
<div id="unlockModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<span class="close" id="closeUnlock">×</span>
|
||||
<h3>Unlock Inbox</h3>
|
||||
<p class="modal-description">Enter password to access this locked inbox.</p>
|
||||
{% if unlockError %}
|
||||
<p class="unlock-error">
|
||||
{% if unlockError == 'invalid_password' %}
|
||||
Invalid password. Please try again.
|
||||
{% elseif unlockError == 'missing_fields' %}
|
||||
Please provide a password.
|
||||
{% else %}
|
||||
An error occurred. Please try again.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<form method="POST" action="/lock/unlock">
|
||||
<input type="hidden" name="address" value="{{ address }}">
|
||||
<input type="hidden" name="redirectTo" value="{{ redirectTo|default(address) }}">
|
||||
<fieldset>
|
||||
<label for="unlockPassword" class="floating-label">Password</label>
|
||||
<input type="password" id="unlockPassword" name="password" required class="modal-input">
|
||||
<button type="submit" class="button-primary modal-button">Unlock</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const modal = document.getElementById('unlockModal');
|
||||
const btn = document.getElementById('unlockBtn');
|
||||
const close = document.getElementById('closeUnlock');
|
||||
if (btn) btn.onclick = (e) => { e.preventDefault(); modal.style.display = 'block'; };
|
||||
if (close) close.onclick = () => modal.style.display = 'none';
|
||||
window.onclick = (e) => { if (e.target == modal) modal.style.display = 'none'; };
|
||||
|
||||
// Auto-open modal if there's an unlock error
|
||||
if ('{{ unlockError|default("") }}') {
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,41 @@
|
|||
|
||||
{% block header %}
|
||||
<div class="action-links">
|
||||
{% if authEnabled %}
|
||||
{% if isLocked and hasAccess %}
|
||||
<a href="#" id="removeLockBtn" aria-label="Remove password lock">Remove Lock</a>
|
||||
{% elseif isLocked %}
|
||||
<a href="#" id="unlockBtn" aria-label="Unlock inbox">Unlock</a>
|
||||
{% else %}
|
||||
<a href="#" id="lockBtn" aria-label="Protect inbox with password">Protect Inbox</a>
|
||||
{% if currentUser %}
|
||||
<!-- Inbox Dropdown (multiple actions when logged in) -->
|
||||
<div class="action-dropdown">
|
||||
<button class="dropdown-toggle" aria-label="Inbox actions">Inbox ▾</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="#" id="forwardAllBtn" aria-label="Forward all emails">Forward All</a>
|
||||
{% if authEnabled %}
|
||||
{% if isLocked and hasAccess %}
|
||||
<a href="#" id="removeLockBtn" aria-label="Remove lock">Remove Lock</a>
|
||||
{% elseif not isLocked %}
|
||||
<a href="#" id="lockBtn" aria-label="Lock inbox to your account">Lock Inbox</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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" aria-label="Logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<!-- Simple buttons when not logged in -->
|
||||
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
|
||||
{% if authEnabled %}
|
||||
<a href="/auth" aria-label="Login or Register">Account</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="#" id="forwardAllBtn" aria-label="Forward all emails">Forward All</a>
|
||||
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
|
||||
{% if authEnabled and hasAccess %}
|
||||
<a href="/lock/logout" aria-label="Logout">Logout</a>
|
||||
{% else %}
|
||||
<a href="/logout" aria-label="Logout">Logout</a>
|
||||
{% endif %}
|
||||
|
||||
<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"/>
|
||||
|
|
@ -93,21 +112,22 @@
|
|||
<div id="lockModal" class="modal" style="display: none;" data-lock-error="{{ error|default('') }}">
|
||||
<div class="modal-content">
|
||||
<span class="close" id="closeLock">×</span>
|
||||
<h3>Protect Inbox</h3>
|
||||
<p class="modal-description">Password-protect this inbox. Locked emails won't be deleted. Protection active for {{ locktimer }}hrs after last login.</p>
|
||||
<h3>Lock Inbox</h3>
|
||||
<p class="modal-description">Lock this inbox to your account. Only you will be able to access it while logged in.</p>
|
||||
{% if error and error == 'locking_disabled_for_example' %}
|
||||
<p id="lockServerError" class="unlock-error">Locking is disabled for the example inbox.</p>
|
||||
{% elseif error and error == 'max_locked_inboxes' %}
|
||||
<p id="lockServerError" class="unlock-error">You have reached the maximum of 5 locked inboxes. Please remove a lock before adding a new one.</p>
|
||||
{% elseif error and error == 'already_locked' %}
|
||||
<p id="lockServerError" class="unlock-error">This inbox is already locked by another user.</p>
|
||||
{% elseif error and error == 'not_your_lock' %}
|
||||
<p id="lockServerError" class="unlock-error">You don't own the lock on this inbox.</p>
|
||||
{% endif %}
|
||||
<p id="lockErrorInline" class="unlock-error" style="display:none"></p>
|
||||
<form method="POST" action="/lock/lock">
|
||||
<input type="hidden" name="address" value="{{ address }}">
|
||||
<fieldset>
|
||||
<label for="lockPassword" class="floating-label">Password (min 8 characters)</label>
|
||||
<input type="password" id="lockPassword" name="password" placeholder="Password" required minlength="8" class="modal-input">
|
||||
|
||||
<label for="lockConfirm" class="floating-label">Confirm Password</label>
|
||||
<input type="password" id="lockConfirm" placeholder="Confirm" required minlength="8" class="modal-input">
|
||||
|
||||
<p>This inbox will be protected with your account. Only you will be able to access it while logged in.</p>
|
||||
<button type="submit" class="button-primary modal-button">Lock Inbox</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
|
@ -116,45 +136,15 @@
|
|||
|
||||
{% endif %}
|
||||
|
||||
{% if authEnabled and isLocked and not hasAccess %}
|
||||
<!-- Unlock Modal -->
|
||||
<div id="unlockModal" class="modal" style="display: none;" data-unlock-error="{{ unlockError|default('') }}">
|
||||
<div class="modal-content">
|
||||
<span class="close" id="closeUnlock">×</span>
|
||||
<h3>Unlock Inbox</h3>
|
||||
<p class="modal-description">Enter password to access this locked inbox.</p>
|
||||
{% if unlockError %}
|
||||
<p class="unlock-error">
|
||||
{% if unlockError == 'invalid_password' %}
|
||||
Invalid password. Please try again.
|
||||
{% elseif unlockError == 'missing_fields' %}
|
||||
Please provide a password.
|
||||
{% else %}
|
||||
An error occurred. Please try again.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<form method="POST" action="/lock/unlock">
|
||||
<input type="hidden" name="address" value="{{ address }}">
|
||||
<input type="hidden" name="redirectTo" value="{{ redirectTo }}">
|
||||
<fieldset>
|
||||
<label for="unlockPassword" class="floating-label">Password</label>
|
||||
<input type="password" id="unlockPassword" name="password" placeholder="Password" required class="modal-input">
|
||||
<button type="submit" class="button-primary modal-button">Unlock</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if authEnabled and isLocked and hasAccess %}
|
||||
<!-- Remove Lock Modal -->
|
||||
<div id="removeLockModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<span class="close" id="closeRemoveLock">×</span>
|
||||
<h3>Remove Password Lock</h3>
|
||||
<p class="modal-description">Are you sure you want to remove the password lock from this inbox? This cannot be undone.</p>
|
||||
<h3>Remove Lock</h3>
|
||||
<p class="modal-description">Are you sure you want to remove the lock from this inbox? Anyone will be able to access it.</p>
|
||||
<form method="POST" action="/lock/remove">
|
||||
<input type="hidden" name="address" value="{{ address }}">
|
||||
<fieldset>
|
||||
|
|
@ -183,19 +173,38 @@
|
|||
<div class="modal-content">
|
||||
<span class="close" id="closeForwardAll">×</span>
|
||||
<h3>Forward All Emails</h3>
|
||||
<p class="modal-description">Enter the email address to forward all emails in this inbox to. Limited to 25 emails maximum.</p>
|
||||
{% if mailSummaries|length > 0 %}
|
||||
<p class="modal-info">You have {{ mailSummaries|length }} email(s) in this inbox.</p>
|
||||
|
||||
{% if not currentUser %}
|
||||
<p class="modal-description auth-required">
|
||||
<strong>Login required:</strong> You must be logged in to forward emails.
|
||||
</p>
|
||||
<div class="auth-prompt">
|
||||
<a href="/auth?redirect={{ ('/inbox/' ~ address)|url_encode }}" class="button button-primary">Login or Register</a>
|
||||
</div>
|
||||
{% elseif userForwardEmails|length == 0 %}
|
||||
<p class="modal-description">You don't have any verified forwarding emails yet.</p>
|
||||
<p class="modal-description">Add a verified email address in your account settings to enable forwarding.</p>
|
||||
<a href="/account" class="button button-primary">Go to Account Settings</a>
|
||||
{% else %}
|
||||
<p class="modal-description">Select a verified email address to forward all emails to. Limited to 25 emails maximum.</p>
|
||||
{% if mailSummaries|length > 0 %}
|
||||
<p class="modal-info">You have {{ mailSummaries|length }} email(s) in this inbox.</p>
|
||||
{% endif %}
|
||||
<p id="forwardAllError" class="unlock-error" style="display:none"></p>
|
||||
<form method="POST" action="/inbox/{{ address }}/forward-all">
|
||||
<fieldset>
|
||||
<label for="forwardAllEmail" class="floating-label">Forward to</label>
|
||||
<select id="forwardAllEmail" name="destinationEmail" required class="modal-input">
|
||||
<option value="">Select an email...</option>
|
||||
{% for email in userForwardEmails %}
|
||||
<option value="{{ email.email }}">{{ email.email }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-hint">Manage emails in <a href="/account">Account Settings</a></small>
|
||||
<button type="submit" class="button-primary modal-button">Forward All</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
<p id="forwardAllError" class="unlock-error" style="display:none"></p>
|
||||
<form method="POST" action="/inbox/{{ address }}/forward-all">
|
||||
<fieldset>
|
||||
<label for="forwardAllEmail" class="floating-label">Destination Email</label>
|
||||
<input type="email" id="forwardAllEmail" name="destinationEmail"
|
||||
placeholder="recipient@example.com" required class="modal-input">
|
||||
<button type="submit" class="button-primary modal-button">Forward All</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
{% block header %}
|
||||
<div class="action-links">
|
||||
<a href="/" aria-label="Return to home">← Home</a>
|
||||
<a href="/register" aria-label="Register">Register</a>
|
||||
<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"/>
|
||||
|
|
@ -16,70 +15,110 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="login-auth" class="auth-container">
|
||||
<div class="auth-card">
|
||||
<h1>Welcome Back</h1>
|
||||
<p class="auth-subtitle">Login to access your account</p>
|
||||
|
||||
<div id="auth-unified" class="auth-unified-container">
|
||||
<div class="auth-intro">
|
||||
<h1>Account Access</h1>
|
||||
<p class="auth-subtitle">Login to an existing account or create a new one</p>
|
||||
{% if errorMessage %}
|
||||
<div class="unlock-error">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div class="unlock-error">{{ errorMessage }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if successMessage %}
|
||||
<div class="success-message">
|
||||
{{ successMessage }}
|
||||
</div>
|
||||
<div class="success-message">{{ successMessage }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<fieldset>
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
autocomplete="username"
|
||||
>
|
||||
<div class="auth-forms-grid">
|
||||
<!-- Login Form -->
|
||||
<div class="auth-card">
|
||||
<h2>Login</h2>
|
||||
<p class="auth-card-subtitle">Access your existing account</p>
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<fieldset>
|
||||
<label for="login-username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="login-username"
|
||||
name="username"
|
||||
placeholder="Your username"
|
||||
required
|
||||
autocomplete="username"
|
||||
>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
>
|
||||
<label for="login-password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="login-password"
|
||||
name="password"
|
||||
placeholder="Your password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="button button-primary" type="submit">Login</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>Don't have an account? <a href="/register">Register here</a></p>
|
||||
<!-- Register Form -->
|
||||
<div class="auth-card">
|
||||
<h2>Register</h2>
|
||||
<p class="auth-card-subtitle">Create a new account</p>
|
||||
|
||||
<form method="POST" action="/register">
|
||||
<fieldset>
|
||||
<label for="register-username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="register-username"
|
||||
name="username"
|
||||
placeholder="3-20 characters"
|
||||
required
|
||||
minlength="3"
|
||||
maxlength="20"
|
||||
pattern="[a-zA-Z0-9_]+"
|
||||
autocomplete="username"
|
||||
>
|
||||
<small>Letters, numbers, underscore only</small>
|
||||
|
||||
<label for="register-password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="register-password"
|
||||
name="password"
|
||||
placeholder="Min 8 characters"
|
||||
required
|
||||
minlength="8"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<small>Uppercase, lowercase, and number</small>
|
||||
|
||||
<label for="register-confirm">Confirm Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="register-confirm"
|
||||
name="confirmPassword"
|
||||
placeholder="Re-enter password"
|
||||
required
|
||||
minlength="8"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
|
||||
<button class="button button-primary" type="submit">Create Account</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-features">
|
||||
<h3>Account Features</h3>
|
||||
<ul>
|
||||
<li>✓ Forward emails to verified addresses</li>
|
||||
<li>✓ Lock and protect up to 5 inboxes</li>
|
||||
<li>✓ Manage forwarding destinations</li>
|
||||
<li>✓ Access from any device</li>
|
||||
</ul>
|
||||
|
||||
<div class="auth-guest-section">
|
||||
<h4>Guest Access</h4>
|
||||
<p>You can still use temporary inboxes without an account, but forwarding and locking require registration.</p>
|
||||
<a href="/" class="button">Browse as Guest</a>
|
||||
<div class="auth-features-unified">
|
||||
<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">Manage multiple forwarding destinations</div>
|
||||
<div class="feature-item">Access your locked inboxes anywhere</div>
|
||||
</div>
|
||||
<p class="guest-note">No account needed for basic temporary inboxes • <a href="/">Browse as guest</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,37 @@
|
|||
{% block header %}
|
||||
<div class="action-links">
|
||||
<a href="/inbox/{{ address }}" aria-label="Return to inbox">← Return to inbox</a>
|
||||
<a href="#" id="forwardBtn" aria-label="Forward this email">Forward Email</a>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/delete" aria-label="Delete this email">Delete Email</a>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
|
||||
{% if authEnabled and isLocked and hasAccess %}
|
||||
<a href="/lock/logout" aria-label="Logout">Logout</a>
|
||||
|
||||
{% if currentUser %}
|
||||
<!-- Email Dropdown (multiple actions when logged in) -->
|
||||
<div class="action-dropdown">
|
||||
<button class="dropdown-toggle" aria-label="Email actions">Email ▾</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="#" id="forwardBtn" aria-label="Forward this email">Forward</a>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/delete" aria-label="Delete this email">Delete</a>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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="/" aria-label="Home">Home</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="/logout" aria-label="Logout">Logout</a>
|
||||
<!-- Simple buttons when not logged in -->
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/delete" aria-label="Delete this email">Delete</a>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
|
||||
{% if authEnabled %}
|
||||
<a href="/auth" aria-label="Login or Register">Account</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<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"/>
|
||||
|
|
@ -104,19 +127,38 @@
|
|||
<div class="modal-content">
|
||||
<span class="close" id="closeForward">×</span>
|
||||
<h3>Forward Email</h3>
|
||||
<p class="modal-description">Enter the email address to forward this message to.</p>
|
||||
{% if errorMessage %}
|
||||
<p class="unlock-error">{{ errorMessage }}</p>
|
||||
|
||||
{% if not currentUser %}
|
||||
<p class="modal-description auth-required">
|
||||
<strong>Login required:</strong> You must be logged in to forward emails.
|
||||
</p>
|
||||
<div class="auth-prompt">
|
||||
<a href="/auth?redirect={{ ('/inbox/' ~ address ~ '/' ~ uid)|url_encode }}" class="button button-primary">Login or Register</a>
|
||||
</div>
|
||||
{% elseif userForwardEmails|length == 0 %}
|
||||
<p class="modal-description">You don't have any verified forwarding emails yet.</p>
|
||||
<p class="modal-description">Add a verified email address in your account settings to enable forwarding.</p>
|
||||
<a href="/account" class="button button-primary">Go to Account Settings</a>
|
||||
{% else %}
|
||||
<p class="modal-description">Select a verified email address to forward this message to.</p>
|
||||
{% if errorMessage %}
|
||||
<p class="unlock-error">{{ errorMessage }}</p>
|
||||
{% endif %}
|
||||
<p id="forwardError" class="unlock-error" style="display:none"></p>
|
||||
<form method="POST" action="/inbox/{{ address }}/{{ uid }}/forward">
|
||||
<fieldset>
|
||||
<label for="forwardEmail" class="floating-label">Forward to</label>
|
||||
<select id="forwardEmail" name="destinationEmail" required class="modal-input">
|
||||
<option value="">Select an email...</option>
|
||||
{% for email in userForwardEmails %}
|
||||
<option value="{{ email.email }}">{{ email.email }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-hint">Manage emails in <a href="/account">Account Settings</a></small>
|
||||
<button type="submit" class="button-primary modal-button">Forward</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
<p id="forwardError" class="unlock-error" style="display:none"></p>
|
||||
<form method="POST" action="/inbox/{{ address }}/{{ uid }}/forward">
|
||||
<fieldset>
|
||||
<label for="forwardEmail" class="floating-label">Destination Email</label>
|
||||
<input type="email" id="forwardEmail" name="destinationEmail"
|
||||
placeholder="recipient@example.com" required class="modal-input">
|
||||
<button type="submit" class="button-primary modal-button">Forward</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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 { sanitizeHtmlTwigFilter } = require('./views/twig-filters')
|
||||
|
||||
const Helper = require('../../application/helper')
|
||||
|
|
@ -43,20 +44,22 @@ app.use(express.json())
|
|||
app.use(express.urlencoded({ extended: false }))
|
||||
|
||||
// Cookie parser for signed cookies (email verification)
|
||||
app.use(cookieParser(config.lock.sessionSecret))
|
||||
app.use(cookieParser(config.user.sessionSecret))
|
||||
|
||||
// Session support (always enabled for forward verification and inbox locking)
|
||||
app.use(session({
|
||||
secret: config.lock.sessionSecret,
|
||||
secret: config.user.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
|
||||
}))
|
||||
|
||||
// Clear session when user goes Home so locked inboxes require password again
|
||||
// Clear lock session data when user goes Home (but preserve authentication)
|
||||
app.get('/', (req, res, next) => {
|
||||
if (req.session) {
|
||||
req.session.destroy(() => 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()
|
||||
}
|
||||
|
|
@ -87,6 +90,19 @@ app.use(
|
|||
)
|
||||
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
|
||||
|
||||
// Middleware to expose user session to all templates
|
||||
app.use((req, res, next) => {
|
||||
res.locals.authEnabled = config.user.authEnabled
|
||||
res.locals.currentUser = null
|
||||
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 show loading page until IMAP is ready
|
||||
app.use((req, res, next) => {
|
||||
const isImapReady = req.app.get('isImapReady')
|
||||
|
|
@ -99,6 +115,7 @@ app.use((req, res, next) => {
|
|||
app.use('/', loginRouter)
|
||||
if (config.user.authEnabled) {
|
||||
app.use('/', authRouter)
|
||||
app.use('/', accountRouter)
|
||||
}
|
||||
app.use('/inbox', inboxRouter)
|
||||
app.use('/error', errorRouter)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue