From e7533ebb9ab7ee31651d056f9c22f5a06fc22775 Mon Sep 17 00:00:00 2001 From: Anton Lilleby Date: Wed, 15 Jan 2025 09:43:40 +0100 Subject: [PATCH] feat: update app to use email content templates --- app/src/lib/actions/external/action.ts | 15 +- app/src/lib/actions/internal/action.ts | 43 +++--- .../email/event/{updated.ts => accepted.ts} | 38 ++---- app/src/lib/email/event/canceled.ts | 32 +---- app/src/lib/email/event/confirm-decline.ts | 30 ++++ app/src/lib/email/event/declined.ts | 67 +++++++++ app/src/lib/email/event/registration.ts | 129 ------------------ app/src/lib/email/event/unregistration.ts | 101 -------------- app/src/lib/email/nodemailer.ts | 28 +++- .../routes/api/send-event-canceled/+server.ts | 17 +-- .../+server.ts | 20 ++- .../unregistration/[token]/+page.server.ts | 23 +++- studio/actions/publish-event.tsx | 4 +- studio/lib/event-email.ts | 20 +-- 14 files changed, 219 insertions(+), 348 deletions(-) rename app/src/lib/email/event/{updated.ts => accepted.ts} (62%) create mode 100644 app/src/lib/email/event/confirm-decline.ts create mode 100644 app/src/lib/email/event/declined.ts delete mode 100644 app/src/lib/email/event/registration.ts delete mode 100644 app/src/lib/email/event/unregistration.ts rename app/src/routes/api/{send-event-update => send-event-updated}/+server.ts (78%) diff --git a/app/src/lib/actions/external/action.ts b/app/src/lib/actions/external/action.ts index 7708c69..d469a86 100644 --- a/app/src/lib/actions/external/action.ts +++ b/app/src/lib/actions/external/action.ts @@ -17,9 +17,9 @@ import { registrationSchemaExternal, unregistrationSchemaExternal, } from "$lib/schemas/external/schema"; -import { sendRegistrationConfirmed } from "$lib/email/event/registration"; -import { sendConfirmUnregistration } from "$lib/email/event/unregistration"; import { RateLimiter } from "sveltekit-rate-limiter/server"; +import { sendEmailAccepted } from "$lib/email/event/accepted"; +import { sendEmailConfirmDecline } from "$lib/email/event/confirm-decline"; const limiter = new RateLimiter({ IP: [20, "h"], // 20 rquests per hour from the same IP @@ -155,17 +155,20 @@ export const submitRegistrationExternal: Actions["submitRegistrationExternal"] = const emailPayload = { id, - mailTo: email, + to: email, summary: eventContent.title, description: eventContent.summary, start: eventContent.start, end: eventContent.end, location: eventContent.place, organiser: eventContent.organisers.join(" | "), + subject: eventContent.emailTemplate.registrationSubject, + message: eventContent.emailTemplate.registrationMessage, + reminder: eventContent.emailReminder, }; if (process.env.NODE_ENV !== "development") { - const { error: emailError } = await sendRegistrationConfirmed(emailPayload); + const { error: emailError } = await sendEmailAccepted(emailPayload); if (emailError) { console.error("Error: Failed to send email"); @@ -263,14 +266,14 @@ export const submitUnregistrationExternal: Actions["submitUnregistrationExternal const emailPayload = { id, - mailTo: email, + to: email, summary: eventContent.title, organiser: eventContent.organisers.join(" | "), token, }; if (process.env.NODE_ENV !== "development") { - const { error: emailError } = await sendConfirmUnregistration(emailPayload); + const { error: emailError } = await sendEmailConfirmDecline(emailPayload); if (emailError) { console.error("Error: Failed to send email"); diff --git a/app/src/lib/actions/internal/action.ts b/app/src/lib/actions/internal/action.ts index 5ee4c8a..5bdfaba 100644 --- a/app/src/lib/actions/internal/action.ts +++ b/app/src/lib/actions/internal/action.ts @@ -1,13 +1,9 @@ -import validator from "validator"; -import { type Actions } from "@sveltejs/kit"; -import { superValidate, message } from "sveltekit-superforms/server"; -import { zod } from "sveltekit-superforms/adapters"; -import { getEventContent } from "$lib/server/sanity/queries"; +import { sendEmailAccepted } from "$lib/email/event/accepted"; +import { sendEmailDeclined } from "$lib/email/event/declined"; import { - getEvent, - getEventParticipant, - setParticipantNotAttending, -} from "$lib/server/supabase/queries"; + registrationSchemaInternal, + unregistrationSchemaInternal, +} from "$lib/schemas/internal/schema"; import { deleteEventParticipant, executeTransaction, @@ -15,13 +11,17 @@ import { insertEventFoodPreference, insertEventParticipantOptions, } from "$lib/server/kysley/transactions"; +import { getEventContent } from "$lib/server/sanity/queries"; import { - registrationSchemaInternal, - unregistrationSchemaInternal, -} from "$lib/schemas/internal/schema"; -import { sendRegistrationConfirmed } from "$lib/email/event/registration"; -import { sendUnregistrationConfirmed } from "$lib/email/event/unregistration"; + getEvent, + getEventParticipant, + setParticipantNotAttending, +} from "$lib/server/supabase/queries"; +import { type Actions } from "@sveltejs/kit"; import { RateLimiter } from "sveltekit-rate-limiter/server"; +import { zod } from "sveltekit-superforms/adapters"; +import { message, superValidate } from "sveltekit-superforms/server"; +import validator from "validator"; /** ** IP: Allows 40 requests per hour from the same IP address. @@ -175,17 +175,20 @@ export const submitRegistrationInternal: Actions["submitRegistrationInternal"] = const emailPayload = { id, - mailTo: email, + to: email, summary: eventContent.title, description: eventContent.summary, start: eventContent.start, end: eventContent.end, location: eventContent.place, organiser: eventContent.organisers.join(" | "), + subject: eventContent.emailTemplate.registrationSubject, + message: eventContent.emailTemplate.registrationMessage, + reminder: eventContent.emailReminder, }; if (process.env.NODE_ENV !== "development") { - const { error: emailError } = await sendRegistrationConfirmed(emailPayload); + const { error: emailError } = await sendEmailAccepted(emailPayload); if (emailError) { console.error("Error: Failed to send email"); @@ -291,23 +294,25 @@ export const submitUnregistrationInternal: Actions["submitUnregistrationInternal const emailPayload = { id, - mailTo: email, + to: email, summary: eventContent.title, description: eventContent.summary, start: eventContent.start, end: eventContent.end, location: eventContent.place, organiser: eventContent.organisers.join(" | "), + subject: eventContent.emailTemplate.unregistrationSubject, + message: eventContent.emailTemplate.unregistrationMessage, }; if (process.env.NODE_ENV !== "development") { - const { error: emailError } = await sendUnregistrationConfirmed(emailPayload); + const { error: emailError } = await sendEmailDeclined(emailPayload); if (emailError) { console.error("Error: Failed to send email"); return message(unregistrationForm, { - text: "Du er nå meldt av arrangement 👋 Vi kunne dessverre ikke sende e-post bekreftelse.", + text: "Du er nå meldt av arrangementet 👋 Vi kunne dessverre ikke sende en e-post bekreftelse.", warning: true, }); } diff --git a/app/src/lib/email/event/updated.ts b/app/src/lib/email/event/accepted.ts similarity index 62% rename from app/src/lib/email/event/updated.ts rename to app/src/lib/email/event/accepted.ts index 1cbcdcd..f02e86f 100644 --- a/app/src/lib/email/event/updated.ts +++ b/app/src/lib/email/event/accepted.ts @@ -5,18 +5,20 @@ import ical, { ICalCalendarMethod, } from "ical-generator"; import { toHTML } from "@portabletext/to-html"; -import { sendMail as sendEmail } from "../nodemailer"; +import { composeEmail, sendEmail, wrapWithStyles } from "../nodemailer"; import { PUBLIC_APP_BASE_URL } from "$env/static/public"; -import type { EventUpdatedProps } from "../../../routes/api/send-event-update/+server"; +import type { EventUpdatedProps } from "../../../routes/api/send-event-updated/+server"; -interface EventUpdatedExtendedProps extends EventUpdatedProps { +interface EmailAcceptedProps extends EventUpdatedProps { to: string; } -export const sendEmailUpdated = async (props: EventUpdatedExtendedProps) => { + +export const sendEmailAccepted = async (props: EmailAcceptedProps) => { const icsFile = createIcsFile(props); - const emailTemplate = createEmailTemplate({ + const emailTemplate = composeEmail({ ...props, + subject: `${props.subject} ${props.summary}`, icsFile, }); @@ -34,7 +36,7 @@ const createIcsFile = ({ location, organiser, reminder, -}: EventUpdatedExtendedProps) => { +}: EmailAcceptedProps) => { const url = `${PUBLIC_APP_BASE_URL}/event/${id}`; const calendar = ical({ name: organiser, method: ICalCalendarMethod.REQUEST }); const alarms = []; @@ -43,7 +45,9 @@ const createIcsFile = ({ alarms.push({ type: ICalAlarmType.email, summary: reminder.threeDaysSubject, - description: reminder.threeDaysMessage ? toHTML(reminder.threeDaysMessage) : "", + description: reminder.threeDaysMessage + ? wrapWithStyles(toHTML(reminder.threeDaysMessage)) + : "", trigger: 259200, // ->3 days before event starts }); } @@ -52,7 +56,7 @@ const createIcsFile = ({ alarms.push({ type: ICalAlarmType.email, summary: reminder.oneHourSubject, - description: reminder.oneHourMessage ? toHTML(reminder.oneHourMessage) : "", + description: reminder.oneHourMessage ? wrapWithStyles(toHTML(reminder.oneHourMessage)) : "", trigger: 3600, // -> 1 hour before event starts }); } @@ -81,21 +85,3 @@ const createIcsFile = ({ return Buffer.from(calendar.toString()); }; - -interface EmailProps - extends Pick { - icsFile: Buffer; -} - -const createEmailTemplate = ({ to, subject, message, summary, icsFile }: EmailProps) => { - return { - to, - from: "Skjer ", - subject: `${subject} ${summary}`, - html: toHTML(message), - icalEvent: { - method: "request", - content: icsFile, - }, - }; -}; diff --git a/app/src/lib/email/event/canceled.ts b/app/src/lib/email/event/canceled.ts index b22ba60..a656f95 100644 --- a/app/src/lib/email/event/canceled.ts +++ b/app/src/lib/email/event/canceled.ts @@ -1,22 +1,22 @@ import { PUBLIC_APP_BASE_URL } from "$env/static/public"; -import { sendMail as sendEmail } from "$lib/email/nodemailer"; -import { toHTML } from "@portabletext/to-html"; +import { composeEmail, sendEmail } from "$lib/email/nodemailer"; import ical, { ICalAttendeeRole, ICalAttendeeStatus, ICalCalendarMethod } from "ical-generator"; import type { EventCanceledProps } from "../../../routes/api/send-event-canceled/+server"; -interface EventCanceledExtendedProps extends EventCanceledProps { +interface EmailCanceledProps extends EventCanceledProps { to: string; } -export const sendEmailCanceled = async (props: EventCanceledExtendedProps) => { +export const sendEmailCanceled = async (props: EmailCanceledProps) => { const icsFile = createIcsFile(props); - const emailTemplate = createEmailTemplate({ + const email = composeEmail({ ...props, + subject: `${props.subject} ${props.summary}`, icsFile, }); - const result = await sendEmail(emailTemplate); + const result = await sendEmail(email); return result; }; @@ -29,7 +29,7 @@ const createIcsFile = ({ end, location, organiser, -}: EventCanceledExtendedProps) => { +}: EmailCanceledProps) => { const url = `${PUBLIC_APP_BASE_URL}/event/${id}`; const calendar = ical({ name: organiser, method: ICalCalendarMethod.CANCEL }); calendar.createEvent({ @@ -55,21 +55,3 @@ const createIcsFile = ({ return Buffer.from(calendar.toString()); }; - -interface EmailProps - extends Pick { - icsFile: Buffer; -} - -const createEmailTemplate = ({ to, subject, message, summary, icsFile }: EmailProps) => { - return { - to, - from: "Skjer ", - subject: `${subject} ${summary}`, - html: toHTML(message), - icalEvent: { - method: "request", - content: icsFile, - }, - }; -}; diff --git a/app/src/lib/email/event/confirm-decline.ts b/app/src/lib/email/event/confirm-decline.ts new file mode 100644 index 0000000..b4c07b0 --- /dev/null +++ b/app/src/lib/email/event/confirm-decline.ts @@ -0,0 +1,30 @@ +import { PUBLIC_APP_BASE_URL } from "$env/static/public"; +import { sendEmail } from "$lib/email/nodemailer"; + +interface EmailConfirmDeclineProps { + to: string; + summary: string; + organiser: string; + token: string; +} + +// TODO: Consider making this a function that takes a template from the Event Schema +export const sendEmailConfirmDecline = async (props: EmailConfirmDeclineProps) => { + const url = `${PUBLIC_APP_BASE_URL}/event/unregistration/${props.token}`; + const html = ` +

Hei,

+

Vi har mottatt en forespørsel om å melde deg av «${props.summary}».

+

For å bekrefte denne handlingen, vennligst klikk på følgende lenke:

+

Bekreft avregistrering

+
`; + + const emailTemplate = { + to: props.to, + from: "Skjer ", + subject: `Bekreft avregistrering: ${props.summary}`, + html, + }; + + const result = await sendEmail(emailTemplate); + return result; +}; diff --git a/app/src/lib/email/event/declined.ts b/app/src/lib/email/event/declined.ts new file mode 100644 index 0000000..20a0f6f --- /dev/null +++ b/app/src/lib/email/event/declined.ts @@ -0,0 +1,67 @@ +import ical, { ICalAttendeeRole, ICalAttendeeStatus, ICalCalendarMethod } from "ical-generator"; +import { composeEmail, sendEmail } from "../nodemailer"; +import { PUBLIC_APP_BASE_URL } from "$env/static/public"; +import type { BlockContent } from "$models/sanity.model"; + +interface EmailDeclinedProps { + id: string; + to: string; + summary: string; + description?: string; + start: string; + end: string; + location: string; + organiser: string; + subject: string; + message: BlockContent; +} + +export const sendEmailDeclined = async (props: EmailDeclinedProps) => { + const icsFile = createIcsFile(props); + + const emailTemplate = composeEmail({ + ...props, + subject: `${props.subject} ${props.summary}`, + icsFile, + }); + + const result = await sendEmail(emailTemplate); + return result; +}; + +const createIcsFile = ({ + id, + to, + summary, + description, + start, + end, + location, + organiser, +}: EmailDeclinedProps) => { + const url = `${PUBLIC_APP_BASE_URL}/event/${id}`; + const calendar = ical({ name: organiser, method: ICalCalendarMethod.REQUEST }); + + calendar.createEvent({ + id, + summary, + description, + location, + start, + end, + url, + attendees: [ + { + email: to, + status: ICalAttendeeStatus.DECLINED, + role: ICalAttendeeRole.REQ, + }, + ], + organizer: { + name: organiser, + email: "no-reply@capragruppen.no", + }, + }); + + return Buffer.from(calendar.toString()); +}; diff --git a/app/src/lib/email/event/registration.ts b/app/src/lib/email/event/registration.ts deleted file mode 100644 index 655bcb0..0000000 --- a/app/src/lib/email/event/registration.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { PUBLIC_APP_BASE_URL } from "$env/static/public"; -import ical, { - ICalAlarmType, - ICalAttendeeRole, - ICalAttendeeStatus, - ICalCalendarMethod, -} from "ical-generator"; -import { sendMail } from "$lib/email/nodemailer"; - -interface EventProps { - id: string; - mailTo: string; - summary: string; - description?: string; - start: string; - end: string; - location: string; - organiser: string; -} - -interface EmailParams extends Pick { - subject: string; - icsFile: Buffer; - html?: string; -} - -export const sendRegistrationConfirmed = async (props: EventProps) => { - const icsFile = createIcsFile(props); - - const html = ` -

Du er påmeldt! 🎉 Velkommen til oss!

-

Gode nyheter – du er offisielt påmeldt! 🎊 Vi gleder oss til å ha deg med! Har du noen - spørsmål så håper vi at du tar kontakt. Forbered deg på spennende innhold, nye bekjentskaper og en god - start på/avslutning på dagen. Dette vil du ikke gå glipp av!

-

Vennlig hilsen oss i Capra, Fryde og Liflig

-

P.S. Følg med på innboksen din for flere spennende oppdateringer og overraskelser før - arrangementet! 🚀

-
`; - - const mailParams = createMailParams({ - ...props, - subject: `Påmelding bekreftet: ${props.summary}`, - html, - icsFile, - }); - - const result = await sendMail(mailParams); - return result; -}; - -export const sendInviteUpdate = async (props: EventProps) => { - const icsFile = createIcsFile(props); - const mailParams = createMailParams({ - ...props, - subject: `Oppdatert invitasjon: ${props.summary}`, - icsFile, - }); - - const result = await sendMail(mailParams); - return result; -}; - -const createIcsFile = ({ - id, - summary, - description, - start, - end, - location, - organiser, - mailTo, -}: EventProps) => { - const url = `${PUBLIC_APP_BASE_URL}/event/${id}`; - - const calendar = ical({ name: organiser, method: ICalCalendarMethod.REQUEST }); - calendar.createEvent({ - id, - summary, - description, - location, - start, - end, - url, - attendees: [ - { - email: mailTo, - status: ICalAttendeeStatus.ACCEPTED, - role: ICalAttendeeRole.REQ, - }, - ], - organizer: { - name: organiser, - email: "no-reply@capraconsulting.no", - }, - alarms: [ - { - type: ICalAlarmType.display, - description: "Arrangementet starter straks", - relatesTo: "START", // -> 10 minutes before event starts - }, - { - type: ICalAlarmType.display, - description: "Påminnelse om arrangement i morgen", - trigger: 86400, // -> 24 hours before event starts - }, - ], - }); - - return Buffer.from(calendar.toString()); -}; - -const createMailParams = ({ organiser, mailTo, subject, icsFile, html = "" }: EmailParams) => { - return { - from: `${organiser} `, - to: mailTo, - subject, - html, - icalEvent: { - method: "request", - content: icsFile, - }, - }; -}; diff --git a/app/src/lib/email/event/unregistration.ts b/app/src/lib/email/event/unregistration.ts deleted file mode 100644 index 101d8b5..0000000 --- a/app/src/lib/email/event/unregistration.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { PUBLIC_APP_BASE_URL } from "$env/static/public"; -import { sendMail } from "$lib/email/nodemailer"; -import ical, { ICalAttendeeRole, ICalAttendeeStatus, ICalCalendarMethod } from "ical-generator"; - -export const sendUnregistrationConfirmed = async (props: { - id: string; - mailTo: string; - summary: string; - description?: string; - start: string; - end: string; - location: string; - organiser: string; -}) => { - const url = `${PUBLIC_APP_BASE_URL}/event/${props.id}`; - - const html = ` -

Hei,

-

Vi fikk nettopp beskjed om at du har meldt deg av vårt arrangement.

-

Ingen bekymringer - det kommer flere muligheter til å få litt faglig eller sosialt påfyll hos oss. - Følg med på her eller meld deg på vårt nyhetsbrev.

-

Vi håper å se deg på et av våre fremtidige arrangementer!

-

Med vennlig hilsen,

-

oss i Capra, Liflig og Fryde

-
`; - - const calendar = ical({ name: props.organiser, method: ICalCalendarMethod.REQUEST }); - calendar.createEvent({ - id: props.id, - summary: props.summary, - description: props.description, - location: props.location, - start: props.start, - end: props.end, - url, - attendees: [ - { - email: props.mailTo, - status: ICalAttendeeStatus.DECLINED, - role: ICalAttendeeRole.REQ, - }, - ], - organizer: { - name: props.organiser, - email: "no-reply@capraconsulting.no", - }, - }); - - const icsFile = Buffer.from(calendar.toString()); - - const mailParams = { - from: `${props.organiser} `, - to: props.mailTo, - subject: `Avregistrering bekreftet: ${props.summary}`, - html, - icalEvent: { - method: "request", - content: icsFile, - }, - }; - - const result = await sendMail(mailParams); - return result; -}; - -export const sendConfirmUnregistration = async (props: { - mailTo: string; - summary: string; - organiser: string; - token: string; -}) => { - const url = `${PUBLIC_APP_BASE_URL}/event/unregistration/${props.token}`; - const html = ` -

Hei,

-

Vi har mottatt din forespørsel om å avregistrere deg fra «${props.summary}».

-

For å bekrefte denne handlingen, vennligst klikk på følgende lenke:

-

Bekreft avregistrering

-
`; - - const mailParams = { - from: `${props.organiser} `, - to: props.mailTo, - subject: `Bekreft avregistrering: ${props.summary}`, - html, - }; - - const result = await sendMail(mailParams); - return result; -}; diff --git a/app/src/lib/email/nodemailer.ts b/app/src/lib/email/nodemailer.ts index e0183d6..144251f 100644 --- a/app/src/lib/email/nodemailer.ts +++ b/app/src/lib/email/nodemailer.ts @@ -1,4 +1,6 @@ import { SMTP_AUTH_KEY, SMTP_AUTH_USER, SMTP_HOST } from "$env/static/private"; +import type { BlockContent } from "$models/sanity.model"; +import { toHTML } from "@portabletext/to-html"; import nodemailer from "nodemailer"; import type Mail from "nodemailer/lib/mailer"; @@ -12,7 +14,7 @@ const transporter = nodemailer.createTransport({ }, }); -export const sendMail = async (mailParams: Mail.Options) => { +export const sendEmail = async (mailParams: Mail.Options) => { try { await transporter.sendMail(mailParams); return { error: false }; @@ -20,3 +22,27 @@ export const sendMail = async (mailParams: Mail.Options) => { return { error: true }; } }; + +interface EmailTemplateProps { + to: string; + subject: string; + message: BlockContent; + icsFile: Buffer; +} + +export const composeEmail = ({ to, subject, message, icsFile }: EmailTemplateProps) => { + return { + to, + from: "Skjer ", + subject, + html: wrapWithStyles(toHTML(message)), + icalEvent: { + method: "request", + content: icsFile, + }, + }; +}; + +export const wrapWithStyles = (html: string) => { + return `
${html}
`; +}; diff --git a/app/src/routes/api/send-event-canceled/+server.ts b/app/src/routes/api/send-event-canceled/+server.ts index 5113270..e91cb55 100644 --- a/app/src/routes/api/send-event-canceled/+server.ts +++ b/app/src/routes/api/send-event-canceled/+server.ts @@ -39,21 +39,18 @@ export const POST: RequestHandler = async ({ request }) => { return json({ message: "No participants found for this event" }, { status: 200 }); } - const sendPromises = participants.map(({ email }) => - sendEmailCanceled({ - ...props, - to: email, - }) + const sendEmailPromises = participants.map(({ email }) => + sendEmailCanceled({ ...props, to: email }) ); - const results = await Promise.allSettled(sendPromises); + const results = await Promise.allSettled(sendEmailPromises); - const failedSends = results.filter(({ status }) => status === "rejected"); - if (failedSends.length) { - console.error("Failed email sends", failedSends); + const failedEmailSends = results.filter(({ status }) => status === "rejected"); + if (failedEmailSends.length) { + console.error("Failed email sends", failedEmailSends); return json( - { error: "One or more emails failed to send", failedSends: failedSends.length }, + { error: "One or more emails failed to send", failedSends: failedEmailSends.length }, { status: 207 } ); } diff --git a/app/src/routes/api/send-event-update/+server.ts b/app/src/routes/api/send-event-updated/+server.ts similarity index 78% rename from app/src/routes/api/send-event-update/+server.ts rename to app/src/routes/api/send-event-updated/+server.ts index c4b1d53..b424a25 100644 --- a/app/src/routes/api/send-event-update/+server.ts +++ b/app/src/routes/api/send-event-updated/+server.ts @@ -1,5 +1,5 @@ import { APP_API_TOKEN } from "$env/static/private"; -import { sendEmailUpdated } from "$lib/email/event/updated"; +import { sendEmailAccepted } from "$lib/email/event/accepted"; import { getAttendingParticipants } from "$lib/server/supabase/queries"; import type { BlockContent, EmailReminder } from "$models/sanity.model"; import { json, type RequestHandler } from "@sveltejs/kit"; @@ -40,21 +40,19 @@ export const POST: RequestHandler = async ({ request }) => { return json({ message: "No participants found for this event" }, { status: 200 }); } - const sendPromises = participants.map(({ email }) => - sendEmailUpdated({ - ...props, - to: email, - }) + const sendEmailPromises = participants.map(({ email }) => + sendEmailAccepted({ ...props, to: email }) ); - const results = await Promise.allSettled(sendPromises); + const results = await Promise.allSettled(sendEmailPromises); - const failedSends = results.filter(({ status }) => status === "rejected"); - if (failedSends.length) { - console.error("Failed email sends", failedSends); + const failedEmailSends = results.filter(({ status }) => status === "rejected"); + + if (failedEmailSends.length) { + console.error("Failed email sends", failedEmailSends); return json( - { error: "One or more emails failed to send", failedSends: failedSends.length }, + { error: "One or more emails failed to send", failedSends: failedEmailSends.length }, { status: 207 } ); } diff --git a/app/src/routes/event/unregistration/[token]/+page.server.ts b/app/src/routes/event/unregistration/[token]/+page.server.ts index b6e9cfe..865a23a 100644 --- a/app/src/routes/event/unregistration/[token]/+page.server.ts +++ b/app/src/routes/event/unregistration/[token]/+page.server.ts @@ -4,9 +4,24 @@ import { getUnsubscribeSecret } from "$lib/auth/secret"; import type { DecodedToken } from "$models/jwt.model"; import { setParticipantNotAttending } from "$lib/server/supabase/queries"; import { getEventContent } from "$lib/server/sanity/queries"; -import { sendUnregistrationConfirmed } from "$lib/email/event/unregistration"; +import { sendEmailDeclined } from "$lib/email/event/declined"; + +const rateLimitMap: Map = new Map(); export const load: PageServerLoad = async ({ params: { token } }) => { + const now = Date.now(); + const lastAccess = rateLimitMap.get(token); + + if (lastAccess && now - lastAccess < 30000) { + return { + error: true, + message: + "Du har nådd grensen for antall forsøk. Vennligst vent 30 sekunder før du prøver igjen.", + }; + } + + rateLimitMap.set(token, now); + const tokenDecoded = jwt.decode(token, { complete: true }) as { payload: DecodedToken | null; } | null; @@ -40,17 +55,19 @@ export const load: PageServerLoad = async ({ params: { token } }) => { const emailPayload = { id: document_id, - mailTo: email, + to: email, summary: eventContent.title, description: eventContent.summary, start: eventContent.start, end: eventContent.end, location: eventContent.place, organiser: eventContent.organisers.join(" | "), + subject: eventContent.emailTemplate.unregistrationSubject, + message: eventContent.emailTemplate.unregistrationMessage, }; if (process.env.NODE_ENV !== "development") { - const { error: emailError } = await sendUnregistrationConfirmed(emailPayload); + const { error: emailError } = await sendEmailDeclined(emailPayload); if (emailError) { console.error("Error: Failed to send email"); diff --git a/studio/actions/publish-event.tsx b/studio/actions/publish-event.tsx index 0e9ae16..3cf1a3c 100644 --- a/studio/actions/publish-event.tsx +++ b/studio/actions/publish-event.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Stack, Button, Card, Text, useToast } from "@sanity/ui"; import { createEventIfNotExist } from "../supabase/queries"; import { createSlackMessage } from "../lib/event-slack"; -import { sendEmailEventUpdate } from "../lib/event-email"; +import { sendEmailEventUpdated } from "../lib/event-email"; import { eventChangeRequiresUpdate } from "../lib/event-check"; export function createExtendedEventPublishAction(originalPublishAction: DocumentActionComponent) { @@ -51,7 +51,7 @@ export function createExtendedEventPublishAction(originalPublishAction: Document reminder: draftEvent.emailReminder, }; - const result = await sendEmailEventUpdate(emailProps); + const result = await sendEmailEventUpdated(emailProps); if (result) { toast.push({ diff --git a/studio/lib/event-email.ts b/studio/lib/event-email.ts index bc889f2..56ef916 100644 --- a/studio/lib/event-email.ts +++ b/studio/lib/event-email.ts @@ -1,6 +1,6 @@ import { BlockContent, EmailReminder } from "../models/sanity.model"; -interface EventUpdatedProps { +interface EventProps { id: string; summary: string; description?: string; @@ -12,14 +12,16 @@ interface EventUpdatedProps { message: BlockContent; reminder: EmailReminder; } +interface EventUpdatedProps extends EventProps {} +interface EventCanceledProps extends Omit {} -export const sendEmailEventUpdate = async (props: EventUpdatedProps) => { +export const sendEmailEventUpdated = async (props: EventUpdatedProps) => { if (process.env.MODE === "development") return; const url = process.env.SANITY_STUDIO_APP_BASE_URL; try { - const response = await fetch(`${url}/api/send-event-update`, { + const response = await fetch(`${url}/api/send-event-updated`, { method: "POST", headers: { Authorization: `Bearer ${process.env.SANITY_STUDIO_APP_API_TOKEN}`, @@ -35,18 +37,6 @@ export const sendEmailEventUpdate = async (props: EventUpdatedProps) => { } }; -interface EventCanceledProps { - id: string; - summary: string; - description?: string; - start: string; - end: string; - location: string; - organiser: string; - subject: string; - message: BlockContent; -} - export const sendEmailEventCanceled = async (props: EventCanceledProps) => { if (process.env.MODE === "development") return true;