diff --git a/package.json b/package.json index cce3169..d79806f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.0.6", + "version": "2.0.7", "type": "module", "scripts": { "dev": "vite", diff --git a/server/email.js b/server/email.js index 7e67f73..e320d9f 100644 --- a/server/email.js +++ b/server/email.js @@ -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 = ` +

Hi ${escapeHtml(name)},

+

+ You've been set up as a host on the Nuggets episode upload portal. + Use the details below to sign in and submit your episodes. +

+ + + + +
Portal URL${escapeHtml(portalUrl)}
Username${escapeHtml(username)}
Password${escapeHtml(password)}
+

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.

+

If you have any trouble signing in, reply to this email and we'll get you sorted.

+ ` + + 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: '

You received this because an admin created an upload account for you on the Nuggets podcast portal.

', + }), + }) + if (error) console.error('[nuggets-welcome] send error:', error) + } catch (err) { + console.error('[nuggets-welcome] send exception:', err) + } +} diff --git a/server/routes/nuggets-host-upload.js b/server/routes/nuggets-host-upload.js index fb642b1..e6423e5 100644 --- a/server/routes/nuggets-host-upload.js +++ b/server/routes/nuggets-host-upload.js @@ -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) => { diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 9a23c76..a8028ed 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -1101,6 +1101,7 @@ interface UploadHost { id: string name: string username: string + email: string active: boolean createdAt: string } @@ -1111,13 +1112,16 @@ function NuggetsUploadPortalAdmin() { const [error, setError] = useState('') const [addName, setAddName] = useState('') const [addUsername, setAddUsername] = useState('') + const [addEmail, setAddEmail] = useState('') const [addPassword, setAddPassword] = useState('') const [addError, setAddError] = useState('') const [addSaving, setAddSaving] = useState(false) + const [addEmailSent, setAddEmailSent] = useState(false) const [resetId, setResetId] = useState(null) const [resetPw, setResetPw] = useState('') const [resetError, setResetError] = useState('') const [resetSaving, setResetSaving] = useState(false) + const [resetEmailSent, setResetEmailSent] = useState(false) function reload() { setLoading(true) @@ -1138,11 +1142,12 @@ function NuggetsUploadPortalAdmin() { const r = await fetch('/api/admin/nuggets-upload/hosts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: addName, username: addUsername, password: addPassword }), + body: JSON.stringify({ name: addName, username: addUsername, email: addEmail, password: addPassword }), }) const d = await r.json() if (!r.ok) { setAddError(d.message ?? 'Failed.'); return } - setAddName(''); setAddUsername(''); setAddPassword('') + setAddEmailSent(Boolean(d.emailSent)) + setAddName(''); setAddUsername(''); setAddEmail(''); setAddPassword('') reload() } finally { setAddSaving(false) @@ -1177,6 +1182,7 @@ function NuggetsUploadPortalAdmin() { }) const d = await r.json() if (!r.ok) { setResetError(d.message ?? 'Failed.'); return } + setResetEmailSent(Boolean(d.emailSent)) setResetId(null); setResetPw('') reload() } finally { @@ -1190,7 +1196,7 @@ function NuggetsUploadPortalAdmin() {

Nuggets — Upload Portal

Manage host login accounts for the upload portal at{' '} - /host-upload. + /host. Each host gets a username and password to upload episode files to Seafile.

@@ -1207,6 +1213,7 @@ function NuggetsUploadPortalAdmin() { Name Username + Email Status @@ -1216,6 +1223,7 @@ function NuggetsUploadPortalAdmin() { {h.name} {h.username} + {h.email || none} {h.active ? 'Active' : 'Disabled'} @@ -1249,6 +1257,7 @@ function NuggetsUploadPortalAdmin() { {resetError &&

{resetError}

} )} + {resetEmailSent && !resetId &&

✓ New password emailed to host.

}

Add Host Account

@@ -1257,17 +1266,22 @@ function NuggetsUploadPortalAdmin() { setAddName(e.target.value)} required /> -
+
setAddUsername(e.target.value.toLowerCase())} required />
-
+
+ + setAddEmail(e.target.value)} /> +
+
setAddPassword(e.target.value)} required minLength={8} />
{addError &&

{addError}

} + {addEmailSent &&

✓ Welcome email sent to host with login details.

} ) diff --git a/src/App.tsx b/src/App.tsx index 8b8ed23..7c5d178 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3140,7 +3140,7 @@ export default function App() { } /> } /> } /> - } /> + } /> } /> } />