From b88f89ff14e1a56e550848efe20893c810b5f561 Mon Sep 17 00:00:00 2001 From: an2n Date: Mon, 24 Jun 2024 10:41:54 +0200 Subject: [PATCH] feat: send email after unregistration (#73) Co-authored-by: Anton Lilleby --- .../external/EventFormExternal.svelte | 5 -- app/src/components/shared/Header.svelte | 11 ++- app/src/lib/actions/external/action.ts | 60 +++++++++---- app/src/lib/actions/internal/action.ts | 59 ++++++++++--- app/src/lib/auth/secret.ts | 5 +- app/src/lib/email/event/canceled.ts | 80 ++++++++++++++++++ .../registration.ts} | 30 +++---- app/src/lib/email/event/unregistration.ts | 84 +++++++++++++++++++ app/src/lib/schemas/external/schema.ts | 6 +- app/src/lib/server/sanity/queries.ts | 4 +- app/src/lib/server/supabase/queries.ts | 6 +- app/src/models/jwt.model.ts | 3 +- app/src/routes/+page.server.ts | 4 +- .../routes/api/send-event-canceled/+server.ts | 4 +- .../routes/api/send-event-update/+server.ts | 4 +- app/src/routes/api/webhook/+server.ts | 33 ++++++++ app/src/routes/event/[id]/+page.server.ts | 11 +-- .../unregistration/[token]/+page.server.ts | 72 ++++++++++++---- 18 files changed, 389 insertions(+), 92 deletions(-) create mode 100644 app/src/lib/email/event/canceled.ts rename app/src/lib/email/{event-registration.ts => event/registration.ts} (68%) create mode 100644 app/src/lib/email/event/unregistration.ts create mode 100644 app/src/routes/api/webhook/+server.ts diff --git a/app/src/components/external/EventFormExternal.svelte b/app/src/components/external/EventFormExternal.svelte index d9c5d05..961c686 100644 --- a/app/src/components/external/EventFormExternal.svelte +++ b/app/src/components/external/EventFormExternal.svelte @@ -63,11 +63,6 @@ {#if $unregistrationMessage?.text} {$unregistrationMessage.text} - {#if $unregistrationMessage.token} -
- {`/event/unregistration/${$unregistrationMessage.token}`} -
- {/if}
{:else}
- + Animert Capra, Fryde og Liflig-logo diff --git a/app/src/lib/actions/external/action.ts b/app/src/lib/actions/external/action.ts index a3e984a..fc38e52 100644 --- a/app/src/lib/actions/external/action.ts +++ b/app/src/lib/actions/external/action.ts @@ -18,7 +18,8 @@ import { registrationSchemaExternal, unregistrationSchemaExternal, } from "$lib/schemas/external/schema"; -import { sendEventRegistrationConfirmed } from "$lib/email/event-registration"; +import { sendRegistrationConfirmed } from "$lib/email/event/registration"; +import { sendConfirmUnregistration } from "$lib/email/event/unregistration"; export const submitRegistrationExternal: Actions["submitRegistrationExternal"] = async ({ request, @@ -55,7 +56,7 @@ export const submitRegistrationExternal: Actions["submitRegistrationExternal"] = }); } - const eventContent = await getEventContent({ id }); + const eventContent = await getEventContent({ document_id: id }); if (!eventContent) { console.error("Error: The specified event does not exist as content"); @@ -149,15 +150,17 @@ export const submitRegistrationExternal: Actions["submitRegistrationExternal"] = organiser: eventContent.organisers.join(" | "), }; - const { error: emailError } = await sendEventRegistrationConfirmed(emailPayload); + if (process.env.NODE_ENV !== "development") { + const { error: emailError } = await sendRegistrationConfirmed(emailPayload); - if (emailError) { - console.error("Error: Failed to send email"); + if (emailError) { + console.error("Error: Failed to send email"); - return message(registrationForm, { - text: "Det har oppstått en feil. Du har blitt påmeldt arrangement, men e-post bekreftelse er ikke sendt.", - warning: true, - }); + return message(registrationForm, { + text: "Det har oppstått en feil. Du har blitt påmeldt arrangement, men e-post bekreftelse er ikke sendt.", + warning: true, + }); + } } return message(registrationForm, { @@ -209,9 +212,7 @@ export const submitUnregistrationExternal: Actions["submitUnregistrationExternal data: { email }, } = unregistrationForm; - const data = { event_id, email }; - - const eventParticipant = await getEventParticipant(data); + const eventParticipant = await getEventParticipant({ event_id, email }); if (!eventParticipant.data?.email || !eventParticipant.data?.attending) { return message(unregistrationForm, { @@ -220,11 +221,42 @@ export const submitUnregistrationExternal: Actions["submitUnregistrationExternal }); } + const eventContent = await getEventContent({ document_id: id }); + + if (!eventContent) { + console.error("Error: The specified event does not exist as content"); + + return message(unregistrationForm, { + text: "Det har oppstått et problem. Du kan ikke melde deg av dette arrangementet.", + error: true, + }); + } + + const data = { document_id: id, event_id, email }; const secret = getUnsubscribeSecret(data); const token = jwt.sign({ data }, secret, { expiresIn: "2h" }); - // send jwt token to email - // returning for demo purpose + const emailPayload = { + id, + mailTo: email, + summary: eventContent.title, + organiser: eventContent.organisers.join(" | "), + token, + }; + + if (process.env.NODE_ENV !== "development") { + const { error: emailError } = await sendConfirmUnregistration(emailPayload); + + if (emailError) { + console.error("Error: Failed to send email"); + + return message(unregistrationForm, { + text: "Det har oppstått et problem. Du kan ikke melde deg av dette arrangementet.", + warning: true, + }); + } + } + return message(unregistrationForm, { token, text: "En e-post har blitt sendt til adressen du oppga. Vennligst følg instruksjonen i e-posten for å fullføre.", diff --git a/app/src/lib/actions/internal/action.ts b/app/src/lib/actions/internal/action.ts index 6a7ede1..dfbb7ea 100644 --- a/app/src/lib/actions/internal/action.ts +++ b/app/src/lib/actions/internal/action.ts @@ -7,7 +7,7 @@ import { getEventContent } from "$lib/server/sanity/queries"; import { getEvent, getEventParticipant, - updateEventParticipantAttending, + setParticipantNotAttending, } from "$lib/server/supabase/queries"; import { deleteEventParticipant, @@ -20,7 +20,8 @@ import { registrationSchemaInternal, unregistrationSchemaInternal, } from "$lib/schemas/internal/schema"; -import { sendEventRegistrationConfirmed } from "$lib/email/event-registration"; +import { sendRegistrationConfirmed } from "$lib/email/event/registration"; +import { sendUnregistrationConfirmed } from "$lib/email/event/unregistration"; export const submitRegistrationInternal: Actions["submitRegistrationInternal"] = async ({ request, @@ -69,7 +70,7 @@ export const submitRegistrationInternal: Actions["submitRegistrationInternal"] = }); } - const eventContent = await getEventContent({ id }); + const eventContent = await getEventContent({ document_id: id }); if (!eventContent) { console.error("Error: The specified event does not exist as content"); @@ -166,7 +167,7 @@ export const submitRegistrationInternal: Actions["submitRegistrationInternal"] = }; if (process.env.NODE_ENV !== "development") { - const { error: emailError } = await sendEventRegistrationConfirmed(emailPayload); + const { error: emailError } = await sendRegistrationConfirmed(emailPayload); if (emailError) { console.error("Error: Failed to send email"); @@ -234,24 +235,58 @@ export const submitUnregistrationInternal: Actions["submitUnregistrationInternal const eventParticipant = await getEventParticipant(data); - if (eventParticipant.data?.attending) { - await updateEventParticipantAttending(data); - + if (!eventParticipant.data?.attending) { return message(unregistrationForm, { - success: true, - text: "Du er nå meldt av arrangementet.", + text: "Du er allerede meldt av arrangementet. Takk for interessen din!", + warning: true, }); } if (!eventParticipant.data?.email) { return message(unregistrationForm, { - warning: true, text: "Vi finner dessverre ingen opplysninger om din påmelding til arrangementet.", + error: true, + }); + } + + const attendingResult = await setParticipantNotAttending(data); + if (!attendingResult) { + console.error("Error: Failed to update participant attending"); + + return message(unregistrationForm, { + text: "Det har oppstått en feil. Du kan ikke melde deg av dette arrangementet.", + error: true, }); } + const eventContent = await getEventContent({ document_id: id }); + + const emailPayload = { + id, + mailTo: email, + summary: eventContent.title, + description: eventContent.summary, + start: eventContent.start, + end: eventContent.end, + location: eventContent.place, + organiser: eventContent.organisers.join(" | "), + }; + + if (process.env.NODE_ENV !== "development") { + const { error: emailError } = await sendUnregistrationConfirmed(emailPayload); + + if (emailError) { + console.error("Error: Failed to send email"); + + return message(unregistrationForm, { + text: "Det har oppstått en feil. Du er meldt av arrangement 👋 men e-post bekreftelse er ikke sendt.", + warning: true, + }); + } + } + return message(unregistrationForm, { - warning: true, - text: "Du er allerede meldt av arrangementet. Takk for interessen din!", + message: "Du er nå meldt av arrangementet 👋 Vi har sendt deg en bekreftelse på e-post.", + success: true, }); }; diff --git a/app/src/lib/auth/secret.ts b/app/src/lib/auth/secret.ts index 78bc409..90e8442 100644 --- a/app/src/lib/auth/secret.ts +++ b/app/src/lib/auth/secret.ts @@ -1,6 +1,7 @@ import { APP_SECRET } from "$env/static/private"; +import type { TokenData } from "$models/jwt.model"; -export const getUnsubscribeSecret = ({ event_id, email }: { event_id: number; email: string }) => { - const secret = `${event_id}-${email}-${APP_SECRET}`; +export const getUnsubscribeSecret = ({ document_id, event_id, email }: TokenData) => { + const secret = `${document_id}-${event_id}-${email}-${APP_SECRET}`; return secret; }; diff --git a/app/src/lib/email/event/canceled.ts b/app/src/lib/email/event/canceled.ts new file mode 100644 index 0000000..dad4ce4 --- /dev/null +++ b/app/src/lib/email/event/canceled.ts @@ -0,0 +1,80 @@ +import { PUBLIC_APP_BASE_URL } from "$env/static/public"; +import ical, { 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; +} + +export const sendCanceled = async (props: EventProps) => { + const icsFile = createIcsFile(props); + const mailParams = createMailParams({ + ...props, + subject: `Avlyst arrangement: ${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.CANCEL }); + 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", + }, + }); + + return Buffer.from(calendar.toString()); +}; + +const createMailParams = ({ organiser, mailTo, subject, icsFile }: EmailParams) => { + return { + from: `${organiser} `, + to: mailTo, + subject, + icalEvent: { + method: "request", + content: icsFile, + }, + }; +}; diff --git a/app/src/lib/email/event-registration.ts b/app/src/lib/email/event/registration.ts similarity index 68% rename from app/src/lib/email/event-registration.ts rename to app/src/lib/email/event/registration.ts index f426246..51d9f7d 100644 --- a/app/src/lib/email/event-registration.ts +++ b/app/src/lib/email/event/registration.ts @@ -11,19 +11,22 @@ interface EventProps { end: string; location: string; organiser: string; - method?: ICalCalendarMethod; } interface EmailParams extends Pick { subject: string; icsFile: Buffer; + html?: string; } -export const sendEventRegistrationConfirmed = async (props: EventProps) => { +export const sendRegistrationConfirmed = async (props: EventProps) => { const icsFile = createIcsFile(props); + + const url = `${PUBLIC_APP_BASE_URL}/event/${props.id}`; const mailParams = createMailParams({ ...props, - subject: `Registrert: ${props.summary}`, + subject: `Påmelding bekreftet: ${props.summary}`, + html: `Ønsker du å melde deg av arrangementet, kan du gjøre det via vår nettside.`, icsFile, }); @@ -31,7 +34,7 @@ export const sendEventRegistrationConfirmed = async (props: EventProps) => { return result; }; -export const sendEventRegistrationUpdate = async (props: EventProps) => { +export const sendInviteUpdate = async (props: EventProps) => { const icsFile = createIcsFile(props); const mailParams = createMailParams({ ...props, @@ -43,18 +46,6 @@ export const sendEventRegistrationUpdate = async (props: EventProps) => { return result; }; -export const sendEventCanceled = async (props: EventProps) => { - const icsFile = createIcsFile({ ...props, method: ICalCalendarMethod.CANCEL }); - const mailParams = createMailParams({ - ...props, - subject: `Avlyst: ${props.summary}`, - icsFile, - }); - - const result = await sendMail(mailParams); - return result; -}; - const createIcsFile = ({ id, summary, @@ -64,11 +55,10 @@ const createIcsFile = ({ location, organiser, mailTo, - method, }: EventProps) => { const url = `${PUBLIC_APP_BASE_URL}/event/${id}`; - const calendar = ical({ name: organiser, method: method ?? ICalCalendarMethod.REQUEST }); + const calendar = ical({ name: organiser, method: ICalCalendarMethod.REQUEST }); calendar.createEvent({ id, summary, @@ -80,7 +70,6 @@ const createIcsFile = ({ attendees: [ { email: mailTo, - rsvp: true, status: ICalAttendeeStatus.ACCEPTED, role: ICalAttendeeRole.REQ, }, @@ -94,11 +83,12 @@ const createIcsFile = ({ return Buffer.from(calendar.toString()); }; -const createMailParams = ({ organiser, mailTo, subject, icsFile }: EmailParams) => { +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 new file mode 100644 index 0000000..8e28c9c --- /dev/null +++ b/app/src/lib/email/event/unregistration.ts @@ -0,0 +1,84 @@ +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 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}`, + 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/schemas/external/schema.ts b/app/src/lib/schemas/external/schema.ts index c8c6922..4a37618 100644 --- a/app/src/lib/schemas/external/schema.ts +++ b/app/src/lib/schemas/external/schema.ts @@ -1,10 +1,14 @@ import { z } from "zod"; import validator from "validator"; +import { validateDomain } from "$lib/utils/domain"; export const registrationSchemaExternal = z.object({ subject: z.null(), // Honeypot fullName: z.string().min(2), - email: z.string().email(), + email: z + .string() + .email() + .refine((email) => !validateDomain(email)), telephone: z.string().refine(validator.isMobilePhone).nullable(), firm: z.string().min(2).nullable(), foodPreference: z.string().nullable(), diff --git a/app/src/lib/server/sanity/queries.ts b/app/src/lib/server/sanity/queries.ts index 4b4f76a..a6f5a66 100644 --- a/app/src/lib/server/sanity/queries.ts +++ b/app/src/lib/server/sanity/queries.ts @@ -17,8 +17,8 @@ export const pastEventsQuery = groq`*[_type == "event" && start <= now()] | orde export const externalFutureEventsQuery = groq`*[_type == "event" && start > now() && visibleForExternals] | order(start asc) `; export const externalPastEventsQuery = groq`*[_type == "event" && start <= now() && visibleForExternals] | order(start desc)`; -export const getEventContent = async ({ id }: { id: string }) => { - return await clientWithoutStega.fetch(eventQuery, { id }); +export const getEventContent = async ({ document_id }: { document_id: string }) => { + return await clientWithoutStega.fetch(eventQuery, { id: document_id }); }; export const getFutureEvents = async () => { diff --git a/app/src/lib/server/supabase/queries.ts b/app/src/lib/server/supabase/queries.ts index a243e1d..d5f9954 100644 --- a/app/src/lib/server/supabase/queries.ts +++ b/app/src/lib/server/supabase/queries.ts @@ -34,7 +34,7 @@ export const deleteEventParticipant = async ({ return result; }; -export const updateEventParticipantAttending = async ({ +export const setParticipantNotAttending = async ({ event_id, email, }: Pick, "event_id" | "email">) => { @@ -138,7 +138,7 @@ export const getNumberOfParticipants = async ({ return result.data?.event_participant.length || 0; }; -export const getIsAttendingEvent = async ({ +export const getIsParticipantAttendingEvent = async ({ email, document_id, }: { @@ -159,7 +159,7 @@ export const getIsAttendingEvent = async ({ return false; }; -export const getAttendingEventsByEmail = async ({ +export const getParticipantAttendingEvents = async ({ email, }: { email: Tables<"event_participant">["email"]; diff --git a/app/src/models/jwt.model.ts b/app/src/models/jwt.model.ts index 8e22042..4c01fb5 100644 --- a/app/src/models/jwt.model.ts +++ b/app/src/models/jwt.model.ts @@ -1,6 +1,7 @@ import type { JwtPayload } from "jsonwebtoken"; -interface TokenData { +export interface TokenData { + document_id: string; event_id: number; email: string; } diff --git a/app/src/routes/+page.server.ts b/app/src/routes/+page.server.ts index c547b89..1faf1e2 100644 --- a/app/src/routes/+page.server.ts +++ b/app/src/routes/+page.server.ts @@ -5,7 +5,7 @@ import { getExternalPastEvents, getExternalFutureEvents, } from "$lib/server/sanity/queries"; -import { getAttendingEventsByEmail } from "$lib/server/supabase/queries"; +import { getParticipantAttendingEvents } from "$lib/server/supabase/queries"; import type { EventWithAttending } from "$models/databaseView.model"; export const load: PageServerLoad = async ({ url, locals }) => { @@ -15,7 +15,7 @@ export const load: PageServerLoad = async ({ url, locals }) => { if (auth?.user?.email) { const futureEventsContent = await getFutureEvents(); - const futureEventsAttending = await getAttendingEventsByEmail({ + const futureEventsAttending = await getParticipantAttendingEvents({ email: auth.user.email, }); diff --git a/app/src/routes/api/send-event-canceled/+server.ts b/app/src/routes/api/send-event-canceled/+server.ts index 8c261ea..a59430e 100644 --- a/app/src/routes/api/send-event-canceled/+server.ts +++ b/app/src/routes/api/send-event-canceled/+server.ts @@ -1,5 +1,5 @@ import { APP_API_TOKEN } from "$env/static/private"; -import { sendEventCanceled } from "$lib/email/event-registration"; +import { sendCanceled } from "$lib/email/event/canceled"; import { getAttendingParticipants } from "$lib/server/supabase/queries"; import { json, type RequestHandler } from "@sveltejs/kit"; @@ -38,7 +38,7 @@ export const POST: RequestHandler = async ({ request }) => { } const sendPromises = participants.map(({ email }) => - sendEventCanceled({ + sendCanceled({ ...props, mailTo: email, }) diff --git a/app/src/routes/api/send-event-update/+server.ts b/app/src/routes/api/send-event-update/+server.ts index 17eca0e..b92a076 100644 --- a/app/src/routes/api/send-event-update/+server.ts +++ b/app/src/routes/api/send-event-update/+server.ts @@ -1,5 +1,5 @@ import { APP_API_TOKEN } from "$env/static/private"; -import { sendEventRegistrationUpdate } from "$lib/email/event-registration"; +import { sendInviteUpdate } from "$lib/email/event/registration"; import { getAttendingParticipants } from "$lib/server/supabase/queries"; import { json, type RequestHandler } from "@sveltejs/kit"; @@ -38,7 +38,7 @@ export const POST: RequestHandler = async ({ request }) => { } const sendPromises = participants.map(({ email }) => - sendEventRegistrationUpdate({ + sendInviteUpdate({ ...props, mailTo: email, }) diff --git a/app/src/routes/api/webhook/+server.ts b/app/src/routes/api/webhook/+server.ts new file mode 100644 index 0000000..19e7803 --- /dev/null +++ b/app/src/routes/api/webhook/+server.ts @@ -0,0 +1,33 @@ +import { json, type RequestHandler } from "@sveltejs/kit"; + +export const GET: RequestHandler = async ({ params }) => { + const event = "Hei"; + + // For simplicity, assume event details + const eventDetails = { + title: "Sample Event", + description: "This is a sample event.", + startTime: new Date(), + endTime: new Date(Date.now() + 3600 * 1000), // 1 hour later + }; + + const icsContent = ` +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SUMMARY:${eventDetails.title} +DESCRIPTION:${eventDetails.description} +DTSTART:${eventDetails.startTime} +DTEND:${eventDetails.endTime} +END:VEVENT +END:VCALENDAR +`; + + return new Response(icsContent, { + status: 200, + headers: { + "Content-Type": "text/calendar", + "Content-Disposition": `attachment; filename="${event}.ics"`, + }, + }); +}; diff --git a/app/src/routes/event/[id]/+page.server.ts b/app/src/routes/event/[id]/+page.server.ts index 66c36c3..f9fffd8 100644 --- a/app/src/routes/event/[id]/+page.server.ts +++ b/app/src/routes/event/[id]/+page.server.ts @@ -4,7 +4,7 @@ import type { Event } from "$models/sanity.model"; import type { Actions, PageServerLoad } from "./$types"; import { getNumberOfParticipants, - getIsAttendingEvent, + getIsParticipantAttendingEvent, getInternalEventParticipantNames, } from "$lib/server/supabase/queries"; import { @@ -28,14 +28,16 @@ import { eventQuery as query } from "$lib/server/sanity/queries"; export const load: PageServerLoad = async ({ params: { id }, locals }) => { const auth = await locals.auth(); const numberOfParticipants = await getNumberOfParticipants({ document_id: id }); + const initial = await locals.loadQuery(query, { id }); if (auth?.user?.name && auth.user.email) { - const initial = await locals.loadQuery(query, { id }); - const registrationForm = await superValidate(zod(registrationSchemaInternal)); const unregistrationForm = await superValidate(zod(unregistrationSchemaInternal)); - const isAttending = await getIsAttendingEvent({ document_id: id, email: auth.user.email }); + const isAttending = await getIsParticipantAttendingEvent({ + document_id: id, + email: auth.user.email, + }); const internalParticipantNames = await getInternalEventParticipantNames({ document_id: id }); return { @@ -49,7 +51,6 @@ export const load: PageServerLoad = async ({ params: { id }, locals }) => { }; } - const initial = await locals.loadQuery(query, { id }); const registrationForm = await superValidate(zod(registrationSchemaExternal)); const unregistrationForm = await superValidate(zod(unregistrationSchemaExternal)); diff --git a/app/src/routes/event/unregistration/[token]/+page.server.ts b/app/src/routes/event/unregistration/[token]/+page.server.ts index d62d1ca..b6e9cfe 100644 --- a/app/src/routes/event/unregistration/[token]/+page.server.ts +++ b/app/src/routes/event/unregistration/[token]/+page.server.ts @@ -2,32 +2,68 @@ import jwt from "jsonwebtoken"; import type { PageServerLoad } from "./$types"; import { getUnsubscribeSecret } from "$lib/auth/secret"; import type { DecodedToken } from "$models/jwt.model"; -import { updateEventParticipantAttending } from "$lib/server/supabase/queries"; +import { setParticipantNotAttending } from "$lib/server/supabase/queries"; +import { getEventContent } from "$lib/server/sanity/queries"; +import { sendUnregistrationConfirmed } from "$lib/email/event/unregistration"; export const load: PageServerLoad = async ({ params: { token } }) => { const tokenDecoded = jwt.decode(token, { complete: true }) as { payload: DecodedToken | null; } | null; - if (tokenDecoded?.payload?.data) { - const secret = getUnsubscribeSecret(tokenDecoded.payload.data); - - try { - if (jwt.verify(token, secret)) { - await updateEventParticipantAttending(tokenDecoded.payload.data); - return { success: true, message: "Du er nå meldt av arrangementet." }; - } - } catch (error) { - if (error instanceof jwt.TokenExpiredError) { - return { - error: true, - message: "Lenken du brukte er ikke lenger gyldig. Vennligst forsøk igjen senere.", - }; - } + if (!tokenDecoded?.payload?.data) { + return { + error: true, + message: "Vi kunne dessverre ikke melde deg av arrangementet. Vennligst prøv igjen senere.", + }; + } + + const { data } = tokenDecoded.payload; + const secret = getUnsubscribeSecret(data); + + try { + if (jwt.verify(token, secret)) { + await setParticipantNotAttending(tokenDecoded.payload.data); + } + } catch (error) { + if (error instanceof jwt.TokenExpiredError) { + return { + error: true, + message: "Lenken du brukte er ikke lenger gyldig. Vennligst forsøk igjen senere.", + }; + } + } + + const { document_id, email } = data; + + const eventContent = await getEventContent({ document_id }); + + const emailPayload = { + id: document_id, + mailTo: email, + summary: eventContent.title, + description: eventContent.summary, + start: eventContent.start, + end: eventContent.end, + location: eventContent.place, + organiser: eventContent.organisers.join(" | "), + }; + + if (process.env.NODE_ENV !== "development") { + const { error: emailError } = await sendUnregistrationConfirmed(emailPayload); + + if (emailError) { + console.error("Error: Failed to send email"); + + return { + text: "Det har oppstått en feil. Du er meldt av arrangement 👋 men e-post bekreftelse er ikke sendt.", + warning: true, + }; } } + return { - error: true, - message: "Vi kunne dessverre ikke melde deg av arrangementet. Vennligst prøv igjen senere.", + success: true, + message: "Du er nå meldt av arrangementet 👋 Vi har sendt deg en bekreftelse på e-post.", }; };