mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
[Feat]: UX Debug mode
Adds UX debug mode while mocking the imap server and other critical parts of the service simply to test UI elements for faster development
This commit is contained in:
parent
16ccc978f9
commit
a7691ccf43
13 changed files with 963 additions and 105 deletions
175
app.js
175
app.js
|
|
@ -25,21 +25,30 @@ const helper = new(Helper)
|
||||||
const { app, io, server } = require('./infrastructure/web/web')
|
const { app, io, server } = require('./infrastructure/web/web')
|
||||||
const ClientNotification = require('./infrastructure/web/client-notification')
|
const ClientNotification = require('./infrastructure/web/client-notification')
|
||||||
const ImapService = require('./application/imap-service')
|
const ImapService = require('./application/imap-service')
|
||||||
|
const MockMailService = require('./application/mocks/mock-mail-service')
|
||||||
const MailProcessingService = require('./application/mail-processing-service')
|
const MailProcessingService = require('./application/mail-processing-service')
|
||||||
const SmtpService = require('./application/smtp-service')
|
const SmtpService = require('./application/smtp-service')
|
||||||
const AuthService = require('./application/auth-service')
|
const AuthService = require('./application/auth-service')
|
||||||
|
const MockAuthService = require('./application/mocks/mock-auth-service')
|
||||||
const MailRepository = require('./domain/mail-repository')
|
const MailRepository = require('./domain/mail-repository')
|
||||||
const InboxLock = require('./domain/inbox-lock')
|
const InboxLock = require('./domain/inbox-lock')
|
||||||
|
const MockInboxLock = require('./application/mocks/mock-inbox-lock')
|
||||||
const VerificationStore = require('./domain/verification-store')
|
const VerificationStore = require('./domain/verification-store')
|
||||||
const UserRepository = require('./domain/user-repository')
|
const UserRepository = require('./domain/user-repository')
|
||||||
|
const MockUserRepository = require('./application/mocks/mock-user-repository')
|
||||||
const StatisticsStore = require('./domain/statistics-store')
|
const StatisticsStore = require('./domain/statistics-store')
|
||||||
|
|
||||||
const clientNotification = new ClientNotification()
|
const clientNotification = new ClientNotification()
|
||||||
debug('Client notification service initialized')
|
debug('Client notification service initialized')
|
||||||
clientNotification.use(io)
|
clientNotification.use(io)
|
||||||
|
|
||||||
const smtpService = new SmtpService(config)
|
// Initialize SMTP service only if not in UX debug mode
|
||||||
debug('SMTP service initialized')
|
const smtpService = config.uxDebugMode ? null : new SmtpService(config)
|
||||||
|
if (smtpService) {
|
||||||
|
debug('SMTP service initialized')
|
||||||
|
} else {
|
||||||
|
debug('SMTP service disabled (UX debug mode)')
|
||||||
|
}
|
||||||
app.set('smtpService', smtpService)
|
app.set('smtpService', smtpService)
|
||||||
|
|
||||||
const verificationStore = new VerificationStore()
|
const verificationStore = new VerificationStore()
|
||||||
|
|
@ -53,7 +62,7 @@ app.set('config', config)
|
||||||
let inboxLock = null
|
let inboxLock = null
|
||||||
let statisticsStore = null
|
let statisticsStore = null
|
||||||
|
|
||||||
if (config.user.authEnabled) {
|
if (config.user.authEnabled && !config.uxDebugMode) {
|
||||||
// Migrate legacy database files for backwards compatibility
|
// Migrate legacy database files for backwards compatibility
|
||||||
Helper.migrateDatabase(config.user.databasePath)
|
Helper.migrateDatabase(config.user.databasePath)
|
||||||
|
|
||||||
|
|
@ -90,22 +99,50 @@ if (config.user.authEnabled) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, config.imap.refreshIntervalSeconds * 1000)
|
}, config.imap.refreshIntervalSeconds * 1000)
|
||||||
|
|
||||||
console.log('User authentication system enabled')
|
|
||||||
} else {
|
} else {
|
||||||
// No auth enabled - initialize statistics store without persistence
|
// No auth enabled OR UX debug mode - initialize statistics store without persistence
|
||||||
statisticsStore = new StatisticsStore()
|
statisticsStore = new StatisticsStore()
|
||||||
debug('Statistics store initialized (in-memory only, no database)')
|
if (config.uxDebugMode) {
|
||||||
|
debug('Statistics store initialized (UX debug mode - clean slate)')
|
||||||
|
|
||||||
|
// In UX debug mode, create mock auth system
|
||||||
|
const mockUserRepository = new MockUserRepository(config)
|
||||||
|
debug('Mock user repository initialized')
|
||||||
|
app.set('userRepository', mockUserRepository)
|
||||||
|
|
||||||
|
const mockAuthService = new MockAuthService()
|
||||||
|
debug('Mock auth service initialized')
|
||||||
|
app.set('authService', mockAuthService)
|
||||||
|
|
||||||
|
inboxLock = new MockInboxLock(mockUserRepository)
|
||||||
|
app.set('inboxLock', inboxLock)
|
||||||
|
debug('Mock inbox lock service initialized')
|
||||||
|
|
||||||
|
debug('Mock authentication system enabled for UX debug mode')
|
||||||
|
} else {
|
||||||
|
debug('Statistics store initialized (in-memory only, no database)')
|
||||||
|
app.set('userRepository', null)
|
||||||
|
app.set('authService', null)
|
||||||
|
app.set('inboxLock', null)
|
||||||
|
debug('User authentication system disabled')
|
||||||
|
}
|
||||||
app.set('statisticsStore', statisticsStore)
|
app.set('statisticsStore', statisticsStore)
|
||||||
|
|
||||||
app.set('userRepository', null)
|
if (!config.uxDebugMode) {
|
||||||
app.set('authService', null)
|
debug('User authentication system disabled')
|
||||||
app.set('inboxLock', null)
|
}
|
||||||
debug('User authentication system disabled')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const imapService = new ImapService(config, inboxLock)
|
// Initialize IMAP or Mock service based on debug mode
|
||||||
debug('IMAP service initialized')
|
const imapService = config.uxDebugMode ?
|
||||||
|
new MockMailService(config) :
|
||||||
|
new ImapService(config, inboxLock)
|
||||||
|
|
||||||
|
if (config.uxDebugMode) {
|
||||||
|
debug('Mock Mail Service initialized (UX Debug Mode)')
|
||||||
|
} else {
|
||||||
|
debug('IMAP service initialized')
|
||||||
|
}
|
||||||
app.set('imapService', imapService)
|
app.set('imapService', imapService)
|
||||||
|
|
||||||
const mailProcessingService = new MailProcessingService(
|
const mailProcessingService = new MailProcessingService(
|
||||||
|
|
@ -121,13 +158,29 @@ debug('Mail processing service initialized')
|
||||||
|
|
||||||
// Initialize statistics with current count
|
// Initialize statistics with current count
|
||||||
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, async() => {
|
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, async() => {
|
||||||
|
// In UX debug mode, populate mock emails first
|
||||||
|
if (config.uxDebugMode) {
|
||||||
|
// Load mock emails into repository
|
||||||
|
const mockEmails = imapService.getMockEmails()
|
||||||
|
mockEmails.forEach(({ mail }) => {
|
||||||
|
mailProcessingService.onNewMail(mail)
|
||||||
|
})
|
||||||
|
debug(`UX Debug Mode: Loaded ${mockEmails.length} mock emails`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then initialize statistics with the correct count
|
||||||
const count = mailProcessingService.getCount()
|
const count = mailProcessingService.getCount()
|
||||||
statisticsStore.initialize(count)
|
statisticsStore.initialize(count)
|
||||||
|
|
||||||
// Get and set the largest UID for all-time total
|
if (config.uxDebugMode) {
|
||||||
const largestUid = await helper.getLargestUid(imapService)
|
statisticsStore.updateLargestUid(2) // 2 mock emails
|
||||||
statisticsStore.updateLargestUid(largestUid)
|
debug(`UX Debug Mode: Statistics initialized with ${count} emails, largest UID: 2`)
|
||||||
debug(`Statistics initialized with ${count} emails, largest UID: ${largestUid}`)
|
} else {
|
||||||
|
// Get and set the largest UID for all-time total
|
||||||
|
const largestUid = await helper.getLargestUid(imapService)
|
||||||
|
statisticsStore.updateLargestUid(largestUid)
|
||||||
|
debug(`Statistics initialized with ${count} emails, largest UID: ${largestUid}`)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set up timer sync broadcasting after IMAP is ready
|
// Set up timer sync broadcasting after IMAP is ready
|
||||||
|
|
@ -135,6 +188,69 @@ imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () => {
|
||||||
clientNotification.startTimerSync(imapService)
|
clientNotification.startTimerSync(imapService)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Display startup banner when everything is ready
|
||||||
|
let imapReady = false
|
||||||
|
let serverReady = false
|
||||||
|
|
||||||
|
function displayStartupBanner() {
|
||||||
|
if (!imapReady || !serverReady) return
|
||||||
|
|
||||||
|
const mailCount = mailProcessingService.getCount()
|
||||||
|
const domains = config.email.domains.join(', ')
|
||||||
|
const purgeTime = `${config.email.purgeTime.time} ${config.email.purgeTime.unit}`
|
||||||
|
const refreshInterval = config.uxDebugMode ? 'N/A' : `${config.imap.refreshIntervalSeconds}s`
|
||||||
|
|
||||||
|
// Determine mode based on environment
|
||||||
|
let mode = 'PRODUCTION'
|
||||||
|
if (config.uxDebugMode) {
|
||||||
|
mode = 'UX DEBUG'
|
||||||
|
} else if (process.env.DEBUG) {
|
||||||
|
mode = 'DEBUG'
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n' + '═'.repeat(70))
|
||||||
|
console.log(` 48hr.email - ${mode} MODE`)
|
||||||
|
console.log('═'.repeat(70))
|
||||||
|
console.log(` Server: http://localhost:${config.http.port}`)
|
||||||
|
console.log(` Domains: ${domains}`)
|
||||||
|
console.log(` Emails loaded: ${mailCount}`)
|
||||||
|
console.log(` Purge after: ${purgeTime}`)
|
||||||
|
console.log(` IMAP refresh: ${refreshInterval}`)
|
||||||
|
|
||||||
|
if (!config.uxDebugMode && config.email.examples.account && config.email.examples.uids) {
|
||||||
|
console.log(` Example inbox: ${config.email.examples.account}`)
|
||||||
|
console.log(` Example UIDs: ${config.email.examples.uids.join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.uxDebugMode) {
|
||||||
|
console.log(` Authentication: Mock (any username/password works)`)
|
||||||
|
const mockUserRepo = app.get('userRepository')
|
||||||
|
if (mockUserRepo) {
|
||||||
|
console.log(` Demo forward: ${mockUserRepo.mockForwardEmail}`)
|
||||||
|
console.log(` Demo locked: ${mockUserRepo.mockLockedInbox}`)
|
||||||
|
}
|
||||||
|
} else if (config.user.authEnabled) {
|
||||||
|
console.log(` Authentication: Enabled`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.http.features.statistics) {
|
||||||
|
console.log(` Statistics: Enabled`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('═'.repeat(70))
|
||||||
|
console.log(` Ready! Press Ctrl+C to stop\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () => {
|
||||||
|
imapReady = true
|
||||||
|
displayStartupBanner()
|
||||||
|
})
|
||||||
|
|
||||||
|
server.on('ready', () => {
|
||||||
|
serverReady = true
|
||||||
|
displayStartupBanner()
|
||||||
|
})
|
||||||
|
|
||||||
// Track IMAP initialization state
|
// Track IMAP initialization state
|
||||||
let isImapReady = false
|
let isImapReady = false
|
||||||
app.set('isImapReady', false)
|
app.set('isImapReady', false)
|
||||||
|
|
@ -158,13 +274,17 @@ debug('Bound IMAP deleted mail event handler')
|
||||||
mailProcessingService.on('error', err => {
|
mailProcessingService.on('error', err => {
|
||||||
debug('Fatal error from mail processing service:', err.message)
|
debug('Fatal error from mail processing service:', err.message)
|
||||||
console.error('Error from mailProcessingService, stopping.', err)
|
console.error('Error from mailProcessingService, stopping.', err)
|
||||||
process.exit(1)
|
if (!config.uxDebugMode) {
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
imapService.on(ImapService.EVENT_ERROR, error => {
|
imapService.on(ImapService.EVENT_ERROR, error => {
|
||||||
debug('Fatal error from IMAP service:', error.message)
|
debug('Fatal error from IMAP service:', error.message)
|
||||||
console.error('Fatal error from IMAP service', error)
|
console.error('Fatal error from IMAP service', error)
|
||||||
process.exit(1)
|
if (!config.uxDebugMode) {
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.set('mailProcessingService', mailProcessingService)
|
app.set('mailProcessingService', mailProcessingService)
|
||||||
|
|
@ -173,11 +293,18 @@ app.set('config', config)
|
||||||
app.locals.imapService = imapService
|
app.locals.imapService = imapService
|
||||||
app.locals.mailProcessingService = mailProcessingService
|
app.locals.mailProcessingService = mailProcessingService
|
||||||
|
|
||||||
debug('Starting IMAP connection and message loading')
|
if (config.uxDebugMode) {
|
||||||
|
debug('Starting Mock Mail Service (UX Debug Mode)')
|
||||||
|
} else {
|
||||||
|
debug('Starting IMAP connection and message loading')
|
||||||
|
}
|
||||||
|
|
||||||
imapService.connectAndLoadMessages().catch(error => {
|
imapService.connectAndLoadMessages().catch(error => {
|
||||||
debug('Failed to connect to IMAP:', error.message)
|
debug('Failed to connect:', error.message)
|
||||||
console.error('Fatal error from IMAP service', error)
|
console.error('Fatal error from mail service', error)
|
||||||
process.exit(1)
|
if (!config.uxDebugMode) {
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
server.on('error', error => {
|
server.on('error', error => {
|
||||||
|
|
@ -200,4 +327,4 @@ server.on('error', error => {
|
||||||
console.error('Fatal web server error', error)
|
console.error('Fatal web server error', error)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -37,6 +37,9 @@ function parseBool(v) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
// UX Debug Mode
|
||||||
|
uxDebugMode: parseBool(process.env.UX_DEBUG_MODE) || false,
|
||||||
|
|
||||||
email: {
|
email: {
|
||||||
domains: parseValue(process.env.EMAIL_DOMAINS) || [],
|
domains: parseValue(process.env.EMAIL_DOMAINS) || [],
|
||||||
purgeTime: {
|
purgeTime: {
|
||||||
|
|
@ -107,9 +110,26 @@ const config = {
|
||||||
|
|
||||||
// validation
|
// validation
|
||||||
debug('Validating configuration...')
|
debug('Validating configuration...')
|
||||||
if (!config.imap.user || !config.imap.password || !config.imap.host) {
|
|
||||||
debug('IMAP configuration validation failed: missing user, password, or host')
|
// Skip IMAP validation in UX debug mode
|
||||||
throw new Error("IMAP is not configured. Check IMAP_* env vars.");
|
if (!config.uxDebugMode) {
|
||||||
|
if (!config.imap.user || !config.imap.password || !config.imap.host) {
|
||||||
|
debug('IMAP configuration validation failed: missing user, password, or host')
|
||||||
|
throw new Error("IMAP is not configured. Check IMAP_* env vars.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In UX debug mode, provide default domain if none configured
|
||||||
|
if (config.uxDebugMode && !config.email.domains.length) {
|
||||||
|
config.email.domains = ['ux-debug.local']
|
||||||
|
debug('UX Debug Mode: Using default domain "ux-debug.local"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// In UX debug mode, set example account to show mock emails
|
||||||
|
if (config.uxDebugMode) {
|
||||||
|
config.email.examples.account = `demo@${config.email.domains[0]}`
|
||||||
|
config.email.examples.uids = [1, 2]
|
||||||
|
debug(`UX Debug Mode: Example account set to ${config.email.examples.account} with UIDs 1, 2`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.email.domains.length) {
|
if (!config.email.domains.length) {
|
||||||
|
|
@ -117,6 +137,6 @@ if (!config.email.domains.length) {
|
||||||
throw new Error("No EMAIL_DOMAINS configured.");
|
throw new Error("No EMAIL_DOMAINS configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(`Configuration validated successfully: ${config.email.domains.length} domains, IMAP host: ${config.imap.host}`)
|
debug(`Configuration validated successfully: ${config.email.domains.length} domains${config.uxDebugMode ? ' (UX DEBUG MODE)' : ''}`)
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|
|
||||||
|
|
@ -140,10 +140,8 @@ class MailProcessingService extends EventEmitter {
|
||||||
onInitialLoadDone() {
|
onInitialLoadDone() {
|
||||||
this.initialLoadDone = true
|
this.initialLoadDone = true
|
||||||
debug('Initial load completed, total mails:', this.mailRepository.mailCount())
|
debug('Initial load completed, total mails:', this.mailRepository.mailCount())
|
||||||
console.log(`Initial load done, got ${this.mailRepository.mailCount()} mails`)
|
|
||||||
console.log(`Fetching and deleting mails every ${this.config.imap.refreshIntervalSeconds} seconds`)
|
// Don't print old-style logs here, app.js will handle the startup banner
|
||||||
console.log(`Mails older than ${this.config.email.purgeTime.time} ${this.config.email.purgeTime.unit} will be deleted`)
|
|
||||||
console.log(`The example emails are: ${this.config.email.examples.uids.join(', ')}, on the account ${this.config.email.examples.account}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewMail(mail) {
|
onNewMail(mail) {
|
||||||
|
|
|
||||||
37
application/mocks/mock-auth-service.js
Normal file
37
application/mocks/mock-auth-service.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Mock Auth Service for UX Debug Mode
|
||||||
|
* Provides dummy authentication without database
|
||||||
|
*/
|
||||||
|
|
||||||
|
const crypto = require('crypto')
|
||||||
|
|
||||||
|
class MockAuthService {
|
||||||
|
constructor() {
|
||||||
|
// Mock user data
|
||||||
|
this.mockUser = {
|
||||||
|
id: 1,
|
||||||
|
username: 'demo',
|
||||||
|
password_hash: 'mock', // Any password works
|
||||||
|
created_at: Date.now() - 86400000, // 1 day ago
|
||||||
|
last_login: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(username, password) {
|
||||||
|
// Accept any username/password in debug mode
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: this.mockUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(username, password) {
|
||||||
|
// Accept any registration
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: this.mockUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MockAuthService
|
||||||
125
application/mocks/mock-inbox-lock.js
Normal file
125
application/mocks/mock-inbox-lock.js
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
/**
|
||||||
|
* Mock Inbox Lock for UX Debug Mode
|
||||||
|
* Provides dummy inbox locking without database
|
||||||
|
*/
|
||||||
|
|
||||||
|
const debug = require('debug')('48hr-email:mock-inbox-lock')
|
||||||
|
|
||||||
|
class MockInboxLock {
|
||||||
|
constructor(mockUserRepository) {
|
||||||
|
this.mockUserRepository = mockUserRepository
|
||||||
|
this.locks = new Map()
|
||||||
|
|
||||||
|
// Initialize locks from repository
|
||||||
|
this._initializeLocks()
|
||||||
|
|
||||||
|
debug(`Mock locked inboxes: ${Array.from(mockUserRepository.lockedInboxes).join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
_initializeLocks() {
|
||||||
|
// Add the mock locked inboxes from the repository
|
||||||
|
for (const address of this.mockUserRepository.lockedInboxes) {
|
||||||
|
this.locks.set(address.toLowerCase(), {
|
||||||
|
userId: 1,
|
||||||
|
address: address.toLowerCase(),
|
||||||
|
lockedAt: Date.now(),
|
||||||
|
lastAccess: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset to initial state (called when repository resets)
|
||||||
|
reset() {
|
||||||
|
this.locks.clear()
|
||||||
|
this._initializeLocks()
|
||||||
|
debug('Mock inbox locks reset to initial state')
|
||||||
|
}
|
||||||
|
|
||||||
|
isLocked(address) {
|
||||||
|
return this.locks.has(address.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAccess(userId, address) {
|
||||||
|
const lock = this.locks.get(address.toLowerCase())
|
||||||
|
if (!lock) return true // Not locked
|
||||||
|
return lock.userId === userId
|
||||||
|
}
|
||||||
|
|
||||||
|
isLockedByUser(address, userId) {
|
||||||
|
const lock = this.locks.get(address.toLowerCase())
|
||||||
|
if (!lock) return false
|
||||||
|
return lock.userId === userId
|
||||||
|
}
|
||||||
|
|
||||||
|
lock(userId, address) {
|
||||||
|
const normalizedAddress = address.toLowerCase()
|
||||||
|
if (this.locks.has(normalizedAddress)) {
|
||||||
|
throw new Error('Inbox is already locked')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.locks.set(normalizedAddress, {
|
||||||
|
userId,
|
||||||
|
address: normalizedAddress,
|
||||||
|
lockedAt: Date.now(),
|
||||||
|
lastAccess: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mockUserRepository.lockedInboxes.add(address)
|
||||||
|
debug(`Locked inbox: ${normalizedAddress}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
release(userId, address) {
|
||||||
|
const normalizedAddress = address.toLowerCase()
|
||||||
|
const lock = this.locks.get(normalizedAddress)
|
||||||
|
|
||||||
|
if (!lock) {
|
||||||
|
throw new Error('Inbox is not locked')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lock.userId !== userId) {
|
||||||
|
throw new Error('You do not own this lock')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.locks.delete(normalizedAddress)
|
||||||
|
this.mockUserRepository.lockedInboxes.delete(address)
|
||||||
|
debug(`Released lock on ${normalizedAddress}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAccess(userId, address) {
|
||||||
|
const lock = this.locks.get(address.toLowerCase())
|
||||||
|
if (lock && lock.userId === userId) {
|
||||||
|
lock.lastAccess = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserLockedInboxes(userId) {
|
||||||
|
const userLocks = []
|
||||||
|
for (const [address, lock] of this.locks.entries()) {
|
||||||
|
if (lock.userId === userId) {
|
||||||
|
userLocks.push({
|
||||||
|
address: address,
|
||||||
|
locked_at: lock.lockedAt,
|
||||||
|
last_access: lock.lastAccess
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userLocks
|
||||||
|
}
|
||||||
|
|
||||||
|
getInactive(hours) {
|
||||||
|
// Mock - return empty array
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserLockCount(userId) {
|
||||||
|
let count = 0
|
||||||
|
for (const lock of this.locks.values()) {
|
||||||
|
if (lock.userId === userId) count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MockInboxLock
|
||||||
364
application/mocks/mock-mail-service.js
Normal file
364
application/mocks/mock-mail-service.js
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
/**
|
||||||
|
* Mock Mail Service for UX Debug Mode
|
||||||
|
* Provides sample emails without requiring IMAP/SMTP connections
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Mail = require('../../domain/mail')
|
||||||
|
const EventEmitter = require('events')
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
// Clara's PGP Public Key
|
||||||
|
const CLARA_PGP_KEY = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQENBGb92JcBCADNMGkl6x2e//Prbbyvlb3EE6BwfOdKpSa+70bJ8fsudlkas5VN
|
||||||
|
Iyuq6Kmuk8V9LD5qBL3e0SMe2x3K5xb+j0Wim/n0OIHkbdnAOtLqEbYMSAzL3keo
|
||||||
|
mqw5qbV47js3rxht9BZ2HYZm5GqOqLz4XuIomSS/EsDcuQDKVtKveE2nRkJUIORr
|
||||||
|
C+DNFcjgJc3yrF1bKE3KQz2ii7qNRH/ChKRXB+OS/7ZviQOSTlFVPGhjIxaI2sRI
|
||||||
|
Uw8U8pWPYyQzh+dpiA3OmzbF1/BB2AQOx98p975KTI4wmalF5PtsKnkFFZ1NPKC6
|
||||||
|
E6G0IIbDkEE1HBpEO4qmIuWd/tFyIP03EwL3ABEBAAG0G0NsYXJhIEsgPGNsYXJh
|
||||||
|
QGNyYXp5Y28ueHl6PokBSgQQAQgAHQUCZv3YlwQLCQcIAxUICgQWAAIBAhkBAhsD
|
||||||
|
Ah4BACEJEAGLYq6lsVaPFiEEmKN22IQaxMpTgI1sAYtirqWxVo8IOAf9HJglE8hQ
|
||||||
|
bqGtbCISKGOkeIq8TFr9A2MRaevNVQtf4o9TnzMi+9nFGfi6yniiceBz9oUWoXvt
|
||||||
|
ZkhEzc0Hn6PAX/sOW3r6wPu5gSaGjUJfd339aDasyZvdOoQ4cukcErIaFnAG7KmP
|
||||||
|
0Q7lyRp5K7dUmuc9b9dg5ngf+M8306dj/djHWCPtsaLJc7ExrfeT1lNA7MeY7DlE
|
||||||
|
9jdvm4hfwQZND16nXKKLZn/RZUkR5Zoo1LE+GSL0/GCFZeH1PnEt5kcI3QKyx9wn
|
||||||
|
+DlMcAZCVs2X5JzTbJQKr9Cwv1syOlaZmVeUTuKiTfAB71wINQkFHdmONIg0h9wp
|
||||||
|
ThTjXOlDsQvnP7kBDQRm/diXAQgAg8BaBpL//o62UrrbQ79WbVzVTH+2f+xVD8bE
|
||||||
|
tyEL1QjllFfPdc5oT9nQ5RPfN6IJpbN0/p688pQa10gFgjEN0WtI51Vda/PQ1FQ8
|
||||||
|
q1xXbH6zJXP3FAPEPTId4Rw7Gb+vaUaBo3O0ZyKpAxzEy2gIvXz2ChfL6ENn5QZ/
|
||||||
|
1DsBeQQE3YbgG+jXAL//JGjINoppOTCfnEMlKaZYdkLvA2KiJKqtD+JDTVFkdk02
|
||||||
|
1Jext8Td6wkd72i0+DQI9RaJJr5oDXlxAN0iX4OMSdo35e2Mj4AktjvO8JzRvZjU
|
||||||
|
uPCGYH9DpVoB0OCNRmD/2CeUyQgiehk8NHXLxf8h1duTGZYYRQARAQABiQE2BBgB
|
||||||
|
CAAJBQJm/diXAhsMACEJEAGLYq6lsVaPFiEEmKN22IQaxMpTgI1sAYtirqWxVo/R
|
||||||
|
cQgAmJ0taRUkOmoepQR6JNJejF1JiITQy5eSvZzXDBoEWip4fcl4FRAy5yz6s/sC
|
||||||
|
NtweWyWMg3+lu+s7qh3r1Qw5EN7ukgUy+fvk6xY3TBxcJ1aC/KvKbaeTrZt0Bt6U
|
||||||
|
sQipNDI/cPkL2ILzqt/shEgj9g/EWARe1X5SQ0nEhCYLi7xZV9lBe3dU+EUlmwSe
|
||||||
|
gmxppMfACd9hyVV4SbO6l5NKmXgkYWNMzFzjfg3pxAPuJjaaYN85XETqpKwdfPRt
|
||||||
|
KUPuyh+UdOt8GPRBcFxjRJQrBRw2nBJxCCEJOJAJJ2ySpHQBwpaXsK0WW2SGkaxF
|
||||||
|
ggOCb56KkepgTvU3Xdv5opRZAg==
|
||||||
|
=HEe7
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----`
|
||||||
|
|
||||||
|
class MockMailService extends EventEmitter {
|
||||||
|
constructor(config) {
|
||||||
|
super()
|
||||||
|
this.config = config
|
||||||
|
this.mockEmails = this._generateMockEmails()
|
||||||
|
this.logoAttachment = this._getLogoAttachment()
|
||||||
|
}
|
||||||
|
|
||||||
|
_getLogoAttachment() {
|
||||||
|
// Try to read the service logo
|
||||||
|
const logoPath = path.join(__dirname, '../infrastructure/web/public/images/logo.png')
|
||||||
|
if (fs.existsSync(logoPath)) {
|
||||||
|
return {
|
||||||
|
filename: '48hr-email-logo.png',
|
||||||
|
content: fs.readFileSync(logoPath),
|
||||||
|
contentType: 'image/png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateMockEmails() {
|
||||||
|
const domain = this.config.email.domains[0]
|
||||||
|
const now = new Date()
|
||||||
|
const earlier = new Date(now.getTime() - 3600000) // 1 hour ago
|
||||||
|
|
||||||
|
return [{
|
||||||
|
mail: Mail.create(
|
||||||
|
[`demo@${domain}`], [{ name: 'Clara K', address: 'clara@crazyco.xyz' }],
|
||||||
|
earlier.toISOString(),
|
||||||
|
'Welcome to 48hr.email - Plain Text Demo',
|
||||||
|
1
|
||||||
|
),
|
||||||
|
fullMail: {
|
||||||
|
text: `Hello from 48hr.email!
|
||||||
|
|
||||||
|
This is a plain text demonstration email for UX debugging purposes.
|
||||||
|
|
||||||
|
48hr.email is your favorite open-source temporary email service, created by ClaraCrazy.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Disposable email addresses
|
||||||
|
- No registration required
|
||||||
|
- Auto-delete after configured time
|
||||||
|
- Open source (GPL-3.0)
|
||||||
|
- Self-hostable
|
||||||
|
|
||||||
|
For more information, visit: https://48hr.email
|
||||||
|
GitHub: https://github.com/Crazyco-xyz/48hr.email
|
||||||
|
Discord: https://discord.gg/crazyco
|
||||||
|
|
||||||
|
---
|
||||||
|
Clara's PGP Public Key:
|
||||||
|
|
||||||
|
${CLARA_PGP_KEY}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a mock email generated for UX debug mode.
|
||||||
|
No actual IMAP or SMTP connections were used.`,
|
||||||
|
textAsHtml: `<p>Hello from 48hr.email!</p>
|
||||||
|
<p>This is a plain text demonstration email for UX debugging purposes.</p>
|
||||||
|
<p>48hr.email is your favorite open-source temporary email service, created by ClaraCrazy.</p>
|
||||||
|
<p>Features:<br/>
|
||||||
|
- Disposable email addresses<br/>
|
||||||
|
- No registration required<br/>
|
||||||
|
- Auto-delete after configured time<br/>
|
||||||
|
- Open source (GPL-3.0)<br/>
|
||||||
|
- Self-hostable</p>
|
||||||
|
<p>For more information, visit: <a href="https://48hr.email">https://48hr.email</a><br/>
|
||||||
|
GitHub: <a href="https://github.com/Crazyco-xyz/48hr.email">https://github.com/Crazyco-xyz/48hr.email</a><br/>
|
||||||
|
Discord: <a href="https://discord.gg/crazyco">https://discord.gg/crazyco</a></p>
|
||||||
|
<p>---<br/>
|
||||||
|
Clara's PGP Public Key:</p>
|
||||||
|
<pre style="background: #1a1a1a; color: #666; padding: 12px; border-radius: 6px; border: 1px solid rgba(155, 77, 202, 0.2);">${CLARA_PGP_KEY}</pre>
|
||||||
|
<p>---</p>
|
||||||
|
<p>This is a mock email generated for UX debug mode.<br/>
|
||||||
|
No actual IMAP or SMTP connections were used.</p>`,
|
||||||
|
html: null,
|
||||||
|
subject: 'Welcome to 48hr.email - Plain Text Demo',
|
||||||
|
from: { text: 'Clara K <clara@crazyco.xyz>' },
|
||||||
|
to: { text: `demo@${domain}` },
|
||||||
|
date: earlier,
|
||||||
|
attachments: this.logoAttachment ? [this.logoAttachment] : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mail: Mail.create(
|
||||||
|
[`demo@${domain}`], [{ name: '48hr.email', address: 'noreply@48hr.email' }],
|
||||||
|
now.toISOString(),
|
||||||
|
'HTML Email Demo - Features Overview',
|
||||||
|
2
|
||||||
|
),
|
||||||
|
fullMail: {
|
||||||
|
text: `48hr.email - HTML Email Demo
|
||||||
|
|
||||||
|
This is the plain text version of the HTML email.
|
||||||
|
|
||||||
|
Visit https://48hr.email for more information.
|
||||||
|
|
||||||
|
Clara's PGP Key is attached to this email.`,
|
||||||
|
html: `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #e0e0e0;
|
||||||
|
background: #131516;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid rgba(155, 77, 202, 0.3);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #9b4dca;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #9b4dca;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.feature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
}
|
||||||
|
.feature-item {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: rgba(155, 77, 202, 0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
.pgp-key {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid rgba(155, 77, 202, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: #666;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
line-height: 1.3;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #9b4dca;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #b366e6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div>
|
||||||
|
<h1>48hr.email</h1>
|
||||||
|
<p class="subtitle">Temporary inbox, no registration</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="section">
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>Open-source temporary email service. Create disposable addresses instantly and receive emails without registration. Emails auto-delete after the configured purge time.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Features</h2>
|
||||||
|
<div class="feature-grid">
|
||||||
|
<div class="feature-item">Instant addresses</div>
|
||||||
|
<div class="feature-item">No registration</div>
|
||||||
|
<div class="feature-item">Real-time updates</div>
|
||||||
|
<div class="feature-item">HTML rendering</div>
|
||||||
|
<div class="feature-item">Open source GPL-3.0</div>
|
||||||
|
<div class="feature-item">Self-hostable</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Developer PGP Key (ClaraCrazy)</h2>
|
||||||
|
<div class="pgp-key">${CLARA_PGP_KEY}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p><strong>48hr.email</strong> by <a href="https://crazyco.xyz">ClaraCrazy</a> · <a href="https://github.com/Crazyco-xyz/48hr.email">GitHub</a> · <a href="https://discord.gg/crazyco">Discord</a> · UX Debug Mode</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
subject: 'HTML Email Demo - Features Overview',
|
||||||
|
from: { text: '48hr.email <noreply@48hr.email>' },
|
||||||
|
to: { text: `demo@${domain}` },
|
||||||
|
date: now,
|
||||||
|
attachments: this.logoAttachment ? [this.logoAttachment] : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectAndLoadMessages() {
|
||||||
|
// Simulate async loading
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
// Emit initial load event
|
||||||
|
this.emit('initial load done')
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
getMockEmails() {
|
||||||
|
return this.mockEmails
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchOneFullMail(to, uid, raw = false) {
|
||||||
|
const email = this.mockEmails.find(e => e.mail.uid === parseInt(uid))
|
||||||
|
if (!email) {
|
||||||
|
throw new Error(`Mock email with UID ${uid} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If raw is requested, return a string representation
|
||||||
|
if (raw) {
|
||||||
|
const mail = email.fullMail
|
||||||
|
const headers = [
|
||||||
|
`From: ${mail.from.text}`,
|
||||||
|
`To: ${mail.to.text}`,
|
||||||
|
`Date: ${mail.date}`,
|
||||||
|
`Subject: ${mail.subject}`,
|
||||||
|
`Content-Type: ${mail.html ? 'text/html; charset=UTF-8' : 'text/plain; charset=UTF-8'}`,
|
||||||
|
'',
|
||||||
|
mail.html || mail.text || ''
|
||||||
|
]
|
||||||
|
return headers.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return email.fullMail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stub methods for compatibility
|
||||||
|
deleteMessage() {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteOldMails() {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBox() {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
getSecondsUntilNextRefresh() {
|
||||||
|
// In mock mode, return null (no refresh needed)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLargestUid() {
|
||||||
|
// Return the largest UID from mock emails
|
||||||
|
const mockEmails = this.getMockEmails()
|
||||||
|
if (mockEmails.length === 0) return null
|
||||||
|
return Math.max(...mockEmails.map(e => e.mail.uid))
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, handler) {
|
||||||
|
return super.on(event, handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MockMailService.EVENT_INITIAL_LOAD_DONE = 'initial load done'
|
||||||
|
MockMailService.EVENT_NEW_MAIL = 'mail'
|
||||||
|
MockMailService.EVENT_DELETED_MAIL = 'mailDeleted'
|
||||||
|
MockMailService.EVENT_ERROR = 'error'
|
||||||
|
|
||||||
|
module.exports = MockMailService
|
||||||
131
application/mocks/mock-user-repository.js
Normal file
131
application/mocks/mock-user-repository.js
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
/**
|
||||||
|
* Mock User Repository for UX Debug Mode
|
||||||
|
* Provides dummy user data without database
|
||||||
|
*/
|
||||||
|
|
||||||
|
const debug = require('debug')('48hr-email:mock-user-repo')
|
||||||
|
|
||||||
|
class MockUserRepository {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config
|
||||||
|
|
||||||
|
// Generate a random forwarding email (fixed for this server instance)
|
||||||
|
const randomGmailName = Math.random().toString(36).substring(2, 10)
|
||||||
|
this.mockForwardEmail = `${randomGmailName}@gmail.com`
|
||||||
|
|
||||||
|
// Generate a random locked inbox (fixed for this server instance)
|
||||||
|
const randomWords = ['alpha', 'beta', 'gamma', 'delta', 'omega', 'sigma', 'theta']
|
||||||
|
const word1 = randomWords[Math.floor(Math.random() * randomWords.length)]
|
||||||
|
const word2 = randomWords[Math.floor(Math.random() * randomWords.length)]
|
||||||
|
const num = Math.floor(Math.random() * 999)
|
||||||
|
this.mockLockedInbox = `${word1}${word2}${num}@${config.email.domains[0]}`
|
||||||
|
|
||||||
|
// Store the initial values to reset to
|
||||||
|
this.initialForwardEmail = this.mockForwardEmail
|
||||||
|
this.initialLockedInbox = this.mockLockedInbox
|
||||||
|
|
||||||
|
// In-memory storage that can be modified during a session
|
||||||
|
this.forwardEmails = new Set([this.mockForwardEmail])
|
||||||
|
this.lockedInboxes = new Set([this.mockLockedInbox])
|
||||||
|
|
||||||
|
debug(`Mock forward email: ${this.mockForwardEmail}`)
|
||||||
|
debug(`Mock locked inbox: ${this.mockLockedInbox}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset to initial state (called on new page loads)
|
||||||
|
reset() {
|
||||||
|
this.forwardEmails = new Set([this.initialForwardEmail])
|
||||||
|
this.lockedInboxes = new Set([this.initialLockedInbox])
|
||||||
|
debug('Mock data reset to initial state')
|
||||||
|
}
|
||||||
|
|
||||||
|
// User methods
|
||||||
|
getUserById(userId) {
|
||||||
|
if (userId === 1) {
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
username: 'demo',
|
||||||
|
password_hash: 'mock',
|
||||||
|
created_at: Date.now() - 86400000,
|
||||||
|
last_login: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserByUsername(username) {
|
||||||
|
return this.getUserById(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLastLogin(userId) {
|
||||||
|
// No-op in mock
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward email methods
|
||||||
|
getForwardEmails(userId) {
|
||||||
|
if (userId === 1) {
|
||||||
|
const emails = []
|
||||||
|
let id = 1
|
||||||
|
for (const email of this.forwardEmails) {
|
||||||
|
emails.push({
|
||||||
|
id: id++,
|
||||||
|
user_id: 1,
|
||||||
|
email: email,
|
||||||
|
verified: true,
|
||||||
|
verification_token: null,
|
||||||
|
created_at: Date.now() - 3600000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return emails
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
addForwardEmail(userId, email, token) {
|
||||||
|
this.forwardEmails.add(email)
|
||||||
|
return {
|
||||||
|
id: this.forwardEmails.size,
|
||||||
|
user_id: userId,
|
||||||
|
email: email,
|
||||||
|
verified: false,
|
||||||
|
verification_token: token,
|
||||||
|
created_at: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyForwardEmail(token) {
|
||||||
|
// In mock mode, just return success
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
removeForwardEmail(userId, email) {
|
||||||
|
const deleted = this.forwardEmails.delete(email)
|
||||||
|
debug(`Removed forward email: ${email} (success: ${deleted})`)
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteForwardEmail(userId, email) {
|
||||||
|
// Alias for removeForwardEmail
|
||||||
|
return this.removeForwardEmail(userId, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User stats
|
||||||
|
getUserStats(userId, config) {
|
||||||
|
return {
|
||||||
|
lockedInboxesCount: this.lockedInboxes.size,
|
||||||
|
forwardEmailsCount: this.forwardEmails.size,
|
||||||
|
accountAge: Math.floor((Date.now() - (Date.now() - 86400000)) / 86400000),
|
||||||
|
maxLockedInboxes: config.maxLockedInboxes || 5,
|
||||||
|
maxForwardEmails: config.maxForwardEmails || 5,
|
||||||
|
lockReleaseHours: config.lockReleaseHours || 720
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup - no-op
|
||||||
|
close() {
|
||||||
|
debug('Mock user repository closed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MockUserRepository
|
||||||
|
|
@ -455,23 +455,23 @@ class StatisticsStore {
|
||||||
const cutoff = Date.now() - this._getPurgeCutoffMs()
|
const cutoff = Date.now() - this._getPurgeCutoffMs()
|
||||||
const relevantHistory = this.historicalData.filter(point => point.timestamp >= cutoff)
|
const relevantHistory = this.historicalData.filter(point => point.timestamp >= cutoff)
|
||||||
|
|
||||||
// Aggregate by hour
|
// Aggregate by 15-minute intervals for better granularity
|
||||||
const hourlyBuckets = new Map()
|
const intervalBuckets = new Map()
|
||||||
relevantHistory.forEach(point => {
|
relevantHistory.forEach(point => {
|
||||||
const hour = Math.floor(point.timestamp / 3600000) * 3600000
|
const interval = Math.floor(point.timestamp / 900000) * 900000 // 15 minutes
|
||||||
if (!hourlyBuckets.has(hour)) {
|
if (!intervalBuckets.has(interval)) {
|
||||||
hourlyBuckets.set(hour, 0)
|
intervalBuckets.set(interval, 0)
|
||||||
}
|
}
|
||||||
hourlyBuckets.set(hour, hourlyBuckets.get(hour) + point.receives)
|
intervalBuckets.set(interval, intervalBuckets.get(interval) + point.receives)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Convert to array and sort
|
// Convert to array and sort
|
||||||
const hourlyData = Array.from(hourlyBuckets.entries())
|
const intervalData = Array.from(intervalBuckets.entries())
|
||||||
.map(([timestamp, receives]) => ({ timestamp, receives }))
|
.map(([timestamp, receives]) => ({ timestamp, receives }))
|
||||||
.sort((a, b) => a.timestamp - b.timestamp)
|
.sort((a, b) => a.timestamp - b.timestamp)
|
||||||
|
|
||||||
debug(`Historical timeline: ${hourlyData.length} hourly points within ${config.email.purgeTime.time} ${config.email.purgeTime.unit} window`)
|
debug(`Historical timeline: ${intervalData.length} 15-min interval points within ${config.email.purgeTime.time} ${config.email.purgeTime.unit} window`)
|
||||||
return hourlyData
|
return intervalData
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -512,12 +512,16 @@ class StatisticsStore {
|
||||||
|
|
||||||
debug(`Built hourly patterns for ${hourlyAverages.size} hours from ${this.historicalData.length} data points`)
|
debug(`Built hourly patterns for ${hourlyAverages.size} hours from ${this.historicalData.length} data points`)
|
||||||
|
|
||||||
// Generate predictions for purge duration (in 1-hour intervals)
|
// Generate predictions for a reasonable future window
|
||||||
|
// Limit to 20% of purge duration or 12 hours max to maintain chart balance
|
||||||
|
// Use 15-minute intervals for better granularity
|
||||||
const purgeMs = this._getPurgeCutoffMs()
|
const purgeMs = this._getPurgeCutoffMs()
|
||||||
const predictionHours = Math.ceil(purgeMs / (60 * 60 * 1000))
|
const purgeDurationHours = Math.ceil(purgeMs / (60 * 60 * 1000))
|
||||||
|
const predictionHours = Math.min(12, Math.ceil(purgeDurationHours * 0.2))
|
||||||
|
const predictionIntervals = predictionHours * 4 // Convert hours to 15-min intervals
|
||||||
|
|
||||||
for (let i = 1; i <= predictionHours; i++) {
|
for (let i = 1; i <= predictionIntervals; i++) {
|
||||||
const timestamp = now + (i * 60 * 60 * 1000) // 1 hour intervals
|
const timestamp = now + (i * 15 * 60 * 1000) // 15 minute intervalsals
|
||||||
const futureDate = new Date(timestamp)
|
const futureDate = new Date(timestamp)
|
||||||
const futureHour = futureDate.getHours()
|
const futureHour = futureDate.getHours()
|
||||||
|
|
||||||
|
|
@ -529,8 +533,8 @@ class StatisticsStore {
|
||||||
baseCount = allValues.reduce((sum, v) => sum + v, 0) / allValues.length
|
baseCount = allValues.reduce((sum, v) => sum + v, 0) / allValues.length
|
||||||
}
|
}
|
||||||
|
|
||||||
// baseCount is already per-minute average, scale to full hour
|
// baseCount is already per-minute average, scale to 15 minutes
|
||||||
const scaledCount = baseCount * 60
|
const scaledCount = baseCount * 15
|
||||||
|
|
||||||
// Add randomization (±20%)
|
// Add randomization (±20%)
|
||||||
const randomFactor = 0.8 + (Math.random() * 0.4) // 0.8 to 1.2
|
const randomFactor = 0.8 + (Math.random() * 0.4) // 0.8 to 1.2
|
||||||
|
|
@ -626,23 +630,23 @@ class StatisticsStore {
|
||||||
_getTimeline() {
|
_getTimeline() {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const cutoff = now - this._getPurgeCutoffMs()
|
const cutoff = now - this._getPurgeCutoffMs()
|
||||||
const hourly = {}
|
const buckets = {}
|
||||||
|
|
||||||
// Aggregate by hour
|
// Aggregate by 15-minute intervals for better granularity
|
||||||
this.hourlyData
|
this.hourlyData
|
||||||
.filter(e => e.timestamp >= cutoff)
|
.filter(e => e.timestamp >= cutoff)
|
||||||
.forEach(entry => {
|
.forEach(entry => {
|
||||||
const hour = Math.floor(entry.timestamp / 3600000) * 3600000
|
const interval = Math.floor(entry.timestamp / 900000) * 900000 // 15 minutes
|
||||||
if (!hourly[hour]) {
|
if (!buckets[interval]) {
|
||||||
hourly[hour] = { timestamp: hour, receives: 0, deletes: 0, forwards: 0 }
|
buckets[interval] = { timestamp: interval, receives: 0, deletes: 0, forwards: 0 }
|
||||||
}
|
}
|
||||||
hourly[hour].receives += entry.receives
|
buckets[interval].receives += entry.receives
|
||||||
hourly[hour].deletes += entry.deletes
|
buckets[interval].deletes += entry.deletes
|
||||||
hourly[hour].forwards += entry.forwards
|
buckets[interval].forwards += entry.forwards
|
||||||
})
|
})
|
||||||
|
|
||||||
// Convert to sorted array
|
// Convert to sorted array
|
||||||
return Object.values(hourly).sort((a, b) => a.timestamp - b.timestamp)
|
return Object.values(buckets).sort((a, b) => a.timestamp - b.timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ let statsChart = null;
|
||||||
let chartContext = null;
|
let chartContext = null;
|
||||||
let lastReloadTime = 0;
|
let lastReloadTime = 0;
|
||||||
const RELOAD_COOLDOWN_MS = 2000; // 2 second cooldown between reloads
|
const RELOAD_COOLDOWN_MS = 2000; // 2 second cooldown between reloads
|
||||||
|
let allTimePoints = []; // Store globally so segment callbacks can access it
|
||||||
|
|
||||||
// Initialize stats chart if on stats page
|
// Initialize stats chart if on stats page
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
@ -39,10 +40,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Combine all data and create labels
|
// Combine all data and create labels
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Use a reasonable historical window (show data within the purge time range)
|
// Merge historical and realtime into a single continuous dataset
|
||||||
// This will adapt based on whether purge time is 48 hours, 7 days, etc.
|
// Historical will be blue, current will be green
|
||||||
const allTimePoints = [
|
// Only show historical data that doesn't overlap with realtime (exclude any matching timestamps)
|
||||||
...historicalData.map(d => ({...d, type: 'historical' })),
|
const realtimeTimestamps = new Set(realtimeData.map(d => d.timestamp));
|
||||||
|
const filteredHistorical = historicalData.filter(d => !realtimeTimestamps.has(d.timestamp));
|
||||||
|
|
||||||
|
allTimePoints = [
|
||||||
|
...filteredHistorical.map(d => ({...d, type: 'historical' })),
|
||||||
...realtimeData.map(d => ({...d, type: 'realtime' })),
|
...realtimeData.map(d => ({...d, type: 'realtime' })),
|
||||||
...predictionData.map(d => ({...d, type: 'prediction' }))
|
...predictionData.map(d => ({...d, type: 'prediction' }))
|
||||||
].sort((a, b) => a.timestamp - b.timestamp);
|
].sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
|
@ -58,47 +63,53 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepare datasets
|
// Merge historical and realtime into one dataset with segment coloring
|
||||||
const historicalPoints = allTimePoints.map(d => d.type === 'historical' ? d.receives : null);
|
const combinedPoints = allTimePoints.map(d =>
|
||||||
const realtimePoints = allTimePoints.map(d => d.type === 'realtime' ? d.receives : null);
|
(d.type === 'historical' || d.type === 'realtime') ? d.receives : null
|
||||||
|
);
|
||||||
const predictionPoints = allTimePoints.map(d => d.type === 'prediction' ? d.receives : null);
|
const predictionPoints = allTimePoints.map(d => d.type === 'prediction' ? d.receives : null);
|
||||||
|
|
||||||
// Create gradient for fading effect on historical data
|
// Create gradient for fading effect on historical data
|
||||||
const ctx = chartCanvas.getContext('2d');
|
const ctx = chartCanvas.getContext('2d');
|
||||||
chartContext = ctx;
|
chartContext = ctx;
|
||||||
const historicalGradient = ctx.createLinearGradient(0, 0, chartCanvas.width * 0.3, 0);
|
|
||||||
historicalGradient.addColorStop(0, 'rgba(100, 100, 255, 0.05)');
|
|
||||||
historicalGradient.addColorStop(1, 'rgba(100, 100, 255, 0.15)');
|
|
||||||
|
|
||||||
// Track visibility state for each dataset
|
// Track visibility state for each dataset
|
||||||
const datasetVisibility = [true, true, true];
|
const datasetVisibility = [true, true];
|
||||||
|
|
||||||
statsChart = new Chart(ctx, {
|
statsChart = new Chart(ctx, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Historical',
|
label: 'Email Activity',
|
||||||
data: historicalPoints,
|
data: combinedPoints,
|
||||||
borderColor: 'rgba(100, 149, 237, 0.8)',
|
segment: {
|
||||||
backgroundColor: historicalGradient,
|
borderColor: (context) => {
|
||||||
borderWidth: 2,
|
const index = context.p0DataIndex;
|
||||||
tension: 0.4,
|
const point = allTimePoints[index];
|
||||||
pointRadius: 4,
|
// Blue for historical, green for current
|
||||||
pointBackgroundColor: 'rgba(100, 149, 237, 0.8)',
|
return point && point.type === 'historical' ? 'rgba(100, 149, 237, 0.8)' : '#2ecc71';
|
||||||
spanGaps: true,
|
},
|
||||||
fill: true,
|
backgroundColor: (context) => {
|
||||||
hidden: false
|
const index = context.p0DataIndex;
|
||||||
},
|
const point = allTimePoints[index];
|
||||||
{
|
return point && point.type === 'historical' ? 'rgba(100, 149, 237, 0.15)' : 'rgba(46, 204, 113, 0.15)';
|
||||||
label: 'Current Activity',
|
}
|
||||||
data: realtimePoints,
|
},
|
||||||
borderColor: '#2ecc71',
|
borderColor: '#2ecc71', // Default to green
|
||||||
backgroundColor: 'rgba(46, 204, 113, 0.15)',
|
backgroundColor: 'rgba(46, 204, 113, 0.15)',
|
||||||
borderWidth: 4,
|
borderWidth: 3,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointRadius: 4,
|
pointRadius: (context) => {
|
||||||
pointBackgroundColor: '#2ecc71',
|
const index = context.dataIndex;
|
||||||
|
const point = allTimePoints[index];
|
||||||
|
return point && point.type === 'historical' ? 3 : 4;
|
||||||
|
},
|
||||||
|
pointBackgroundColor: (context) => {
|
||||||
|
const index = context.dataIndex;
|
||||||
|
const point = allTimePoints[index];
|
||||||
|
return point && point.type === 'historical' ? 'rgba(100, 149, 237, 0.8)' : '#2ecc71';
|
||||||
|
},
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
fill: true,
|
fill: true,
|
||||||
hidden: false
|
hidden: false
|
||||||
|
|
@ -198,14 +209,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
legendContainer.className = 'chart-legend-custom';
|
legendContainer.className = 'chart-legend-custom';
|
||||||
legendContainer.innerHTML = `
|
legendContainer.innerHTML = `
|
||||||
<button class="legend-btn active" data-index="0">
|
<button class="legend-btn active" data-index="0">
|
||||||
<span class="legend-indicator" style="background: rgba(100, 149, 237, 0.8);"></span>
|
<span class="legend-indicator" style="background: linear-gradient(to right, rgba(100, 149, 237, 0.8) 0%, #2ecc71 100%);"></span>
|
||||||
<span class="legend-label">Historical</span>
|
<span class="legend-label">Email Activity</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="legend-btn active" data-index="1">
|
<button class="legend-btn active" data-index="1">
|
||||||
<span class="legend-indicator" style="background: #2ecc71;"></span>
|
|
||||||
<span class="legend-label">Current Activity</span>
|
|
||||||
</button>
|
|
||||||
<button class="legend-btn active" data-index="2">
|
|
||||||
<span class="legend-indicator" style="background: #ff9f43; border: 2px dashed rgba(255, 159, 67, 0.5);"></span>
|
<span class="legend-indicator" style="background: #ff9f43; border: 2px dashed rgba(255, 159, 67, 0.5);"></span>
|
||||||
<span class="legend-label">Predicted</span>
|
<span class="legend-label">Predicted</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -256,8 +263,12 @@ function rebuildStatsChart() {
|
||||||
const historicalData = window.historicalData || [];
|
const historicalData = window.historicalData || [];
|
||||||
const predictionData = window.predictionData || [];
|
const predictionData = window.predictionData || [];
|
||||||
|
|
||||||
const allTimePoints = [
|
// Only show historical data that doesn't overlap with realtime (exclude any matching timestamps)
|
||||||
...historicalData.map(d => ({...d, type: 'historical' })),
|
const realtimeTimestamps = new Set(realtimeData.map(d => d.timestamp));
|
||||||
|
const filteredHistorical = historicalData.filter(d => !realtimeTimestamps.has(d.timestamp));
|
||||||
|
|
||||||
|
allTimePoints = [
|
||||||
|
...filteredHistorical.map(d => ({...d, type: 'historical' })),
|
||||||
...realtimeData.map(d => ({...d, type: 'realtime' })),
|
...realtimeData.map(d => ({...d, type: 'realtime' })),
|
||||||
...predictionData.map(d => ({...d, type: 'prediction' }))
|
...predictionData.map(d => ({...d, type: 'prediction' }))
|
||||||
].sort((a, b) => a.timestamp - b.timestamp);
|
].sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
|
@ -277,16 +288,16 @@ function rebuildStatsChart() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepare datasets
|
// Merge historical and realtime into one dataset with segment coloring
|
||||||
const historicalPoints = allTimePoints.map(d => d.type === 'historical' ? d.receives : null);
|
const combinedPoints = allTimePoints.map(d =>
|
||||||
const realtimePoints = allTimePoints.map(d => d.type === 'realtime' ? d.receives : null);
|
(d.type === 'historical' || d.type === 'realtime') ? d.receives : null
|
||||||
|
);
|
||||||
const predictionPoints = allTimePoints.map(d => d.type === 'prediction' ? d.receives : null);
|
const predictionPoints = allTimePoints.map(d => d.type === 'prediction' ? d.receives : null);
|
||||||
|
|
||||||
// Update chart data
|
// Update chart data
|
||||||
statsChart.data.labels = labels;
|
statsChart.data.labels = labels;
|
||||||
statsChart.data.datasets[0].data = historicalPoints;
|
statsChart.data.datasets[0].data = combinedPoints;
|
||||||
statsChart.data.datasets[1].data = realtimePoints;
|
statsChart.data.datasets[1].data = predictionPoints;
|
||||||
statsChart.data.datasets[2].data = predictionPoints;
|
|
||||||
|
|
||||||
// Update the chart
|
// Update the chart
|
||||||
statsChart.update();
|
statsChart.update();
|
||||||
|
|
|
||||||
|
|
@ -1842,6 +1842,9 @@ label {
|
||||||
border: 1px solid var(--overlay-white-10);
|
border: 1px solid var(--overlay-white-10);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 40vh;
|
min-height: 40vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
resize: vertical;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-text-content {
|
.mail-text-content {
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,28 @@ const templateContext = require('../template-context')
|
||||||
// GET /account - Account dashboard
|
// GET /account - Account dashboard
|
||||||
router.get('/account', requireAuth, async(req, res) => {
|
router.get('/account', requireAuth, async(req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const config = req.app.get('config')
|
||||||
const userRepository = req.app.get('userRepository')
|
const userRepository = req.app.get('userRepository')
|
||||||
const inboxLock = req.app.get('inboxLock')
|
const inboxLock = req.app.get('inboxLock')
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
const Helper = require('../../../application/helper')
|
const Helper = require('../../../application/helper')
|
||||||
const helper = new Helper()
|
const helper = new Helper()
|
||||||
|
|
||||||
|
// In UX debug mode, reset mock data to initial state only on fresh page load
|
||||||
|
// (not on redirects after form submissions)
|
||||||
|
if (config.uxDebugMode && userRepository && userRepository.reset) {
|
||||||
|
// Check if this is a redirect from a form submission
|
||||||
|
const isRedirect = req.session.accountSuccess || req.session.accountError
|
||||||
|
|
||||||
|
if (!isRedirect) {
|
||||||
|
// This is a fresh page load, reset to initial state
|
||||||
|
userRepository.reset()
|
||||||
|
if (inboxLock && inboxLock.reset) {
|
||||||
|
inboxLock.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get user's verified forwarding emails
|
// Get user's verified forwarding emails
|
||||||
const forwardEmails = userRepository.getForwardEmails(req.session.userId)
|
const forwardEmails = userRepository.getForwardEmails(req.session.userId)
|
||||||
|
|
||||||
|
|
@ -24,7 +40,6 @@ router.get('/account', requireAuth, async(req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user stats
|
// Get user stats
|
||||||
const config = req.app.get('config')
|
|
||||||
const stats = userRepository.getUserStats(req.session.userId, config.user)
|
const stats = userRepository.getUserStats(req.session.userId, config.user)
|
||||||
|
|
||||||
const successMessage = req.session.accountSuccess
|
const successMessage = req.session.accountSuccess
|
||||||
|
|
@ -225,6 +240,13 @@ router.post('/account/change-password',
|
||||||
body('confirmNewPassword').notEmpty().withMessage('Password confirmation is required'),
|
body('confirmNewPassword').notEmpty().withMessage('Password confirmation is required'),
|
||||||
async(req, res) => {
|
async(req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const config = req.app.get('config')
|
||||||
|
// Block password change in UX debug mode
|
||||||
|
if (config.uxDebugMode) {
|
||||||
|
req.session.accountError = 'Password changes are disabled in UX debug mode'
|
||||||
|
return res.redirect('/account')
|
||||||
|
}
|
||||||
|
|
||||||
const errors = validationResult(req)
|
const errors = validationResult(req)
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
req.session.accountError = errors.array()[0].msg
|
req.session.accountError = errors.array()[0].msg
|
||||||
|
|
@ -278,6 +300,13 @@ router.post('/account/delete',
|
||||||
body('confirmText').equals('DELETE').withMessage('You must type DELETE to confirm'),
|
body('confirmText').equals('DELETE').withMessage('You must type DELETE to confirm'),
|
||||||
async(req, res) => {
|
async(req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const config = req.app.get('config')
|
||||||
|
// Block account deletion in UX debug mode
|
||||||
|
if (config.uxDebugMode) {
|
||||||
|
req.session.accountError = 'Account deletion is disabled in UX debug mode'
|
||||||
|
return res.redirect('/account')
|
||||||
|
}
|
||||||
|
|
||||||
const errors = validationResult(req)
|
const errors = validationResult(req)
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
req.session.accountError = errors.array()[0].msg
|
req.session.accountError = errors.array()[0].msg
|
||||||
|
|
@ -321,4 +350,4 @@ router.post('/account/delete',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
@ -37,7 +37,12 @@ const server = http.createServer(app)
|
||||||
const io = socketio(server)
|
const io = socketio(server)
|
||||||
|
|
||||||
app.set('socketio', io)
|
app.set('socketio', io)
|
||||||
app.use(logger('dev'))
|
|
||||||
|
// HTTP request logging - only enable with DEBUG environment variable
|
||||||
|
if (process.env.DEBUG && process.env.DEBUG.includes('48hr-email')) {
|
||||||
|
app.use(logger('dev'))
|
||||||
|
}
|
||||||
|
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use(express.urlencoded({ extended: false }))
|
app.use(express.urlencoded({ extended: false }))
|
||||||
|
|
||||||
|
|
@ -200,6 +205,9 @@ server.on('listening', () => {
|
||||||
const addr = server.address()
|
const addr = server.address()
|
||||||
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
|
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
|
||||||
debug('Listening on ' + bind)
|
debug('Listening on ' + bind)
|
||||||
|
|
||||||
|
// Emit event for app.js to display startup banner
|
||||||
|
server.emit('ready')
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = { app, io, server }
|
module.exports = { app, io, server }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "48hr.email",
|
"name": "48hr.email",
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "48hr.email is your favorite open-source tempmail client.",
|
"description": "48hr.email is your favorite open-source tempmail client.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --trace-warnings ./app.js",
|
"start": "node --trace-warnings ./app.js",
|
||||||
"debug": "DEBUG=48hr-email:* node --nolazy --inspect-brk=9229 --trace-warnings ./app.js",
|
"debug": "DEBUG=48hr-email:* node --nolazy --inspect-brk=9229 --trace-warnings ./app.js",
|
||||||
|
"ux-debug": "UX_DEBUG_MODE=true node --trace-warnings ./app.js",
|
||||||
"test": "xo",
|
"test": "xo",
|
||||||
"env:check": "node scripts/check-env.js"
|
"env:check": "node scripts/check-env.js"
|
||||||
},
|
},
|
||||||
|
|
@ -80,4 +81,4 @@
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue