Add host welcome/reset emails, email field, and /host URL; v2.0.7

- Host accounts now have an optional email field
- Creating a host with an email sends a welcome email with the portal
  URL, username, and password via Resend
- Resetting a password also emails the new credentials if the host
  has an email on file; admin sees a confirmation either way
- Admin panel shows email column and confirmation messages
- Portal URL changed from /host-upload to /host

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-09-08 14:01:51 -04:00
1 parent 1e396644a9
commit a12a471c41
5 files changed
+90 -10

No files matched your search

+41
View File
@@ -764,3 +764,44 @@ export async function sendNuggetsUploadNotification({ to, source, hostName, epis
console.error('[nuggets-notify] send exception:', err)
}
}
export async function sendNuggetsHostWelcomeEmail({ to, name, username, password, portalUrl }) {
if (!process.env.RESEND_API_KEY || !to) return
const resend = new Resend(process.env.RESEND_API_KEY)
const from = process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM
const bodyHtml = `
<p style="margin:0 0 14px;font-family:Arial,sans-serif;font-size:15px;color:#2c2417;">Hi ${escapeHtml(name)},</p>
<p style="margin:0 0 20px;font-family:Arial,sans-serif;font-size:15px;color:#2c2417;">
You've been set up as a host on the <strong>Nuggets</strong> episode upload portal.
Use the details below to sign in and submit your episodes.
</p>
<table style="width:100%;border-collapse:collapse;font-family:Arial,sans-serif;font-size:14px;color:#2c2417;margin-bottom:20px;">
<tr><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;border-radius:4px;color:#7a7060;width:110px;">Portal URL</td><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;"><a href="${escapeHtml(portalUrl)}" style="color:#c8a96e;">${escapeHtml(portalUrl)}</a></td></tr>
<tr><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;color:#7a7060;">Username</td><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;font-family:monospace;font-weight:700;">${escapeHtml(username)}</td></tr>
<tr><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;color:#7a7060;">Password</td><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;font-family:monospace;font-weight:700;">${escapeHtml(password)}</td></tr>
</table>
<p style="margin:0 0 10px;font-family:Arial,sans-serif;font-size:14px;color:#2c2417;">When you're ready to submit an episode, fill in the episode name, the Bible passage, a short description, and attach your audio file. Everything uploads in one step.</p>
<p style="margin:0;font-family:Arial,sans-serif;font-size:14px;color:#7a7060;">If you have any trouble signing in, reply to this email and we'll get you sorted.</p>
`
try {
const { error } = await resend.emails.send({
from,
to: [to],
subject: 'Your Nuggets Host Upload Portal Access',
text: `Hi ${name},\n\nYou've been set up as a host on the Nuggets episode upload portal.\n\nPortal URL: ${portalUrl}\nUsername: ${username}\nPassword: ${password}\n\nWhen submitting an episode, fill in the episode name, Bible passage, a short description, and attach your audio file.\n\nIf you have any trouble, reply to this email.`,
html: buildBrandedEmailHtml({
title: 'Upload Portal Access',
eyebrow: 'Nuggets',
bodyHtml,
ctaLabel: 'Open Upload Portal',
ctaUrl: portalUrl,
footerHtml: '<p style="margin:0;font-family:Arial,sans-serif;font-size:12px;color:#7a7060;">You received this because an admin created an upload account for you on the Nuggets podcast portal.</p>',
}),
})
if (error) console.error('[nuggets-welcome] send error:', error)
} catch (err) {
console.error('[nuggets-welcome] send exception:', err)
}
}
+28 -3
View File
@@ -6,7 +6,7 @@ import rateLimit from 'express-rate-limit'
import { createHash, timingSafeEqual } from 'node:crypto'
import { parseCookies, cookieSecureFlag } from '../helpers.js'
import { requireAdminAuth } from '../auth.js'
import { sendNuggetsUploadNotification } from '../email.js'
import { sendNuggetsUploadNotification, sendNuggetsHostWelcomeEmail } from '../email.js'
import { markSeafileFileSeen } from '../nuggets-seafile-watcher.js'
import {
NUGGETS_UPLOAD_HOSTS_FILE,
@@ -281,6 +281,7 @@ export function register(app) {
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : ''
const username = typeof req.body?.username === 'string' ? req.body.username.trim().toLowerCase() : ''
const password = typeof req.body?.password === 'string' ? req.body.password : ''
const email = typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : ''
if (!name || !username || !password) {
res.status(400).json({ message: 'Name, username, and password are required.' })
@@ -301,6 +302,7 @@ export function register(app) {
id: randomUUID(),
name,
username,
email,
passwordHash: hashPassword(password),
active: true,
createdAt: new Date().toISOString(),
@@ -308,8 +310,19 @@ export function register(app) {
data.hosts.push(host)
await saveUploadHosts(data)
// Send welcome email if an address was provided
if (email) {
sendNuggetsHostWelcomeEmail({
to: email,
name,
username,
password,
portalUrl: `${process.env.SITE_URL ?? 'https://versebyversewithnate.us'}/host`,
}).catch(err => console.error('[nuggets-host] welcome email error:', err))
}
const { passwordHash: _ph, ...safeHost } = host
res.json({ ok: true, host: safeHost })
res.json({ ok: true, host: safeHost, emailSent: Boolean(email) })
})
app.put('/api/admin/nuggets-upload/hosts/:id', requireAdminAuth, async (req, res) => {
@@ -332,11 +345,23 @@ export function register(app) {
patch.passwordHash = hashPassword(req.body.password)
}
const newPassword = typeof req.body?.password === 'string' && req.body.password.length > 0 ? req.body.password : null
data.hosts[idx] = { ...data.hosts[idx], ...patch }
await saveUploadHosts(data)
// Email the host their new password if they have an address on file
if (newPassword && data.hosts[idx].email) {
sendNuggetsHostWelcomeEmail({
to: data.hosts[idx].email,
name: data.hosts[idx].name,
username: data.hosts[idx].username,
password: newPassword,
portalUrl: `${process.env.SITE_URL ?? 'https://versebyversewithnate.us'}/host`,
}).catch(err => console.error('[nuggets-host] reset email error:', err))
}
const { passwordHash: _ph, ...safeHost } = data.hosts[idx]
res.json({ ok: true, host: safeHost })
res.json({ ok: true, host: safeHost, emailSent: Boolean(newPassword && data.hosts[idx].email) })
})
app.delete('/api/admin/nuggets-upload/hosts/:id', requireAdminAuth, async (req, res) => {