From a5713814623fccacd6a77757d13fc38a5c9545b1 Mon Sep 17 00:00:00 2001 From: ClaraCrazy Date: Fri, 26 Dec 2025 09:10:25 +0100 Subject: [PATCH] [Feat]: Add Inbox Locking Add support for locking specific inboxes with a password for X time, configurable via .env vars. This allows for users to bridge the gap between public free tempmail services and private personal mail services. Cheers! --- .env.example | 6 + README.md | 1 - app.js | 20 +- application/config.js | 7 + domain/inbox-lock.js | 95 ++++ infrastructure/web/middleware/lock.js | 38 ++ .../web/public/javascripts/lock-modals.js | 136 ++++++ .../web/public/stylesheets/custom.css | 110 +++++ infrastructure/web/routes/inbox.js | 34 +- infrastructure/web/routes/lock.js | 112 +++++ infrastructure/web/views/error.twig | 50 +- infrastructure/web/views/inbox.twig | 106 +++- infrastructure/web/views/mail.twig | 6 +- infrastructure/web/web.js | 36 +- package-lock.json | 457 +++++++++++++++++- package.json | 2 + 16 files changed, 1185 insertions(+), 31 deletions(-) create mode 100644 domain/inbox-lock.js create mode 100644 infrastructure/web/middleware/lock.js create mode 100644 infrastructure/web/public/javascripts/lock-modals.js create mode 100644 infrastructure/web/routes/lock.js diff --git a/.env.example b/.env.example index 35e3e4f..d89bf7b 100644 --- a/.env.example +++ b/.env.example @@ -31,3 +31,9 @@ HTTP_DISPLAY_SORT=2 # Domain display # 2 = alphabetical + first item shuffled, # 3 = shuffle all HTTP_HIDE_OTHER=false # true = only show first domain, false = show all + +# --- INBOX LOCKING (optional) --- +LOCK_ENABLED=false # Enable inbox locking with passwords +LOCK_SESSION_SECRET="change-this-secret-in-production" # Secret for session encryption +LOCK_DATABASE_PATH="./db/locked-inboxes.db" # Path to lock database +LOCK_RELEASE_HOURS=720 # Auto-release locked inboxes after X hours of inactivity (default 30 days) diff --git a/README.md b/README.md index 9585a9b..dba457d 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,6 @@ WantedBy=multi-user.target ### TODO (PRs welcome): - Add user registration: - Optional "premium" domains that arent visible to the public to prevent them from being scraped and flagged - - Allow people to set a password for their email (releases X time after last login) - Allow people to set up forwarding #### Unsure: diff --git a/app.js b/app.js index b60f996..309a80c 100644 --- a/app.js +++ b/app.js @@ -13,6 +13,7 @@ const ClientNotification = require('./infrastructure/web/client-notification') const ImapService = require('./application/imap-service') const MailProcessingService = require('./application/mail-processing-service') const MailRepository = require('./domain/mail-repository') +const InboxLock = require('./domain/inbox-lock') const clientNotification = new ClientNotification() debug('Client notification service initialized') @@ -55,6 +56,23 @@ imapService.on(ImapService.EVENT_ERROR, error => { }) app.set('mailProcessingService', mailProcessingService) +app.set('config', config) + +// Initialize inbox locking if enabled +if (config.lock.enabled) { + const inboxLock = new InboxLock(config.lock.dbPath) + app.set('inboxLock', inboxLock) + console.log(`Inbox locking enabled (auto-release after ${config.lock.releaseHours} hours)`) + + // Check for inactive locked inboxes + setInterval(() => { + const inactive = inboxLock.getInactive(config.lock.releaseHours) + if (inactive.length > 0) { + console.log(`Releasing ${inactive.length} inactive locked inbox(es)`) + inactive.forEach(address => inboxLock.release(address)) + } + }, config.imap.refreshIntervalSeconds * 1000) +} app.locals.imapService = imapService app.locals.mailProcessingService = mailProcessingService @@ -86,4 +104,4 @@ server.on('error', error => { console.error('Fatal web server error', error) process.exit(1) } -}) \ No newline at end of file +}) diff --git a/application/config.js b/application/config.js index 378bf47..990bd48 100644 --- a/application/config.js +++ b/application/config.js @@ -60,6 +60,13 @@ const config = { branding: parseValue(process.env.HTTP_BRANDING), displaySort: Number(process.env.HTTP_DISPLAY_SORT), hideOther: parseBool(process.env.HTTP_HIDE_OTHER) + }, + + lock: { + enabled: parseBool(process.env.LOCK_ENABLED) || false, + sessionSecret: parseValue(process.env.LOCK_SESSION_SECRET) || 'change-me-in-production', + dbPath: parseValue(process.env.LOCK_DATABASE_PATH) || './db/locked-inboxes.db', + releaseHours: Number(process.env.LOCK_RELEASE_HOURS) || 720 // 30 days default } }; diff --git a/domain/inbox-lock.js b/domain/inbox-lock.js new file mode 100644 index 0000000..e34e916 --- /dev/null +++ b/domain/inbox-lock.js @@ -0,0 +1,95 @@ +const Database = require('better-sqlite3') +const bcrypt = require('bcrypt') +const path = require('path') + +class InboxLock { + constructor(dbPath = './db/locked-inboxes.db') { + // Ensure data directory exists + const fs = require('fs') + const dir = path.dirname(dbPath) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + + this.db = new Database(dbPath) + this.db.pragma('journal_mode = WAL') + this._initTable() + } + + _initTable() { + this.db.exec(` + CREATE TABLE IF NOT EXISTS locked_inboxes ( + address TEXT PRIMARY KEY, + password_hash TEXT NOT NULL, + locked_at INTEGER NOT NULL, + last_access INTEGER NOT NULL + ) + `) + } + + async lock(address, password) { + const passwordHash = await bcrypt.hash(password, 10) + const now = Date.now() + + const stmt = this.db.prepare(` + INSERT INTO locked_inboxes (address, password_hash, locked_at, last_access) + VALUES (?, ?, ?, ?) + `) + + try { + stmt.run(address.toLowerCase(), passwordHash, now, now) + return true + } catch (error) { + if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { + throw new Error('This inbox is already locked') + } + throw error + } + } + + async unlock(address, password) { + const stmt = this.db.prepare('SELECT * FROM locked_inboxes WHERE address = ?') + const inbox = stmt.get(address.toLowerCase()) + + if (!inbox) { + return null + } + + const valid = await bcrypt.compare(password, inbox.password_hash) + if (!valid) { + return null + } + + // Update last access + this.updateAccess(address) + return inbox + } + + isLocked(address) { + const stmt = this.db.prepare('SELECT address FROM locked_inboxes WHERE address = ?') + return stmt.get(address.toLowerCase()) !== undefined + } + + updateAccess(address) { + const stmt = this.db.prepare('UPDATE locked_inboxes SET last_access = ? WHERE address = ?') + stmt.run(Date.now(), address.toLowerCase()) + } + + getInactive(hoursThreshold) { + const cutoff = Date.now() - (hoursThreshold * 60 * 60 * 1000) + const stmt = this.db.prepare('SELECT address FROM locked_inboxes WHERE last_access < ?') + return stmt.all(cutoff).map(row => row.address) + } + + release(address) { + const stmt = this.db.prepare('DELETE FROM locked_inboxes WHERE address = ?') + stmt.run(address.toLowerCase()) + } + + getAllLocked() { + const stmt = this.db.prepare('SELECT address FROM locked_inboxes') + return stmt.all().map(row => row.address) + } +} + +module.exports = InboxLock \ No newline at end of file diff --git a/infrastructure/web/middleware/lock.js b/infrastructure/web/middleware/lock.js new file mode 100644 index 0000000..329ddfa --- /dev/null +++ b/infrastructure/web/middleware/lock.js @@ -0,0 +1,38 @@ +function checkLockAccess(req, res, next) { + const inboxLock = req.app.get('inboxLock') + const address = req.params.address + + if (!address || !inboxLock) { + return next() + } + + const isLocked = inboxLock.isLocked(address) + const hasAccess = req.session && req.session.lockedInbox === address.toLowerCase() + + // Block access to locked inbox without proper authentication + if (isLocked && !hasAccess) { + const count = req.app.get('mailProcessingService').getCount() + const unlockError = req.session ? req.session.unlockError : undefined + if (req.session) delete req.session.unlockError + + return res.render('error', { + purgeTime: require('../../../application/helper').prototype.purgeTimeElemetBuilder(), + address: address, + count: count, + message: 'This inbox is locked. Please unlock it to access.', + branding: req.app.get('config').http.branding, + showUnlockButton: true, + unlockError: unlockError, + redirectTo: req.originalUrl + }) + } + + // Update last access if they have access + if (isLocked && hasAccess) { + inboxLock.updateAccess(address) + } + + next() +} + +module.exports = { checkLockAccess } \ No newline at end of file diff --git a/infrastructure/web/public/javascripts/lock-modals.js b/infrastructure/web/public/javascripts/lock-modals.js new file mode 100644 index 0000000..6d8614b --- /dev/null +++ b/infrastructure/web/public/javascripts/lock-modals.js @@ -0,0 +1,136 @@ +document.addEventListener('DOMContentLoaded', function() { + // Lock modal elements + const lockModal = document.getElementById('lockModal'); + const lockBtn = document.getElementById('lockBtn'); + const closeLock = document.getElementById('closeLock'); + const lockForm = document.querySelector('#lockModal form'); + + // Unlock modal elements + const unlockModal = document.getElementById('unlockModal'); + const unlockBtn = document.getElementById('unlockBtn'); + const closeUnlock = document.getElementById('closeUnlock'); + + // Remove lock modal elements + const removeLockModal = document.getElementById('removeLockModal'); + const removeLockBtn = document.getElementById('removeLockBtn'); + const closeRemoveLock = document.getElementById('closeRemoveLock'); + const cancelRemoveLock = document.getElementById('cancelRemoveLock'); + + // Open/close helpers + const openModal = function(modal) { if (modal) modal.style.display = 'block'; }; + const closeModal = function(modal) { if (modal) modal.style.display = 'none'; }; + + // Protect / Lock modal logic + if (lockBtn) { + lockBtn.onclick = function(e) { + e.preventDefault(); + openModal(lockModal); + }; + } + if (closeLock) { + closeLock.onclick = function() { + closeModal(lockModal); + }; + } + + if (lockForm) { + lockForm.addEventListener('submit', function(e) { + const pwElement = document.getElementById('lockPassword'); + const cfElement = document.getElementById('lockConfirm'); + const pw = pwElement ? pwElement.value : ''; + const cf = cfElement ? cfElement.value : ''; + const err = document.getElementById('lockErrorInline'); + const serverErr = document.getElementById('lockServerError'); + if (serverErr) serverErr.style.display = 'none'; + + if (pw !== cf) { + e.preventDefault(); + if (err) { + err.textContent = 'Passwords do not match.'; + err.style.display = 'block'; + } + return; + } + if (pw.length < 8) { + e.preventDefault(); + if (err) { + err.textContent = 'Password must be at least 8 characters.'; + err.style.display = 'block'; + } + return; + } + if (err) err.style.display = 'none'; + }); + } + + // Auto-open lock modal if server provided an error (via data attribute) + if (lockModal) { + const lockErrorValue = (lockModal.dataset.lockError || '').trim(); + if (lockErrorValue) { + openModal(lockModal); + const err = document.getElementById('lockErrorInline'); + const serverErr = document.getElementById('lockServerError'); + if (serverErr) serverErr.style.display = 'none'; + + if (err) { + if (lockErrorValue === 'locking_disabled_for_example') { + err.textContent = 'Locking is disabled for the example inbox.'; + } else if (lockErrorValue === 'invalid') { + err.textContent = 'Please provide a valid password.'; + } else if (lockErrorValue === 'server_error') { + err.textContent = 'A server error occurred. Please try again.'; + } else if (lockErrorValue === 'remove_failed') { + err.textContent = 'Failed to remove lock. Please try again.'; + } else { + err.textContent = 'An error occurred. Please try again.'; + } + err.style.display = 'block'; + } + } + } + + // Unlock modal logic + if (unlockBtn) { + unlockBtn.onclick = function(e) { + e.preventDefault(); + openModal(unlockModal); + }; + } + if (closeUnlock) { + closeUnlock.onclick = function() { + closeModal(unlockModal); + }; + } + + if (unlockModal) { + const unlockErrorValue = (unlockModal.dataset.unlockError || '').trim(); + if (unlockErrorValue) { + openModal(unlockModal); + } + } + + // Remove lock modal logic + if (removeLockBtn) { + removeLockBtn.onclick = function(e) { + e.preventDefault(); + openModal(removeLockModal); + }; + } + if (closeRemoveLock) { + closeRemoveLock.onclick = function() { + closeModal(removeLockModal); + }; + } + if (cancelRemoveLock) { + cancelRemoveLock.onclick = function() { + closeModal(removeLockModal); + }; + } + + // Close modals when clicking outside + window.onclick = function(e) { + if (e.target === lockModal) closeModal(lockModal); + if (e.target === unlockModal) closeModal(unlockModal); + if (e.target === removeLockModal) closeModal(removeLockModal); + }; +}); diff --git a/infrastructure/web/public/stylesheets/custom.css b/infrastructure/web/public/stylesheets/custom.css index e9274a6..184b019 100644 --- a/infrastructure/web/public/stylesheets/custom.css +++ b/infrastructure/web/public/stylesheets/custom.css @@ -541,4 +541,114 @@ label { border-color: rgba(155, 77, 202, 0.3); transform: translateX(5px); text-decoration: none; +} + + +/* Modal Styles */ + +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.8); +} + +.modal-content { + background-color: #131516; + margin: 10% auto; + padding: 2rem; + border: 1px solid #9b4dca; + border-radius: 0.4rem; + width: 90%; + max-width: 400px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +.modal-content h3 { + margin-top: 0; + color: #cccccc; +} + +.modal-description { + color: #999; + margin-bottom: 1.5rem; + font-size: 1.4rem; +} + +.close { + float: right; + font-size: 2.8rem; + font-weight: bold; + cursor: pointer; + color: #cccccc; + line-height: 20px; + transition: color 0.3s; +} + +.close:hover, +.close:focus { + color: #9b4dca; +} + +.floating-label { + position: relative; + top: 12px; + left: 12px; + width: fit-content; + line-height: 1; + padding: 0 6px; + font-size: 1.4rem; + background-color: #131516; + z-index: 999; + color: #cccccc; +} + +.modal-input { + border-radius: 0.4rem; + color: #cccccc; + font-size: 1.6rem; + height: 4.2rem; + padding: 0 1.4rem; + margin-bottom: 1rem; + background-color: transparent; + border: 1px solid #444; + width: 100%; +} + +.modal-input:focus { + border-color: #9b4dca; + outline: none; +} + +.modal-button { + width: 100%; + margin-top: 1rem; + background-color: #9b4dca; +} + + +/* Lock Error Messages */ + +.unlock-error { + color: #ff8c00; + margin-bottom: 1rem; + padding: 0.8rem; + background: rgba(255, 140, 0, 0.1); + border-left: 3px solid #ff8c00; +} + + +/* Remove Lock Button Styles */ + +.modal-button-danger { + background-color: #e74c3c; +} + +.modal-button-cancel { + background-color: #555; } \ No newline at end of file diff --git a/infrastructure/web/routes/inbox.js b/infrastructure/web/routes/inbox.js index fe22107..12b052c 100644 --- a/infrastructure/web/routes/inbox.js +++ b/infrastructure/web/routes/inbox.js @@ -6,6 +6,7 @@ const debug = require('debug')('48hr-email:routes') const config = require('../../../application/config') const Helper = require('../../../application/helper') const helper = new(Helper) +const { checkLockAccess } = require('../middleware/lock') const purgeTime = helper.purgeTimeElemetBuilder() @@ -18,17 +19,29 @@ const sanitizeAddress = param('address').customSanitizer( } ) -router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) => { +router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, checkLockAccess, async(req, res, next) => { try { const mailProcessingService = req.app.get('mailProcessingService') if (!mailProcessingService) { throw new Error('Mail processing service not available') } debug(`Inbox request for ${req.params.address}`) + const inboxLock = req.app.get('inboxLock') const count = await mailProcessingService.getCount() const largestUid = await req.app.locals.imapService.getLargestUid() const totalcount = helper.countElementBuilder(count, largestUid) debug(`Rendering inbox with ${count} total mails`) + const isLocked = inboxLock && inboxLock.isLocked(req.params.address) + const hasAccess = req.session && req.session.lockedInbox === req.params.address + + // Pull any lock error from session and clear it after reading + const lockError = req.session ? req.session.lockError : undefined + const unlockErrorSession = req.session ? req.session.unlockError : undefined + if (req.session) { + delete req.session.lockError + delete req.session.unlockError + } + res.render('inbox', { title: `${config.http.branding[0]} | ` + req.params.address, purgeTime: purgeTime, @@ -37,6 +50,12 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) = totalcount: totalcount, mailSummaries: mailProcessingService.getMailSummaries(req.params.address), branding: config.http.branding, + lockEnabled: config.lock.enabled, + isLocked: isLocked, + hasAccess: hasAccess, + unlockError: unlockErrorSession, + error: lockError, + redirectTo: req.originalUrl }) } catch (error) { debug(`Error loading inbox for ${req.params.address}:`, error.message) @@ -48,6 +67,7 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) = router.get( '^/:address/:uid([0-9]+)', sanitizeAddress, + checkLockAccess, async(req, res, next) => { try { const mailProcessingService = req.app.get('mailProcessingService') @@ -67,6 +87,11 @@ router.get( // Emails are immutable, cache if found res.set('Cache-Control', 'private, max-age=600') + + const inboxLock = req.app.get('inboxLock') + const isLocked = inboxLock && inboxLock.isLocked(req.params.address) + const hasAccess = req.session && req.session.lockedInbox === req.params.address + debug(`Rendering email view for UID ${req.params.uid}`) res.render('mail', { title: mail.subject + " | " + req.params.address, @@ -77,6 +102,9 @@ router.get( mail, uid: req.params.uid, branding: config.http.branding, + lockEnabled: config.lock.enabled, + isLocked: isLocked, + hasAccess: hasAccess }) } else { debug(`Email ${req.params.uid} not found for ${req.params.address}`) @@ -95,6 +123,7 @@ router.get( router.get( '^/:address/delete-all', sanitizeAddress, + checkLockAccess, async(req, res, next) => { try { const mailProcessingService = req.app.get('mailProcessingService') @@ -118,6 +147,7 @@ router.get( router.get( '^/:address/:uid/delete', sanitizeAddress, + checkLockAccess, async(req, res, next) => { try { const mailProcessingService = req.app.get('mailProcessingService') @@ -136,6 +166,7 @@ router.get( router.get( '^/:address/:uid/:checksum([a-f0-9]+)', sanitizeAddress, + checkLockAccess, async(req, res, next) => { try { const mailProcessingService = req.app.get('mailProcessingService') @@ -195,6 +226,7 @@ router.get( router.get( '^/:address/:uid/raw', sanitizeAddress, + checkLockAccess, async(req, res, next) => { try { const mailProcessingService = req.app.get('mailProcessingService') diff --git a/infrastructure/web/routes/lock.js b/infrastructure/web/routes/lock.js new file mode 100644 index 0000000..607003c --- /dev/null +++ b/infrastructure/web/routes/lock.js @@ -0,0 +1,112 @@ +const express = require('express') +const router = express.Router() + +router.post('/lock', async(req, res) => { + const { address, password } = req.body + + if (!address || !password || password.length < 8) { + if (req.session) req.session.lockError = 'invalid' + return res.redirect(`/inbox/${address}`) + } + + try { + const inboxLock = req.app.get('inboxLock') + const mailProcessingService = req.app.get('mailProcessingService') + const config = req.app.get('config') + + // Prevent locking the example inbox; allow UI but block DB insert + if (config && config.email && config.email.examples && config.email.examples.account && address.toLowerCase() === config.email.examples.account.toLowerCase()) { + if (req.session) req.session.lockError = 'locking_disabled_for_example' + return res.redirect(`/inbox/${address}`) + } + + await inboxLock.lock(address, password) + + // Clear cache for this inbox + if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) { + mailProcessingService.cachedFetchFullMail.clear() + } + + req.session.lockedInbox = address + res.redirect(`/inbox/${address}`) + } catch (error) { + console.error('Lock error:', error) + if (req.session) req.session.lockError = 'server_error' + res.redirect(`/inbox/${address}`) + } +}) + +router.post('/unlock', async(req, res) => { + const { address, password, redirectTo } = req.body + const destination = redirectTo && redirectTo.startsWith('/') ? redirectTo : `/inbox/${address}` + + if (!address || !password) { + if (req.session) req.session.unlockError = 'missing_fields' + return res.redirect(destination) + } + + try { + const inboxLock = req.app.get('inboxLock') + const inbox = await inboxLock.unlock(address, password) + + if (!inbox) { + if (req.session) req.session.unlockError = 'invalid_password' + return res.redirect(destination) + } + + req.session.lockedInbox = address + res.redirect(destination) + } catch (error) { + console.error('Unlock error:', error) + if (req.session) req.session.unlockError = 'server_error' + res.redirect(destination) + } +}) + +router.get('/logout', (req, res) => { + const mailProcessingService = req.app.get('mailProcessingService') + + // Clear cache before logout + if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) { + mailProcessingService.cachedFetchFullMail.clear() + } + + req.session.destroy() + res.redirect('/') +}) + +router.post('/remove', async(req, res) => { + const { address } = req.body + + if (!address) { + return res.redirect('/') + } + + // Check if user has access to this locked inbox + const hasAccess = req.session && req.session.lockedInbox === address.toLowerCase() + + if (!hasAccess) { + return res.redirect(`/inbox/${address}`) + } + + try { + const inboxLock = req.app.get('inboxLock') + const mailProcessingService = req.app.get('mailProcessingService') + + await inboxLock.release(address) + + // Clear cache when removing lock + if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) { + mailProcessingService.cachedFetchFullMail.clear() + } + + req.session.destroy() + res.redirect(`/inbox/${address}`) + } catch (error) { + console.error('Remove lock error:', error) + if (req.session) req.session.lockError = 'remove_failed' + res.redirect(`/inbox/${address}`) + } +}) + +module.exports = router \ No newline at end of file diff --git a/infrastructure/web/views/error.twig b/infrastructure/web/views/error.twig index f0f218f..06ece55 100644 --- a/infrastructure/web/views/error.twig +++ b/infrastructure/web/views/error.twig @@ -2,8 +2,10 @@ {% block header %} {% endblock %} @@ -11,4 +13,48 @@

{{message}}

{{error.status}}

{{error.stack}}
+ + {% if showUnlockButton %} + + + + {% endif %} {% endblock %} diff --git a/infrastructure/web/views/inbox.twig b/infrastructure/web/views/inbox.twig index 940cdb2..8169708 100644 --- a/infrastructure/web/views/inbox.twig +++ b/infrastructure/web/views/inbox.twig @@ -2,14 +2,27 @@ {% block header %} {% endblock %} {% block body %} - +

{{ address }}

@@ -31,15 +44,90 @@ {% endfor %} - {% if not mailSummaries %} -
-
-

Inbox Empty

-

Your emails will appear here once they arrive.

-
-
+ {% if not mailSummaries %} +
+ There are no mails yet. +
+ {% endif %} + + {% if lockEnabled and not isLocked %} + + + + {% endif %} + + {% if lockEnabled and isLocked and not hasAccess %} + + + + {% endif %} + + {% if lockEnabled and isLocked and hasAccess %} + + + + {# JS handled in /javascripts/lock-modals.js #} + {% endif %} {% endblock %} diff --git a/infrastructure/web/views/mail.twig b/infrastructure/web/views/mail.twig index 6da233f..99a1be3 100644 --- a/infrastructure/web/views/mail.twig +++ b/infrastructure/web/views/mail.twig @@ -5,7 +5,11 @@ ← Return to inbox Delete Email View Raw - Logout + {% if lockEnabled and isLocked and hasAccess %} + Logout + {% else %} + Logout + {% endif %}
{% endblock %} diff --git a/infrastructure/web/web.js b/infrastructure/web/web.js index 403dc1e..95f4529 100644 --- a/infrastructure/web/web.js +++ b/infrastructure/web/web.js @@ -13,6 +13,7 @@ const config = require('../../application/config') const inboxRouter = require('./routes/inbox') const loginRouter = require('./routes/login') const errorRouter = require('./routes/error') +const lockRouter = require('./routes/lock') const { sanitizeHtmlTwigFilter } = require('./views/twig-filters') const Helper = require('../../application/helper') @@ -39,13 +40,25 @@ app.use(logger('dev')) app.use(express.json()) app.use(express.urlencoded({ extended: false })) -// Session middleware -app.use(session({ - secret: '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', // They will hate me for this, its temporary tho, I swear! - resave: false, - saveUninitialized: true, - cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours -})) +// Session support for inbox locking +if (config.lock.enabled) { + const session = require('express-session') + app.use(session({ + secret: config.lock.sessionSecret, + resave: false, + saveUninitialized: false, + cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours + })) +} + +// Clear session when user goes Home so locked inboxes require password again +app.get('/', (req, res, next) => { + if (config.lock.enabled && req.session) { + req.session.destroy(() => next()) + } else { + next() + } +}) // Remove trailing slash middleware (except for root) app.use((req, res, next) => { @@ -72,15 +85,12 @@ app.use( ) Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter) -/** -app.get('/', (req, res, _next) => { - res.redirect('/login') -}) -**/ - app.use('/', loginRouter) app.use('/inbox', inboxRouter) app.use('/error', errorRouter) +if (config.lock.enabled) { + app.use('/lock', lockRouter) +} // Catch 404 and forward to error handler app.use((req, res, next) => { diff --git a/package-lock.json b/package-lock.json index 70258c8..5d56c82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "dependencies": { "array.prototype.flatmap": "^1.3.3", "async-retry": "^1.3.3", + "bcrypt": "^6.0.0", + "better-sqlite3": "^12.5.0", "compression": "^1.8.1", "debug": "^4.4.3", "dotenv": "^17.2.3", @@ -1225,6 +1227,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -1262,6 +1284,83 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/better-sqlite3": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz", + "integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -1390,6 +1489,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1537,6 +1660,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -1874,6 +2003,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1986,6 +2139,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2168,6 +2330,15 @@ "semver": "bin/semver" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { "version": "6.6.5", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", @@ -3376,6 +3547,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -3647,6 +3827,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3912,6 +4098,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4086,6 +4278,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4508,6 +4706,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4633,6 +4851,12 @@ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5835,6 +6059,18 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5861,12 +6097,17 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mnemonist": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.27.2.tgz", @@ -5952,6 +6193,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5985,6 +6232,38 @@ "node": ">=4.0.0" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -6229,7 +6508,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -6623,6 +6901,32 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6710,6 +7014,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6860,6 +7174,30 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -7628,6 +7966,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -8012,6 +8395,57 @@ "node": ">=0.6" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/terser": { "version": "5.44.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", @@ -8182,6 +8616,18 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/twig": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/twig/-/twig-0.10.3.tgz", @@ -8494,6 +8940,12 @@ "integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==", "license": "MIT" }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8803,7 +9255,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { diff --git a/package.json b/package.json index 1ff0cb5..ce89614 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "dependencies": { "array.prototype.flatmap": "^1.3.3", "async-retry": "^1.3.3", + "bcrypt": "^6.0.0", + "better-sqlite3": "^12.5.0", "compression": "^1.8.1", "debug": "^4.4.3", "dotenv": "^17.2.3",