mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
[Feat]: V2
Updated: - Update UI, - Update routes - Update filters New: - Add Password change route - Add Account deletion button
This commit is contained in:
parent
3fdf5bf61b
commit
2f58eacfa7
17 changed files with 1209 additions and 135 deletions
|
|
@ -249,7 +249,7 @@ class Helper {
|
||||||
|
|
||||||
// Warn about old locked-inboxes.db
|
// Warn about old locked-inboxes.db
|
||||||
if (fs.existsSync(legacyLockedInboxesDb)) {
|
if (fs.existsSync(legacyLockedInboxesDb)) {
|
||||||
console.log(`⚠️ Found legacy ${legacyLockedInboxesDb}`)
|
console.log(`WARNING: Found legacy ${legacyLockedInboxesDb}`)
|
||||||
console.log(` This database is no longer used. Locks are now stored in ${path.basename(dbPath)}.`)
|
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.`)
|
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)')
|
debug('Legacy locked-inboxes.db detected but not migrated (data already in user_locked_inboxes table)')
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ ${mail.html}
|
||||||
<p><code>${verificationLink}</code></p>
|
<p><code>${verificationLink}</code></p>
|
||||||
|
|
||||||
<div class="warning">
|
<div class="warning">
|
||||||
⚠️ <strong>Important:</strong> This verification link expires in <strong>15 minutes</strong>. Once verified, you'll be able to forward emails to this address for 24 hours.
|
<strong>Important:</strong> This verification link expires in <strong>15 minutes</strong>. Once verified, you'll be able to forward emails to this address for 24 hours.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>If you didn't request this verification, you can safely ignore this email.</p>
|
<p>If you didn't request this verification, you can safely ignore this email.</p>
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,100 @@ class UserRepository {
|
||||||
return `${Math.floor(days / 365)} years`
|
return `${Math.floor(days / 365)} years`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify user password
|
||||||
|
* @param {number} userId - User ID
|
||||||
|
* @param {string} password - Plain text password to verify
|
||||||
|
* @returns {Promise<boolean>} - True if password matches
|
||||||
|
*/
|
||||||
|
async verifyPassword(userId, password) {
|
||||||
|
try {
|
||||||
|
const bcrypt = require('bcrypt')
|
||||||
|
const stmt = this.db.prepare('SELECT password_hash FROM users WHERE id = ?')
|
||||||
|
const user = stmt.get(userId)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
debug(`User not found for password verification: ${userId}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await bcrypt.compare(password, user.password_hash)
|
||||||
|
debug(`Password verification for user ${userId}: ${isValid ? 'success' : 'failed'}`)
|
||||||
|
return isValid
|
||||||
|
} catch (error) {
|
||||||
|
debug(`Error verifying password: ${error.message}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user password
|
||||||
|
* @param {number} userId - User ID
|
||||||
|
* @param {string} newPassword - New plain text password
|
||||||
|
* @returns {Promise<boolean>} - True if successful
|
||||||
|
*/
|
||||||
|
async updatePassword(userId, newPassword) {
|
||||||
|
try {
|
||||||
|
const bcrypt = require('bcrypt')
|
||||||
|
const saltRounds = 10
|
||||||
|
const passwordHash = await bcrypt.hash(newPassword, saltRounds)
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`)
|
||||||
|
const result = stmt.run(passwordHash, userId)
|
||||||
|
|
||||||
|
if (result.changes > 0) {
|
||||||
|
debug(`Password updated for user ${userId}`)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
debug(`User not found for password update: ${userId}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debug(`Error updating password: ${error.message}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete user account and all associated data
|
||||||
|
* @param {number} userId - User ID
|
||||||
|
* @returns {boolean} - True if successful
|
||||||
|
*/
|
||||||
|
deleteUser(userId) {
|
||||||
|
try {
|
||||||
|
// Delete in order due to foreign key constraints:
|
||||||
|
// 1. forward_emails (references users.id)
|
||||||
|
// 2. users
|
||||||
|
|
||||||
|
const deleteForwardEmails = this.db.prepare('DELETE FROM forward_emails WHERE user_id = ?')
|
||||||
|
const deleteUser = this.db.prepare('DELETE FROM users WHERE id = ?')
|
||||||
|
|
||||||
|
// Use transaction for atomicity
|
||||||
|
const deleteTransaction = this.db.transaction((uid) => {
|
||||||
|
deleteForwardEmails.run(uid)
|
||||||
|
const result = deleteUser.run(uid)
|
||||||
|
return result.changes > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const success = deleteTransaction(userId)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
debug(`User ${userId} and all associated data deleted`)
|
||||||
|
} else {
|
||||||
|
debug(`User ${userId} not found for deletion`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return success
|
||||||
|
} catch (error) {
|
||||||
|
debug(`Error deleting user: ${error.message}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close database connection
|
* Close database connection
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -428,7 +428,8 @@ text-muted {
|
||||||
/* Auth pages */
|
/* Auth pages */
|
||||||
|
|
||||||
.auth-container {
|
.auth-container {
|
||||||
max-width: 900px;
|
min-width: 75%;
|
||||||
|
max-width: 1500px;
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -468,9 +469,10 @@ text-muted {
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-card small {
|
.auth-card small {
|
||||||
|
margin-top: -10px !important;
|
||||||
|
font-size: 1.2rem;
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--color-text-gray);
|
color: var(--color-text-gray);
|
||||||
font-size: 0.85rem;
|
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
@ -561,7 +563,8 @@ text-muted {
|
||||||
/* Unified auth page (side-by-side login/register) */
|
/* Unified auth page (side-by-side login/register) */
|
||||||
|
|
||||||
.auth-unified-container {
|
.auth-unified-container {
|
||||||
max-width: 1100px;
|
min-width: 75%;
|
||||||
|
max-width: 1500px;
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -581,6 +584,7 @@ text-muted {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 3rem;
|
gap: 3rem;
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
@ -590,6 +594,27 @@ text-muted {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card .button {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-card h2 {
|
.auth-card h2 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
|
@ -741,7 +766,8 @@ text-muted {
|
||||||
/* Account dashboard page */
|
/* Account dashboard page */
|
||||||
|
|
||||||
.account-container {
|
.account-container {
|
||||||
max-width: 1200px;
|
min-width: 75%;
|
||||||
|
max-width: 1500px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -763,12 +789,14 @@ text-muted {
|
||||||
border: 1px solid var(--color-border-dark);
|
border: 1px solid var(--color-border-dark);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-card h2 {
|
.account-card h2 {
|
||||||
color: var(--color-accent-purple);
|
color: var(--color-accent-purple);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-size: 1.5rem;
|
font-size: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-description {
|
.card-description {
|
||||||
|
|
@ -777,7 +805,7 @@ text-muted {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-stats {
|
.account-card:nth-child(1) {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -910,6 +938,87 @@ form {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-form fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-form label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-form input {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--color-border-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--color-bg-dark);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-form small {
|
||||||
|
color: var(--color-text-gray);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-form .button {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone {
|
||||||
|
border: 2px solid var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone h2 {
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-content {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-list li {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-list li::before {
|
||||||
|
content: '•';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--color-danger);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-content .button {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.account-grid {
|
.account-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
@ -990,6 +1099,390 @@ select:hover {
|
||||||
background-image: none;
|
background-image: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Frosted Glass Utility Class */
|
||||||
|
|
||||||
|
.frosted-glass {
|
||||||
|
background: linear-gradient(135deg, var(--overlay-white-08), var(--overlay-white-04));
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid var(--overlay-white-15);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 8px 32px var(--overlay-black-40), inset 0 1px 0 var(--overlay-white-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .frosted-glass {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.5));
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Homepage Styles */
|
||||||
|
|
||||||
|
.homepage-container {
|
||||||
|
min-width: 75%;
|
||||||
|
max-width: 1500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
padding: 4rem 2rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 4.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inbox-creator {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto 6rem;
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creator-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.4rem;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inbox-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
padding-left: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"] {
|
||||||
|
padding: 1.4rem 1.6rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
border: 1px solid var(--overlay-purple-30);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--overlay-white-04);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent-purple-light);
|
||||||
|
background: var(--overlay-white-06);
|
||||||
|
box-shadow: 0 0 0 3px var(--overlay-purple-15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper::after {
|
||||||
|
content: '▾';
|
||||||
|
position: absolute;
|
||||||
|
right: 1.6rem;
|
||||||
|
top: 35%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group select {
|
||||||
|
padding-left: 1.6rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
border: 1px solid var(--overlay-purple-30);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--overlay-white-04);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group select:hover {
|
||||||
|
background: var(--overlay-white-06);
|
||||||
|
border-color: var(--overlay-purple-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent-purple-light);
|
||||||
|
box-shadow: 0 0 0 3px var(--overlay-purple-15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group select option {
|
||||||
|
background: var(--color-bg-dark);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-count {
|
||||||
|
margin-top: -10px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--color-text-dimmer);
|
||||||
|
padding-left: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.2rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
padding: 1.4rem 2.4rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
flex: 1;
|
||||||
|
height: 5.2rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn svg {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--color-accent-purple), var(--color-accent-purple-bright));
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 16px var(--overlay-purple-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px var(--overlay-purple-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover svg {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--overlay-white-08);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
border: 1px solid var(--overlay-purple-25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--overlay-white-12);
|
||||||
|
border-color: var(--overlay-purple-35);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover svg {
|
||||||
|
transform: rotate(15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.2rem;
|
||||||
|
padding: 1.4rem 1.8rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: rgba(46, 204, 113, 0.1);
|
||||||
|
border: 1px solid #2ecc71;
|
||||||
|
color: #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: rgba(176, 0, 0, 0.1);
|
||||||
|
border: 1px solid var(--color-error);
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background: var(--overlay-warning-10);
|
||||||
|
border: 1px solid var(--color-warning);
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert span {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
padding: 3rem 2.5rem;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 12px 40px var(--overlay-black-45), inset 0 1px 0 var(--overlay-white-15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Info Section */
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-top: 5rem;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content {
|
||||||
|
padding: 4rem 3rem;
|
||||||
|
min-width: 75%;
|
||||||
|
max-width: 1500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content h2 {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content h3 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--color-accent-purple-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content p {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content ul li {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content ul li::before {
|
||||||
|
content: '•';
|
||||||
|
position: absolute;
|
||||||
|
left: 0.5rem;
|
||||||
|
color: var(--color-accent-purple);
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content ul li strong {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content .note {
|
||||||
|
background: var(--overlay-purple-10);
|
||||||
|
border-left: 4px solid var(--color-accent-purple);
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3.2rem;
|
||||||
|
}
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
.inbox-creator {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.info-content {
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
}
|
||||||
|
.info-content h2 {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
}
|
||||||
|
.info-content h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Legacy Login Styles (kept for compatibility) */
|
||||||
|
|
||||||
#login {
|
#login {
|
||||||
padding-top: 15vh;
|
padding-top: 15vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1596,6 +2089,8 @@ label {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
box-shadow: 0 4px 6px var(--overlay-black-30);
|
box-shadow: 0 4px 6px var(--overlay-black-30);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content h3 {
|
.modal-content h3 {
|
||||||
|
|
@ -1646,6 +2141,23 @@ label {
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content fieldset {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content .button,
|
||||||
|
.modal-content .modal-button {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-input {
|
.modal-input {
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
|
|
@ -1969,7 +2481,8 @@ body.light-mode .theme-icon-light {
|
||||||
/* Statistics Page */
|
/* Statistics Page */
|
||||||
|
|
||||||
.stats-container {
|
.stats-container {
|
||||||
max-width: 1200px;
|
min-width: 75%;
|
||||||
|
max-width: 1500px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
@ -2047,12 +2560,57 @@ body.light-mode .theme-icon-light {
|
||||||
.hamburger-menu {
|
.hamburger-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
/* Hide dropdowns on mobile, show links directly */
|
||||||
|
.action-dropdown {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.action-dropdown .dropdown-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.action-dropdown .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
position: static;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
min-width: auto;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Add section headers before dropdown menus on mobile */
|
||||||
|
.action-dropdown .dropdown-menu::before {
|
||||||
|
content: attr(data-section-title);
|
||||||
|
display: block;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
padding: 12px 0 8px 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-top: 1px solid var(--overlay-white-10);
|
||||||
|
}
|
||||||
|
.action-dropdown:first-of-type .dropdown-menu::before {
|
||||||
|
margin-top: 0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.action-dropdown .dropdown-menu a {
|
||||||
|
padding: 10px 0;
|
||||||
|
text-align: left;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.action-dropdown .dropdown-menu a:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-accent-purple-light);
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
.action-links.mobile-open .theme-toggle {
|
.action-links.mobile-open .theme-toggle {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.action-links.mobile-hidden>a,
|
.action-links.mobile-hidden>a,
|
||||||
.action-links.mobile-hidden>button:not(.hamburger-menu) {
|
.action-links.mobile-hidden>button:not(.hamburger-menu),
|
||||||
|
.action-links.mobile-hidden>.action-dropdown {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.action-links.mobile-open {
|
.action-links.mobile-open {
|
||||||
|
|
@ -2066,7 +2624,7 @@ body.light-mode .theme-icon-light {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
box-shadow: 0 10px 40px var(--overlay-black-40);
|
box-shadow: 0 10px 40px var(--overlay-black-40);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
min-width: 200px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
.action-links.mobile-open>.hamburger-menu {
|
.action-links.mobile-open>.hamburger-menu {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -2075,7 +2633,24 @@ body.light-mode .theme-icon-light {
|
||||||
.action-links.mobile-open>button:not(.hamburger-menu) {
|
.action-links.mobile-open>button:not(.hamburger-menu) {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
|
padding: 10px 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
height: auto;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
}
|
||||||
|
.action-links.mobile-open>a:hover,
|
||||||
|
.action-links.mobile-open>button:not(.hamburger-menu):hover {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-accent-purple-light);
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
.qr-icon-btn {
|
.qr-icon-btn {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ router.post('/account/forward-email/add',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Send verification email
|
// Send verification email
|
||||||
const baseUrl = config.http.baseUrl || 'http://localhost:3000'
|
const baseUrl = config.http.baseUrl
|
||||||
const branding = config.http.branding[0] || '48hr.email'
|
const branding = config.http.branding[0]
|
||||||
|
|
||||||
await smtpService.sendVerificationEmail(
|
await smtpService.sendVerificationEmail(
|
||||||
email,
|
email,
|
||||||
|
|
@ -220,4 +220,108 @@ router.post('/account/locked-inbox/release',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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
|
module.exports = router
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ const router = new express.Router()
|
||||||
const { body, validationResult } = require('express-validator')
|
const { body, validationResult } = require('express-validator')
|
||||||
const debug = require('debug')('48hr-email:auth-routes')
|
const debug = require('debug')('48hr-email:auth-routes')
|
||||||
const { redirectIfAuthenticated } = require('../middleware/auth')
|
const { redirectIfAuthenticated } = require('../middleware/auth')
|
||||||
|
const config = require('../../../application/config')
|
||||||
|
const Helper = require('../../../application/helper')
|
||||||
|
const helper = new Helper()
|
||||||
|
|
||||||
|
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||||
|
|
||||||
// Simple in-memory rate limiters for registration and login
|
// Simple in-memory rate limiters for registration and login
|
||||||
const registrationRateLimitStore = new Map()
|
const registrationRateLimitStore = new Map()
|
||||||
|
|
@ -87,6 +92,7 @@ router.get('/auth', redirectIfAuthenticated, (req, res) => {
|
||||||
res.render('auth', {
|
res.render('auth', {
|
||||||
title: `Login or Register | ${config.http.branding[0]}`,
|
title: `Login or Register | ${config.http.branding[0]}`,
|
||||||
branding: config.http.branding,
|
branding: config.http.branding,
|
||||||
|
purgeTime: purgeTime,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
successMessage
|
successMessage
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ router.get('/', async(req, res, next) => {
|
||||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
||||||
username: randomWord(),
|
username: randomWord(),
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
|
purgeTimeRaw: config.email.purgeTime,
|
||||||
domains: helper.getDomains(),
|
domains: helper.getDomains(),
|
||||||
branding: config.http.branding,
|
branding: config.http.branding,
|
||||||
example: config.email.examples.account,
|
example: config.email.examples.account,
|
||||||
|
|
@ -59,6 +60,7 @@ router.post(
|
||||||
userInputError: true,
|
userInputError: true,
|
||||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
|
purgeTimeRaw: config.email.purgeTime,
|
||||||
username: randomWord(),
|
username: randomWord(),
|
||||||
domains: helper.getDomains(),
|
domains: helper.getDomains(),
|
||||||
branding: config.http.branding,
|
branding: config.http.branding,
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,28 @@
|
||||||
<p class="account-subtitle">Welcome back, <strong>{{ username }}</strong></p>
|
<p class="account-subtitle">Welcome back, <strong>{{ username }}</strong></p>
|
||||||
|
|
||||||
{% if successMessage %}
|
{% if successMessage %}
|
||||||
<div class="success-message">
|
<div class="alert alert-success">
|
||||||
{{ successMessage }}
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
|
<path d="M20 6L9 17l-5-5"></path>
|
||||||
|
</svg>
|
||||||
|
<p>{{ successMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if errorMessage %}
|
{% if errorMessage %}
|
||||||
<div class="unlock-error">
|
<div class="alert alert-error">
|
||||||
{{ errorMessage }}
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||||
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
<p>{{ errorMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="account-grid">
|
<div class="account-grid">
|
||||||
<!-- Account Stats -->
|
<!-- Account Stats -->
|
||||||
<div class="account-card account-stats">
|
<div class="account-card frosted-glass">
|
||||||
<h2>Account Overview</h2>
|
<h2>Account Overview</h2>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
|
|
@ -52,7 +60,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Forwarding Emails Section -->
|
<!-- Forwarding Emails Section -->
|
||||||
<div class="account-card">
|
<div class="account-card frosted-glass">
|
||||||
<h2>Forwarding Emails</h2>
|
<h2>Forwarding Emails</h2>
|
||||||
<p class="card-description">Add verified emails to forward messages to. Each email must be verified before use.</p>
|
<p class="card-description">Add verified emails to forward messages to. Each email must be verified before use.</p>
|
||||||
|
|
||||||
|
|
@ -85,7 +93,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Locked Inboxes Section -->
|
<!-- Locked Inboxes Section -->
|
||||||
<div class="account-card">
|
<div class="account-card frosted-glass">
|
||||||
<h2>Locked Inboxes</h2>
|
<h2>Locked Inboxes</h2>
|
||||||
<p class="card-description">Manage your locked inboxes. These are protected by your account and only accessible when logged in. Locks auto-release after 7 days without login.</p>
|
<p class="card-description">Manage your locked inboxes. These are protected by your account and only accessible when logged in. Locks auto-release after 7 days without login.</p>
|
||||||
|
|
||||||
|
|
@ -116,6 +124,105 @@
|
||||||
<p class="limit-reached">Maximum {{ stats.maxLockedInboxes }} inboxes locked</p>
|
<p class="limit-reached">Maximum {{ stats.maxLockedInboxes }} inboxes locked</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Change Password Section -->
|
||||||
|
<div class="account-card frosted-glass">
|
||||||
|
<h2>Change Password</h2>
|
||||||
|
<p class="card-description">Update your account password. You'll need to enter your current password to confirm.</p>
|
||||||
|
|
||||||
|
<form method="POST" action="/account/change-password" class="password-form">
|
||||||
|
<fieldset>
|
||||||
|
<label for="currentPassword">Current Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="currentPassword"
|
||||||
|
name="currentPassword"
|
||||||
|
placeholder="Enter current password"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label for="newPassword">New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="newPassword"
|
||||||
|
name="newPassword"
|
||||||
|
placeholder="Min 8 characters"
|
||||||
|
required
|
||||||
|
minlength="8"
|
||||||
|
autocomplete="new-password"
|
||||||
|
>
|
||||||
|
<small>Must include uppercase, lowercase, and number</small>
|
||||||
|
|
||||||
|
<label for="confirmNewPassword">Confirm New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="confirmNewPassword"
|
||||||
|
name="confirmNewPassword"
|
||||||
|
placeholder="Re-enter new password"
|
||||||
|
required
|
||||||
|
minlength="8"
|
||||||
|
autocomplete="new-password"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button type="submit" class="button button-primary">Update Password</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Account Section -->
|
||||||
|
<div class="account-card frosted-glass danger-zone">
|
||||||
|
<h2>Danger Zone</h2>
|
||||||
|
<p class="card-description">Permanently delete your account and all associated data. This action cannot be undone.</p>
|
||||||
|
|
||||||
|
<div class="danger-content">
|
||||||
|
<p><strong>Warning:</strong> Deleting your account will:</p>
|
||||||
|
<ul class="danger-list">
|
||||||
|
<li>Remove all forwarding email addresses</li>
|
||||||
|
<li>Release all locked inboxes</li>
|
||||||
|
<li>Permanently delete your account data</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="button button-danger button-full-width" id="deleteAccountBtn">Delete Account</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Account Modal -->
|
||||||
|
<div id="deleteAccountModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" id="closeDeleteAccount">×</span>
|
||||||
|
<h3>Delete Account</h3>
|
||||||
|
<p class="modal-description" style="color: var(--color-danger);">This action is permanent and cannot be undone!</p>
|
||||||
|
|
||||||
|
<form method="POST" action="/account/delete">
|
||||||
|
<fieldset>
|
||||||
|
<label for="confirmPassword">Enter your password to confirm</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="confirmPassword"
|
||||||
|
name="password"
|
||||||
|
placeholder="Your password"
|
||||||
|
required
|
||||||
|
class="modal-input"
|
||||||
|
autocomplete="current-password"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label for="confirmText">Type "DELETE" to confirm</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="confirmText"
|
||||||
|
name="confirmText"
|
||||||
|
placeholder="Type DELETE"
|
||||||
|
required
|
||||||
|
class="modal-input"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button type="submit" class="button button-danger modal-button">Permanently Delete Account</button>
|
||||||
|
<button type="button" class="button button-secondary modal-button" id="cancelDelete">Cancel</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -161,10 +268,37 @@ if (closeAddEmail) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete Account Modal
|
||||||
|
const deleteAccountBtn = document.getElementById('deleteAccountBtn');
|
||||||
|
const deleteAccountModal = document.getElementById('deleteAccountModal');
|
||||||
|
const closeDeleteAccount = document.getElementById('closeDeleteAccount');
|
||||||
|
const cancelDelete = document.getElementById('cancelDelete');
|
||||||
|
|
||||||
|
if (deleteAccountBtn) {
|
||||||
|
deleteAccountBtn.onclick = function() {
|
||||||
|
deleteAccountModal.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeDeleteAccount) {
|
||||||
|
closeDeleteAccount.onclick = function() {
|
||||||
|
deleteAccountModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelDelete) {
|
||||||
|
cancelDelete.onclick = function() {
|
||||||
|
deleteAccountModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
if (event.target == addEmailModal) {
|
if (event.target == addEmailModal) {
|
||||||
addEmailModal.style.display = 'none';
|
addEmailModal.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
if (event.target == deleteAccountModal) {
|
||||||
|
deleteAccountModal.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -20,50 +20,29 @@
|
||||||
<h1 class="page-title">Account Access</h1>
|
<h1 class="page-title">Account Access</h1>
|
||||||
<p class="auth-subtitle">Login to an existing account or create a new one</p>
|
<p class="auth-subtitle">Login to an existing account or create a new one</p>
|
||||||
{% if errorMessage %}
|
{% if errorMessage %}
|
||||||
<div class="unlock-error">{{ errorMessage }}</div>
|
<div class="alert alert-error">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||||
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if successMessage %}
|
{% if successMessage %}
|
||||||
<div class="success-message">{{ successMessage }}</div>
|
<div class="alert alert-success">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
|
<path d="M20 6L9 17l-5-5"></path>
|
||||||
|
</svg>
|
||||||
|
{{ successMessage }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-forms-grid">
|
<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="login-password">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="login-password"
|
|
||||||
name="password"
|
|
||||||
placeholder="Your password"
|
|
||||||
required
|
|
||||||
autocomplete="current-password"
|
|
||||||
>
|
|
||||||
|
|
||||||
<button class="button button-primary" type="submit">Login</button>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Register Form -->
|
<!-- Register Form -->
|
||||||
<div class="auth-card">
|
<div class="auth-card frosted-glass">
|
||||||
<h2>Register</h2>
|
<h2>Register</h2>
|
||||||
<p class="auth-card-subtitle">Create a new account</p>
|
|
||||||
|
|
||||||
<form method="POST" action="/register">
|
<form method="POST" action="/register">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
@ -108,10 +87,41 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Form -->
|
||||||
|
<div class="auth-card frosted-glass">
|
||||||
|
<h2>Login</h2>
|
||||||
|
|
||||||
|
<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="login-password">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="login-password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Your password"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button class="button button-primary" type="submit">Login</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-features-unified">
|
<div class="auth-features-unified">
|
||||||
<h3>✓ Account Benefits</h3>
|
<h3>Account Benefits</h3>
|
||||||
<div class="features-grid">
|
<div class="features-grid">
|
||||||
<div class="feature-item">Forward emails to verified addresses</div>
|
<div class="feature-item">Forward emails to verified addresses</div>
|
||||||
<div class="feature-item">Lock up to 5 inboxes to your account</div>
|
<div class="feature-item">Lock up to 5 inboxes to your account</div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
{% if authEnabled %}
|
{% if authEnabled %}
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu" data-section-title="Account">
|
||||||
<a href="/account" aria-label="Account settings">Settings</a>
|
<a href="/account" aria-label="Account settings">Settings</a>
|
||||||
<a href="/logout?redirect=/" aria-label="Logout">Logout</a>
|
<a href="/logout?redirect=/" aria-label="Logout">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<!-- Inbox Dropdown (multiple actions when logged in) -->
|
<!-- Inbox Dropdown (multiple actions when logged in) -->
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Inbox actions">Inbox ▾</button>
|
<button class="dropdown-toggle" aria-label="Inbox actions">Inbox ▾</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu" data-section-title="Inbox Actions">
|
||||||
<a href="#" id="forwardAllBtn" aria-label="Forward all emails">Forward All</a>
|
<a href="#" id="forwardAllBtn" aria-label="Forward all emails">Forward All</a>
|
||||||
{% if authEnabled %}
|
{% if authEnabled %}
|
||||||
{% if isLocked and hasAccess %}
|
{% if isLocked and hasAccess %}
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
{% if authEnabled %}
|
{% if authEnabled %}
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu" data-section-title="Account">
|
||||||
<a href="/account" aria-label="Account settings">Settings</a>
|
<a href="/account" aria-label="Account settings">Settings</a>
|
||||||
<a href="/logout?redirect={{ ('/inbox/' ~ address) | url_encode }}" aria-label="Logout">Logout</a>
|
<a href="/logout?redirect={{ ('/inbox/' ~ address) | url_encode }}" aria-label="Logout">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -53,8 +53,11 @@
|
||||||
<script src="/javascripts/qrcode.js"></script>
|
<script src="/javascripts/qrcode.js"></script>
|
||||||
<script src="/javascripts/inbox-init.js" defer data-address="{{ address }}" data-expiry-time="{{ expiryTime }}" data-expiry-unit="{{ expiryUnit }}" data-refresh-interval="{{ refreshInterval }}"></script>
|
<script src="/javascripts/inbox-init.js" defer data-address="{{ address }}" data-expiry-time="{{ expiryTime }}" data-expiry-unit="{{ expiryUnit }}" data-refresh-interval="{{ refreshInterval }}"></script>
|
||||||
{% if forwardAllSuccess %}
|
{% if forwardAllSuccess %}
|
||||||
<div class="success-message">
|
<div class="alert alert-success">
|
||||||
✓ Successfully forwarded {{ forwardAllSuccess }} email(s)!
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
|
<path d="M20 6L9 17l-5-5"></path>
|
||||||
|
</svg>
|
||||||
|
Successfully forwarded {{ forwardAllSuccess }} email(s)!
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if verificationSent %}
|
{% if verificationSent %}
|
||||||
|
|
@ -63,7 +66,12 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if errorMessage %}
|
{% if errorMessage %}
|
||||||
<div class="unlock-error">
|
<div class="alert alert-error">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||||
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||||
|
</svg>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -84,7 +92,7 @@
|
||||||
<div class="emails-container">
|
<div class="emails-container">
|
||||||
{% for mail in mailSummaries %}
|
{% for mail in mailSummaries %}
|
||||||
<a href="{{ mail.to[0] }}/{{ mail.uid }}" class="email-link">
|
<a href="{{ mail.to[0] }}/{{ mail.uid }}" class="email-link">
|
||||||
<div class="email-card">
|
<div class="email-card frosted-glass">
|
||||||
<div class="email-header">
|
<div class="email-header">
|
||||||
<div class="email-sender">
|
<div class="email-sender">
|
||||||
<div class="sender-name">{{ mail.from[0].name }}</div>
|
<div class="sender-name">{{ mail.from[0].name }}</div>
|
||||||
|
|
@ -111,20 +119,20 @@
|
||||||
{% if authEnabled and not isLocked %}
|
{% if authEnabled and not isLocked %}
|
||||||
<!-- Lock Modal -->
|
<!-- Lock Modal -->
|
||||||
<div id="lockModal" class="modal" style="display: none;" data-lock-error="{{ error|default('') }}">
|
<div id="lockModal" class="modal" style="display: none;" data-lock-error="{{ error|default('') }}">
|
||||||
<div class="modal-content">
|
<div class="modal-content frosted-glass">
|
||||||
<span class="close" id="closeLock">×</span>
|
<span class="close" id="closeLock">×</span>
|
||||||
<h3>Lock Inbox</h3>
|
<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>
|
<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' %}
|
{% if error and error == 'locking_disabled_for_example' %}
|
||||||
<p id="lockServerError" class="unlock-error">Locking is disabled for the example inbox.</p>
|
<p id="lockServerError" class="alert alert-error">Locking is disabled for the example inbox.</p>
|
||||||
{% elseif error and error == 'max_locked_inboxes' %}
|
{% 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>
|
<p id="lockServerError" class="alert alert-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' %}
|
{% elseif error and error == 'already_locked' %}
|
||||||
<p id="lockServerError" class="unlock-error">This inbox is already locked by another user.</p>
|
<p id="lockServerError" class="alert alert-error">This inbox is already locked by another user.</p>
|
||||||
{% elseif error and error == 'not_your_lock' %}
|
{% elseif error and error == 'not_your_lock' %}
|
||||||
<p id="lockServerError" class="unlock-error">You don't own the lock on this inbox.</p>
|
<p id="lockServerError" class="alert alert-error">You don't own the lock on this inbox.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p id="lockErrorInline" class="unlock-error" style="display:none"></p>
|
<p id="lockErrorInline" class="alert alert-error" style="display:none"></p>
|
||||||
<form method="POST" action="/lock/lock">
|
<form method="POST" action="/lock/lock">
|
||||||
<input type="hidden" name="address" value="{{ address }}">
|
<input type="hidden" name="address" value="{{ address }}">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
@ -142,7 +150,7 @@
|
||||||
{% if authEnabled and isLocked and hasAccess %}
|
{% if authEnabled and isLocked and hasAccess %}
|
||||||
<!-- Remove Lock Modal -->
|
<!-- Remove Lock Modal -->
|
||||||
<div id="removeLockModal" class="modal" style="display: none;">
|
<div id="removeLockModal" class="modal" style="display: none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content frosted-glass">
|
||||||
<span class="close" id="closeRemoveLock">×</span>
|
<span class="close" id="closeRemoveLock">×</span>
|
||||||
<h3>Remove Lock</h3>
|
<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>
|
<p class="modal-description">Are you sure you want to remove the lock from this inbox? Anyone will be able to access it.</p>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
{% if authEnabled %}
|
{% if authEnabled %}
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu" data-section-title="Account">
|
||||||
<a href="/account" aria-label="Account settings">Settings</a>
|
<a href="/account" aria-label="Account settings">Settings</a>
|
||||||
<a href="/logout?redirect=/" aria-label="Logout">Logout</a>
|
<a href="/logout?redirect=/" aria-label="Logout">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -32,33 +32,105 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div id="login">
|
<div class="homepage-container">
|
||||||
<h1 class="page-title">Welcome!</h1>
|
<div class="hero-section">
|
||||||
<h4>Here you can either create a new Inbox, or access your old one</h4>
|
<h1 class="page-title hero-title">Your Temporary Inbox</h1>
|
||||||
|
<p class="hero-subtitle">Create instant disposable email addresses. No registration required. Emails auto-delete after {{ purgeTimeRaw|readablePurgeTime }}.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if userInputError %}
|
<div class="inbox-creator frosted-glass">
|
||||||
<blockquote class="warning">
|
<h2 class="creator-title">Get Started</h2>
|
||||||
Your input was invalid. Please try other values.
|
|
||||||
</blockquote>
|
{% if userInputError %}
|
||||||
{% endif %}
|
<div class="alert alert-warning">
|
||||||
<form method="POST" action="/">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
<fieldset>
|
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"></path>
|
||||||
<label for="nameField">Name</label>
|
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||||
<input type="text" id="nameField" name="username" value="{{ username }}">
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||||
<label for="commentField">Domain ({{ domains|length }})</label>
|
</svg>
|
||||||
<div class="dropdown">
|
Your input was invalid. Please try other values.
|
||||||
<select id="commentField" name="domain">
|
|
||||||
{% for domain in domains %}
|
|
||||||
<option value="{{ domain }}">{{ domain }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
{% endif %}
|
||||||
<input class="button" type="submit" value="Access This Inbox">
|
|
||||||
<a class="button" href="/inbox/random">Create Random Inbox</a>
|
<form method="POST" action="/" class="inbox-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="nameField">Choose Your Name</label>
|
||||||
|
<input type="text" id="nameField" name="username" value="{{ username }}" placeholder="e.g., john.doe" required>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
|
||||||
</form>
|
<div class="form-group">
|
||||||
|
<label for="commentField">Select Domain</label>
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select id="commentField" name="domain">
|
||||||
|
{% for domain in domains %}
|
||||||
|
<option value="{{ domain }}">@{{ domain }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<span class="domain-count">{{ domains|length }} domains available</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span>Access Inbox</span>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<a href="/inbox/random" class="btn btn-secondary">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
<span>Random Inbox</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card frosted-glass">
|
||||||
|
<h3>Privacy First</h3>
|
||||||
|
<p>No registration, no tracking. Your temporary email is completely anonymous.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card frosted-glass">
|
||||||
|
<h3>Instant Access</h3>
|
||||||
|
<p>Create unlimited temporary email addresses in seconds. No waiting, no verification.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card frosted-glass">
|
||||||
|
<h3>Auto-Delete</h3>
|
||||||
|
<p>All emails automatically purge after {{ purgeTimeRaw|readablePurgeTime }}. Clean and secure by default.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-content frosted-glass">
|
||||||
|
<h2>What is a Temporary Email?</h2>
|
||||||
|
<p>A temporary email (also known as disposable email or throwaway email) is a service that allows you to receive emails at a temporary address that self-destructs after a certain time. It's perfect for signing up to websites, testing services, or protecting your real inbox from spam.</p>
|
||||||
|
|
||||||
|
<h3>Common Use Cases</h3>
|
||||||
|
<ul class="use-cases-list">
|
||||||
|
<li><strong>Avoid Spam:</strong> Sign up for services without cluttering your primary inbox</li>
|
||||||
|
<li><strong>Test Services:</strong> Try new apps and websites without commitment</li>
|
||||||
|
<li><strong>Online Privacy:</strong> Keep your real email address private from third parties</li>
|
||||||
|
<li><strong>One-Time Verification:</strong> Receive verification codes without long-term exposure</li>
|
||||||
|
<li><strong>Download Content:</strong> Access gated content that requires email confirmation</li>
|
||||||
|
<li><strong>Form Testing:</strong> Test email workflows during development</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Why Choose {{ branding[0] }}?</h3>
|
||||||
|
<ul class="benefits-list">
|
||||||
|
<li><strong>No Sign-Up Required:</strong> Start using immediately without creating an account</li>
|
||||||
|
<li><strong>Multiple Domains:</strong> Choose from several domain options for flexibility</li>
|
||||||
|
<li><strong>Clean Interface:</strong> Simple, modern design focused on usability</li>
|
||||||
|
<li><strong>Real-Time Updates:</strong> See new emails arrive instantly without refreshing</li>
|
||||||
|
<li><strong>Open Source:</strong> Transparent codebase you can review and trust</li>
|
||||||
|
<li><strong>{{ purgeTimeRaw|readablePurgeTime|title }} Retention:</strong> Emails stay accessible for the full duration before auto-deletion</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="note">For extended features like email forwarding and inbox locking, you can optionally create a free account. But for basic temporary email needs, no registration is ever required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<!-- Email Dropdown (multiple actions when logged in) -->
|
<!-- Email Dropdown (multiple actions when logged in) -->
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Email actions">Email ▾</button>
|
<button class="dropdown-toggle" aria-label="Email actions">Email ▾</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu" data-section-title="Email Actions">
|
||||||
<a href="#" id="forwardBtn" aria-label="Forward this email">Forward</a>
|
<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 }}/delete" aria-label="Delete this email">Delete</a>
|
||||||
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
|
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
{% if authEnabled %}
|
{% if authEnabled %}
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu" data-section-title="Account">
|
||||||
<a href="/account" aria-label="Account settings">Settings</a>
|
<a href="/account" aria-label="Account settings">Settings</a>
|
||||||
<a href="/logout?redirect={{ ('/inbox/' ~ address ~ '/' ~ uid) | url_encode }}" aria-label="Logout">Logout</a>
|
<a href="/logout?redirect={{ ('/inbox/' ~ address ~ '/' ~ uid) | url_encode }}" aria-label="Logout">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -48,8 +48,11 @@
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% if forwardSuccess %}
|
{% if forwardSuccess %}
|
||||||
<div class="success-message">
|
<div class="alert alert-success">
|
||||||
✓ Email forwarded successfully!
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
|
<path d="M20 6L9 17l-5-5"></path>
|
||||||
|
</svg>
|
||||||
|
Email forwarded successfully!
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if verificationSent %}
|
{% if verificationSent %}
|
||||||
|
|
@ -125,7 +128,7 @@
|
||||||
|
|
||||||
<!-- Forward Email Modal -->
|
<!-- Forward Email Modal -->
|
||||||
<div id="forwardModal" class="modal" style="display: none;">
|
<div id="forwardModal" class="modal" style="display: none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content frosted-glass">
|
||||||
<span class="close" id="closeForward">×</span>
|
<span class="close" id="closeForward">×</span>
|
||||||
<h3>Forward Email</h3>
|
<h3>Forward Email</h3>
|
||||||
|
|
||||||
|
|
@ -143,9 +146,9 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="modal-description">Select a verified email address to forward this message to.</p>
|
<p class="modal-description">Select a verified email address to forward this message to.</p>
|
||||||
{% if errorMessage %}
|
{% if errorMessage %}
|
||||||
<p class="unlock-error">{{ errorMessage }}</p>
|
<p class="alert alert-error">{{ errorMessage }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p id="forwardError" class="unlock-error" style="display:none"></p>
|
<p id="forwardError" class="alert alert-error" style="display:none"></p>
|
||||||
<form method="POST" action="/inbox/{{ address }}/{{ uid }}/forward">
|
<form method="POST" action="/inbox/{{ address }}/{{ uid }}/forward">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="forwardEmail" class="floating-label">Forward to</label>
|
<label for="forwardEmail" class="floating-label">Forward to</label>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
{% if authEnabled %}
|
{% if authEnabled %}
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu" data-section-title="Account">
|
||||||
<a href="/account" aria-label="Account settings">Settings</a>
|
<a href="/account" aria-label="Account settings">Settings</a>
|
||||||
<a href="/logout?redirect=/stats" aria-label="Logout">Logout</a>
|
<a href="/logout?redirect=/stats" aria-label="Logout">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const sanitizeHtml = require('sanitize-html')
|
const sanitizeHtml = require('sanitize-html')
|
||||||
|
const config = require('../../../application/config')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes <a> tags to always use "noreferrer noopener" and open in a new window.
|
* Transformes <a> tags to always use "noreferrer noopener" and open in a new window.
|
||||||
|
|
@ -6,22 +7,84 @@ const sanitizeHtml = require('sanitize-html')
|
||||||
* @returns {*} dom after transformation
|
* @returns {*} dom after transformation
|
||||||
*/
|
*/
|
||||||
exports.sanitizeHtmlTwigFilter = function(value) {
|
exports.sanitizeHtmlTwigFilter = function(value) {
|
||||||
return sanitizeHtml(value, {
|
return sanitizeHtml(value, {
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
a: ['href', 'target', 'rel']
|
a: ['href', 'target', 'rel']
|
||||||
},
|
},
|
||||||
|
|
||||||
transformTags: {
|
transformTags: {
|
||||||
a(tagName, attribs) {
|
a(tagName, attribs) {
|
||||||
return {
|
return {
|
||||||
tagName,
|
tagName,
|
||||||
attribs: {
|
attribs: {
|
||||||
rel: 'noreferrer noopener',
|
rel: 'noreferrer noopener',
|
||||||
href: attribs.href,
|
href: attribs.href,
|
||||||
target: '_blank'
|
target: '_blank'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert time to highest possible unit (minutes → hours → days),
|
||||||
|
* rounding if necessary and prefixing "~" when rounded.
|
||||||
|
* Mirrors the logic from Helper.convertAndRound()
|
||||||
|
*
|
||||||
|
* @param {number} time
|
||||||
|
* @param {string} unit "minutes" | "hours" | "days"
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function convertAndRound(time, unit) {
|
||||||
|
let value = time
|
||||||
|
let u = unit
|
||||||
|
|
||||||
|
// upgrade units
|
||||||
|
const units = [
|
||||||
|
["minutes", 60, "hours"],
|
||||||
|
["hours", 24, "days"]
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [from, factor, to] of units) {
|
||||||
|
if (u === from && value > factor) {
|
||||||
|
value = value / factor
|
||||||
|
u = to
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine if rounding is needed
|
||||||
|
const rounded = !Number.isSafeInteger(value)
|
||||||
|
if (rounded) value = Math.round(value)
|
||||||
|
|
||||||
|
// Handle singular/plural
|
||||||
|
const displayValue = value === 1 ? value : value
|
||||||
|
const displayUnit = value === 1 ? u.replace(/s$/, '') : u
|
||||||
|
|
||||||
|
return `${rounded ? "~" : ""}${displayValue} ${displayUnit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert purgeTime config to readable format, respecting the convert flag
|
||||||
|
* @param {Object} purgeTime - Object with time, unit, and convert properties
|
||||||
|
* @returns {String} Readable time string
|
||||||
|
*/
|
||||||
|
exports.readablePurgeTime = function(purgeTime) {
|
||||||
|
if (!purgeTime || !purgeTime.time || !purgeTime.unit) {
|
||||||
|
// Fallback to config if not provided
|
||||||
|
if (config.email.purgeTime) {
|
||||||
|
purgeTime = config.email.purgeTime
|
||||||
|
} else {
|
||||||
|
return '48 hours'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = `${purgeTime.time} ${purgeTime.unit}`
|
||||||
|
|
||||||
|
// Only convert if the convert flag is true
|
||||||
|
if (purgeTime.convert) {
|
||||||
|
result = convertAndRound(purgeTime.time, purgeTime.unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const lockRouter = require('./routes/lock')
|
||||||
const authRouter = require('./routes/auth')
|
const authRouter = require('./routes/auth')
|
||||||
const accountRouter = require('./routes/account')
|
const accountRouter = require('./routes/account')
|
||||||
const statsRouter = require('./routes/stats')
|
const statsRouter = require('./routes/stats')
|
||||||
const { sanitizeHtmlTwigFilter } = require('./views/twig-filters')
|
const { sanitizeHtmlTwigFilter, readablePurgeTime } = require('./views/twig-filters')
|
||||||
|
|
||||||
const Helper = require('../../application/helper')
|
const Helper = require('../../application/helper')
|
||||||
const helper = new(Helper)
|
const helper = new(Helper)
|
||||||
|
|
@ -90,6 +90,7 @@ app.use(
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
|
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
|
||||||
|
Twig.extendFilter('readablePurgeTime', readablePurgeTime)
|
||||||
|
|
||||||
// Middleware to expose user session to all templates
|
// Middleware to expose user session to all templates
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
|
|
|
||||||
24
package.json
24
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "48hr.email",
|
"name": "48hr.email",
|
||||||
"version": "1.9.0",
|
"version": "2.0.0",
|
||||||
"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": [
|
||||||
|
|
@ -68,15 +68,17 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"overrides": [{
|
"overrides": [
|
||||||
"files": "public/javascripts/*.js",
|
{
|
||||||
"esnext": false,
|
"files": "public/javascripts/*.js",
|
||||||
"env": [
|
"esnext": false,
|
||||||
"browser"
|
"env": [
|
||||||
],
|
"browser"
|
||||||
"globals": [
|
],
|
||||||
"io"
|
"globals": [
|
||||||
]
|
"io"
|
||||||
}]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue