|
- 🩺 Pediatric AI Scribe
- AI-Powered Clinical Documentation
+ return `
+
+
+
+
+
+
+
- | ${body} |
- |
- This email was sent from Pediatric AI Scribe. If you did not request this, you can safely ignore it — no action is needed and your account remains secure.
+ | | ${body} |
+
|
@@ -31,14 +55,20 @@ function emailWrapper(body) {
`;
}
-function btnStyle() {
- return 'display:inline-block;background:linear-gradient(135deg,#2563eb,#7c3aed);color:white;padding:13px 32px;border-radius:8px;text-decoration:none;font-weight:600;font-size:15px;';
+function btnHtml(href, label) {
+ return ``;
+}
+
+function linkFallback(url) {
+ return `Button not working? Copy this link into your browser:
+ ${escHtml(url)} `;
}
// Email helper — DB settings override env vars
async function getSmtpTransport() {
var nodemailer = require('nodemailer');
- // DB settings override env vars
var host = await db.getSetting('smtp.host').catch(function() { return null; }) || process.env.SMTP_HOST;
if (!host) return null;
var port = parseInt(await db.getSetting('smtp.port').catch(function() { return null; }) || process.env.SMTP_PORT || '587', 10);
@@ -69,7 +99,6 @@ async function sendEmail(to, subject, html) {
// ============================================================
router.post('/register', async (req, res) => {
try {
- // Check if registration is enabled
var regEnabled = await db.getSetting('registration_enabled');
if (regEnabled === 'false') {
return res.status(403).json({ error: 'Registration is currently disabled. Contact an administrator.' });
@@ -86,7 +115,6 @@ router.post('/register', async (req, res) => {
var verifyToken = crypto.randomBytes(32).toString('hex');
var verifyExpires = Date.now() + 24 * 60 * 60 * 1000;
- // First user becomes admin automatically
var userCount = await db.get('SELECT COUNT(*) as count FROM users', []);
var role = (userCount && parseInt(userCount.count) === 0) ? 'admin' : 'user';
@@ -96,25 +124,21 @@ router.post('/register', async (req, res) => {
);
var userId = result.lastInsertRowid;
-
- var verifyUrl = (process.env.APP_URL || 'http://localhost:3000') + '/api/auth/verify-email?token=' + verifyToken;
+ var verifyUrl = safeAppUrl() + '/api/auth/verify-email?token=' + verifyToken;
var verifySubject = await db.getSetting('email.verify.subject') || 'Verify your Pediatric AI Scribe account';
var verifyBody = await db.getSetting('email.verify.body') || 'Great to have you on Pediatric AI Scribe. Please verify your email address by clicking the button below.';
+
await sendEmail(email, verifySubject, emailWrapper(
- `👋 Welcome aboard, ${name}!
- ${verifyBody.replace(/\n/g, ' ')}
-
- Verify My Email
-
- Button not working? Copy and paste this link into your browser:
- ${verifyUrl}
- This link expires in 24 hours. `
+ `Welcome aboard, ${escHtml(name)}!
+ ${escHtml(verifyBody).replace(/\n/g, ' ')}
+ ${btnHtml(verifyUrl, 'Verify My Email')}
+ ${linkFallback(verifyUrl)}
+ This link expires in 24 hours. `
));
await db.run('INSERT INTO audit_log (user_id, action, ip_address, details) VALUES (?, ?, ?, ?)',
[userId, 'register', req.ip, role === 'admin' ? 'First user — auto admin' : 'standard user']);
- // Auto-verify if no SMTP (check DB setting first, then env)
var smtpHost = await db.getSetting('smtp.host').catch(function() { return null; }) || process.env.SMTP_HOST;
if (!smtpHost) {
await db.run('UPDATE users SET email_verified = true, verify_token = NULL WHERE id = ?', [userId]);
@@ -128,8 +152,8 @@ router.post('/register', async (req, res) => {
res.json({ success: true, needsVerification: true, message: 'Check your email for verification link.' + (role === 'admin' ? ' You are the first user and have admin privileges.' : '') });
} catch (err) {
- console.error('Register error:', err);
- res.status(500).json({ error: err.message });
+ console.error('[Auth] Register error:', err.message);
+ res.status(500).json({ error: 'Registration failed. Please try again.' });
}
});
@@ -142,13 +166,12 @@ router.get('/verify-email', async (req, res) => {
if (!token) return res.status(400).send('Missing token');
var user = await db.get('SELECT id, name FROM users WHERE verify_token = ? AND verify_expires > ?', [token, Date.now()]);
if (!user) {
- return res.send('Invalid or Expired LinkGo to app ');
+ return res.send('Invalid or Expired LinkGo to app ');
}
await db.run('UPDATE users SET email_verified = true, verify_token = NULL, verify_expires = NULL WHERE id = ?', [user.id]);
await db.run('INSERT INTO audit_log (user_id, action) VALUES (?, ?)', [user.id, 'email_verified']);
- var safeName = user.name.replace(/&/g, '&').replace(//g, '>');
- res.send('✅ Email Verified!Welcome, ' + safeName + '! Open App ');
- } catch (err) { res.status(500).send('Error: ' + err.message); }
+ res.send('Email Verified!Welcome, ' + escHtml(user.name) + '! Open App ');
+ } catch (err) { console.error('[Auth] Verify error:', err.message); res.status(500).send('Verification failed. Please try again.'); }
});
// RESEND VERIFICATION
@@ -159,19 +182,16 @@ router.post('/resend-verification', async (req, res) => {
if (user.email_verified) return res.json({ success: true, message: 'Already verified.' });
var verifyToken = crypto.randomBytes(32).toString('hex');
await db.run('UPDATE users SET verify_token = ?, verify_expires = ? WHERE id = ?', [verifyToken, Date.now() + 86400000, user.id]);
- var verifyUrl = (process.env.APP_URL || 'http://localhost:3000') + '/api/auth/verify-email?token=' + verifyToken;
+ var verifyUrl = safeAppUrl() + '/api/auth/verify-email?token=' + verifyToken;
await sendEmail(req.body.email, 'Verify your email — Pediatric AI Scribe', emailWrapper(
- `📧 Verify your email
- Here's a fresh verification link for your account. Click below to confirm your email and get started.
-
- Verify My Email
-
- Button not working? Copy and paste this link into your browser:
- ${verifyUrl}
- This link expires in 24 hours. `
+ `Verify your email
+ Here's a fresh verification link for your account. Click below to confirm your email and get started.
+ ${btnHtml(verifyUrl, 'Verify My Email')}
+ ${linkFallback(verifyUrl)}
+ This link expires in 24 hours. `
));
res.json({ success: true, message: 'Verification email sent' });
- } catch (err) { res.status(500).json({ error: err.message }); }
+ } catch (err) { console.error('[Auth] Resend error:', err.message); res.status(500).json({ error: 'Failed to send email' }); }
});
// ============================================================
@@ -214,7 +234,7 @@ router.post('/login', async (req, res) => {
success: true, token: token,
user: { id: user.id, email: user.email, name: user.name, role: user.role, totp_enabled: user.totp_enabled, email_verified: user.email_verified }
});
- } catch (err) { res.status(500).json({ error: err.message }); }
+ } catch (err) { console.error('[Auth] Login error:', err.message); res.status(500).json({ error: 'Login failed' }); }
});
// 2FA
@@ -224,7 +244,7 @@ router.post('/setup-2fa', authMiddleware, async (req, res) => {
await db.run('UPDATE users SET totp_secret = ? WHERE id = ?', [secret.base32, req.user.id]);
var qrUrl = await QRCode.toDataURL(secret.otpauth_url);
res.json({ success: true, secret: secret.base32, qrCode: qrUrl });
- } catch (err) { res.status(500).json({ error: err.message }); }
+ } catch (err) { console.error('[Auth] 2FA setup error:', err.message); res.status(500).json({ error: '2FA setup failed' }); }
});
router.post('/verify-2fa', authMiddleware, async (req, res) => {
@@ -234,7 +254,7 @@ router.post('/verify-2fa', authMiddleware, async (req, res) => {
if (!verified) return res.status(400).json({ error: 'Invalid code' });
await db.run('UPDATE users SET totp_enabled = true WHERE id = ?', [req.user.id]);
res.json({ success: true });
- } catch (err) { res.status(500).json({ error: err.message }); }
+ } catch (err) { console.error('[Auth] 2FA verify error:', err.message); res.status(500).json({ error: '2FA verification failed' }); }
});
router.post('/disable-2fa', authMiddleware, async (req, res) => {
@@ -244,7 +264,7 @@ router.post('/disable-2fa', authMiddleware, async (req, res) => {
if (!valid) return res.status(401).json({ error: 'Wrong password' });
await db.run('UPDATE users SET totp_enabled = false, totp_secret = NULL WHERE id = ?', [req.user.id]);
res.json({ success: true });
- } catch (err) { res.status(500).json({ error: err.message }); }
+ } catch (err) { console.error('[Auth] 2FA disable error:', err.message); res.status(500).json({ error: '2FA disable failed' }); }
});
// Password reset
@@ -254,20 +274,17 @@ router.post('/forgot-password', async (req, res) => {
if (!user) return res.json({ success: true, message: 'If account exists, reset email sent' });
var token = crypto.randomBytes(32).toString('hex');
await db.run('UPDATE users SET reset_token = ?, reset_expires = ? WHERE id = ?', [token, Date.now() + 3600000, user.id]);
- var resetUrl = (process.env.APP_URL || 'http://localhost:3000') + '/reset-password?token=' + token;
+ var resetUrl = safeAppUrl() + '/reset-password?token=' + token;
var resetSubject = await db.getSetting('email.reset.subject') || 'Reset your password — Pediatric AI Scribe';
var resetBody = await db.getSetting('email.reset.body') || 'Someone requested a password reset for your Pediatric AI Scribe account. If that was you, click the button below to choose a new password. This link expires in 1 hour. If you did not request a password reset, no action is needed.';
await sendEmail(req.body.email, resetSubject, emailWrapper(
- `🔐 Password reset request
- ${resetBody.replace(/\n/g, ' ')}
-
- Reset My Password
-
- Button not working? Copy and paste this link into your browser:
- ${resetUrl} `
+ `Password reset request
+ ${escHtml(resetBody).replace(/\n/g, ' ')}
+ ${btnHtml(resetUrl, 'Reset My Password')}
+ ${linkFallback(resetUrl)}`
));
res.json({ success: true, message: 'If account exists, reset email sent' });
- } catch (err) { res.status(500).json({ error: err.message }); }
+ } catch (err) { console.error('[Auth] Forgot password error:', err.message); res.status(500).json({ error: 'Password reset request failed' }); }
});
router.post('/reset-password', async (req, res) => {
@@ -279,7 +296,7 @@ router.post('/reset-password', async (req, res) => {
var hash = await bcrypt.hash(newPassword, 12);
await db.run('UPDATE users SET password = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?', [hash, user.id]);
res.json({ success: true });
- } catch (err) { res.status(500).json({ error: err.message }); }
+ } catch (err) { console.error('[Auth] Reset password error:', err.message); res.status(500).json({ error: 'Password reset failed' }); }
});
// Get current user
@@ -290,7 +307,7 @@ router.get('/me', authMiddleware, async (req, res) => {
[req.user.id]
);
res.json({ user: user });
- } catch (err) { res.status(500).json({ error: err.message }); }
+ } catch (err) { console.error('[Auth] Me error:', err.message); res.status(500).json({ error: 'Failed to load user' }); }
});
// Check if registration is enabled (public endpoint)
@@ -305,4 +322,4 @@ router.get('/registration-status', async (req, res) => {
module.exports = router;
module.exports.__sendEmail = sendEmail;
module.exports.__emailWrapper = emailWrapper;
-module.exports.__btnStyle = btnStyle;
+module.exports.__btnHtml = btnHtml;
|