diff --git a/package.json b/package.json index fab6e44..fa1d12b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.6.1", + "version": "2.7.0", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/contact.js b/server/routes/contact.js index bf0bfed..19fb8b5 100644 --- a/server/routes/contact.js +++ b/server/routes/contact.js @@ -407,7 +407,7 @@ export function register(app) { name: entry.name, email: entry.email, subscribedAt: entry.submittedAt, - source: entry.message?.startsWith('Requested') ? 'download' : 'contact-form', + source: entry.source === 'resend-import' ? 'resend-import' : entry.message?.startsWith('Requested') ? 'download' : 'contact-form', })) .sort((a, b) => new Date(b.subscribedAt).getTime() - new Date(a.subscribedAt).getTime()) res.json({ subscribers, total: subscribers.length }) @@ -632,8 +632,108 @@ export function register(app) { } }) + // Import chosen Resend-only contacts into the app's subscriber list (explicit, per-person opt-in by the admin). + app.post('/api/admin-subscribers/import-from-resend', requireAdminAuth, async (req, res) => { + const { emails } = req.body ?? {} + if (!Array.isArray(emails) || emails.length === 0 || emails.length > 200 || !emails.every(e => typeof e === 'string')) { + res.status(400).json({ message: 'Choose between 1 and 200 contacts to import.' }); return + } + if (!process.env.RESEND_API_KEY) { + res.status(500).json({ message: 'RESEND_API_KEY is not configured.' }); return + } + try { + const diff = await buildResendComparison() + const allowed = new Map(diff.onlyInResend.map(c => [c.email, c])) + const now = new Date().toISOString() + const created = [] + for (const raw of emails) { + const email = raw.trim().toLowerCase() + const contact = allowed.get(email) + if (!contact) continue // not active in Resend, or already a subscriber here + created.push({ + id: randomUUID(), + submittedAt: now, + name: contact.name || email, + email, + message: 'Imported from Resend audience', + messageType: 'general', + subscribe: true, + archived: true, + read: true, + source: 'resend-import', + }) + } + if (created.length > 0) { + state.contactSubmissions = [...created, ...state.contactSubmissions] + queueContactSubmissionsWrite() + } + res.json({ ok: true, imported: created.length, skipped: emails.length - created.length }) + } catch (err) { + console.error('[subscribers] import from resend failed:', err) + res.status(502).json({ message: 'Import failed. Try again in a moment.' }) + } + }) + + // ── Broadcast Email: sent as a real Resend Broadcast to the Resend segment ────── + const UNSUB_FOOTER = '
' + + // App-side {{name}} becomes Resend's per-contact variable; guarantee an unsubscribe link exists. + function toResendBroadcastHtml(html) { + let out = html.replace(/\{\{\{?\s*(?:first_?name|name)\s*\}?\}\}/gi, '{{{FIRST_NAME|Friend}}}') + if (!/RESEND_UNSUBSCRIBE_URL/i.test(out)) { + out = /<\/body>/i.test(out) ? out.replace(/<\/body>/i, `${UNSUB_FOOTER}