feat: add password reset, SMTP settings, and Add Account button

Password reset: users with a registered email can request a reset link
from the login screen. A one-hour signed token is emailed via SMTP;
clicking the link opens a set-new-password form. Tokens are hashed
(SHA-256) before storage and invalidated after use.

SMTP settings: admin-only panel in the Users modal lets admins
configure host, port, encryption, credentials, and from address.
Settings persisted in a new key-value settings table. The SMTP
password is never returned to the client.

Users: email field added to the create/edit form and stored in a new
users.email column. Email is used for password reset lookup.

Add Account: admins now have a + button in the header that opens the
existing setup wizard to add additional checking accounts.

Schema: adds password_reset_tokens and settings tables with automatic
runtime migrations for existing databases.
This commit is contained in:
2026-03-31 10:21:49 -06:00
parent 444e24a191
commit fc114d0ec6
10 changed files with 378 additions and 12 deletions
+39
View File
@@ -0,0 +1,39 @@
'use strict';
const nodemailer = require('nodemailer');
const db = require('../db/database');
function getSmtpSettings() {
const rows = db.prepare("SELECT key, value FROM settings WHERE key LIKE 'smtp_%'").all();
const s = Object.fromEntries(rows.map(r => [r.key, r.value || '']));
return {
host: s.smtp_host || '',
port: parseInt(s.smtp_port, 10) || 587,
secure: s.smtp_secure === '1',
user: s.smtp_user || '',
pass: s.smtp_pass || '',
from: s.smtp_from || '',
};
}
async function sendPasswordReset(toEmail, resetLink) {
const s = getSmtpSettings();
if (!s.host || !s.from) {
throw new Error('SMTP is not configured. Ask an admin to configure email settings.');
}
const transporter = nodemailer.createTransport({
host: s.host,
port: s.port,
secure: s.secure,
auth: s.user ? { user: s.user, pass: s.pass } : undefined,
});
await transporter.sendMail({
from: s.from,
to: toEmail,
subject: 'ezcheck Password Reset',
text: `Click the link below to reset your password. This link expires in 1 hour.\n\n${resetLink}\n\nIf you did not request this, ignore this email.`,
html: `<p>Click the link below to reset your password. This link expires in 1 hour.</p><p><a href="${resetLink}">${resetLink}</a></p><p>If you did not request this, ignore this email.</p>`,
});
}
module.exports = { getSmtpSettings, sendPasswordReset };