From 0bcaf85c234c9768f97f1dd6f0dbf36c4535943f Mon Sep 17 00:00:00 2001
From: Remi <73385395+itsrubberduck@users.noreply.github.com>
Date: Tue, 7 Oct 2025 12:29:21 +0200
Subject: [PATCH] Add waitlist invitation sending from admin
---
app/pages/admin/index.vue | 109 ++++++++++++++++++
server/api/admin/waitlist/[id]/invite.post.ts | 80 +++++++++++++
server/api/admin/waitlist/index.get.ts | 43 ++++++-
server/api/service/auth/register.post.ts | 5 +-
.../api/service/invitations/bootstrap.post.ts | 4 +-
server/api/service/invitations/manual.post.ts | 9 +-
server/models/WaitlistEntry.ts | 6 +
server/utils/invitations.ts | 50 ++++++++
server/utils/notifications.ts | 11 +-
9 files changed, 303 insertions(+), 14 deletions(-)
create mode 100644 server/api/admin/waitlist/[id]/invite.post.ts
create mode 100644 server/utils/invitations.ts
diff --git a/app/pages/admin/index.vue b/app/pages/admin/index.vue
index 2ccbe6f..2bc01de 100644
--- a/app/pages/admin/index.vue
+++ b/app/pages/admin/index.vue
@@ -481,6 +481,17 @@
{{ waitlistError }}
+
Your OpenSquawk invite code: {{INVITE_CODE}}
` + cachedHtmlTemplate = fallback + return fallback + } +} + +export async function renderInvitationEmail(code: string) { + const template = await loadInvitationTemplate() + return template.replace(/{{INVITE_CODE}}/g, code) +} + +export function renderInvitationText(code: string) { + const lines = [ + 'Welcome to OpenSquawk!', + '', + 'Thanks for joining the waitlist — here is your personal invite code:', + code, + '', + 'Use it to register your account:', + 'https://opensquawk.de/login?mode=register', + '', + 'Blue skies,', + 'Your OpenSquawk crew', + ] + + return lines.join('\n') +} diff --git a/server/utils/notifications.ts b/server/utils/notifications.ts index 6296274..67ded91 100644 --- a/server/utils/notifications.ts +++ b/server/utils/notifications.ts @@ -3,7 +3,8 @@ const ADMIN_EMAIL_FALLBACK = 'info@opensquawk.de' interface MailOptions { to: string subject: string - text: string + text?: string + html?: string from?: string } @@ -81,6 +82,7 @@ async function sendViaSmtp(payload: MailPayload) { to: payload.to, subject: payload.subject, text: payload.text, + html: payload.html, }) return true } catch (error) { @@ -90,6 +92,10 @@ async function sendViaSmtp(payload: MailPayload) { } export async function sendMail(options: MailOptions) { + if (!options.text && !options.html) { + throw new Error('Email payload requires text or html content') + } + const payload: MailPayload = { ...options, from: resolveFrom(options.from), @@ -97,7 +103,8 @@ export async function sendMail(options: MailOptions) { const success = await sendViaSmtp(payload) if (!success) { - console.info(`[mail:fallback] ${options.subject}\nRecipient: ${options.to}\n${options.text}`) + const preview = options.text || options.html || '' + console.info(`[mail:fallback] ${options.subject}\nRecipient: ${options.to}\n${preview}`) } return success }