48hr.email/infrastructure/web/routes/account.js
ClaraCrazy 197d9b923e
[Feat]: Bring back performance V2
Electric Boogaloo
2026-01-05 05:21:18 +01:00

328 lines
12 KiB
JavaScript

// 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 purge time for footer
const purgeTime = helper.purgeTimeElemetBuilder()
res.render('account', {
title: 'Account Dashboard',
username: req.session.username,
forwardEmails,
lockedInboxes,
stats,
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
purgeTime: purgeTime,
smtpEnabled: config.email.features.smtp,
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
const branding = (config.http.features.branding || ['48hr.email'])[0]
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')
}
}
)
// POST /account/change-password - Change user password
router.post('/account/change-password',
requireAuth,
body('currentPassword').notEmpty().withMessage('Current password is required'),
body('newPassword').isLength({ min: 8 }).withMessage('New password must be at least 8 characters'),
body('confirmNewPassword').notEmpty().withMessage('Password confirmation is required'),
async(req, res) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
req.session.accountError = errors.array()[0].msg
return res.redirect('/account')
}
const { currentPassword, newPassword, confirmNewPassword } = req.body
// Check if new passwords match
if (newPassword !== confirmNewPassword) {
req.session.accountError = 'New passwords do not match'
return res.redirect('/account')
}
// Validate new password strength
const hasUpperCase = /[A-Z]/.test(newPassword)
const hasLowerCase = /[a-z]/.test(newPassword)
const hasNumber = /[0-9]/.test(newPassword)
if (!hasUpperCase || !hasLowerCase || !hasNumber) {
req.session.accountError = 'Password must include uppercase, lowercase, and number'
return res.redirect('/account')
}
const userRepository = req.app.get('userRepository')
// Verify current password
const isValidPassword = await userRepository.verifyPassword(req.session.userId, currentPassword)
if (!isValidPassword) {
req.session.accountError = 'Current password is incorrect'
return res.redirect('/account')
}
// Update password
await userRepository.updatePassword(req.session.userId, newPassword)
req.session.accountSuccess = 'Password updated successfully'
res.redirect('/account')
} catch (error) {
console.error('Change password error:', error)
req.session.accountError = 'Failed to change password. Please try again.'
res.redirect('/account')
}
}
)
// POST /account/delete - Permanently delete user account
router.post('/account/delete',
requireAuth,
body('password').notEmpty().withMessage('Password is required'),
body('confirmText').equals('DELETE').withMessage('You must type DELETE to confirm'),
async(req, res) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
req.session.accountError = errors.array()[0].msg
return res.redirect('/account')
}
const { password } = req.body
const userRepository = req.app.get('userRepository')
// Verify password
const isValidPassword = await userRepository.verifyPassword(req.session.userId, password)
if (!isValidPassword) {
req.session.accountError = 'Incorrect password'
return res.redirect('/account')
}
// Get user's locked inboxes to release them
const inboxLock = req.app.get('inboxLock')
if (inboxLock) {
const lockedInboxes = inboxLock.getUserLockedInboxes(req.session.userId)
for (const inbox of lockedInboxes) {
inboxLock.release(req.session.userId, inbox.address)
}
}
// Delete user account
await userRepository.deleteUser(req.session.userId)
// Destroy session
req.session.destroy((err) => {
if (err) {
console.error('Session destroy error:', err)
}
res.redirect('/?deleted=true')
})
} catch (error) {
console.error('Delete account error:', error)
req.session.accountError = 'Failed to delete account. Please try again.'
res.redirect('/account')
}
}
)
module.exports = router