diff --git a/src/lib/email.ts b/src/lib/email.ts index fc585a16..694e50b1 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -21,7 +21,8 @@ type EmailTemplateName = | "eventGroupUpdated" | "removeEventAttendee" | "subscribed" - | "unattendEvent"; + | "unattendEvent" + | "rsvpNotification"; export class EmailService { nodemailerTransporter: Transporter | undefined = undefined; diff --git a/src/routes.js b/src/routes.js index 6659670b..aba9b53e 100755 --- a/src/routes.js +++ b/src/routes.js @@ -629,22 +629,36 @@ router.post("/attendevent/:eventID", async (req, res) => { "success", "Attendee added to event " + req.params.eventID, ); - if (req.body.attendeeEmail) { - req.emailService.sendEmailFromTemplate({ - to: req.body.attendeeEmail, - subject: i18next.t("routes.addeventattendeesubject", {eventName: event?.name}), - templateName: "addEventAttendee", - templateData:{ - eventID: req.params.eventID, - removalPassword: req.body.removalPassword, - removalPasswordHash: hashString( - req.body.removalPassword, - ), - }, - }).catch((e) => { - console.error('error sending addEventAttendee email', e.toString()); - res.status(500).end(); - }); + if (req.body.attendeeEmail) { + const inviteToken = attendee.removalPassword; + + const acceptUrl = `https://${config.general.domain}/event/${event.id}/rsvp?token=${inviteToken}&attendance=accepted`; + const declineUrl = `https://${config.general.domain}/event/${event.id}/rsvp?token=${inviteToken}&attendance=declined`; + + req.emailService + .sendEmailFromTemplate({ + to: req.body.attendeeEmail, + subject: i18next.t("routes.addeventattendeesubject", { + eventName: event?.name, + }), + templateName: "addEventAttendee", + templateData: { + eventID: req.params.eventID, + removalPassword: req.body.removalPassword, + removalPasswordHash: hashString( + req.body.removalPassword, + ), + acceptUrl, + declineUrl, + }, + }) + .catch((e) => { + console.error( + "error sending addEventAttendee email", + e.toString(), + ); + res.status(500).end(); + }); } res.redirect(`/${req.params.eventID}`); }) diff --git a/src/routes/event.ts b/src/routes/event.ts index 84a7c6b0..d1e3a9b1 100644 --- a/src/routes/event.ts +++ b/src/routes/event.ts @@ -272,6 +272,78 @@ router.post( }, ); +router.get("/event/:eventID/rsvp", async (req: Request, res: Response) => { + try { + const eventID = req.params.eventID; + const token = String(req.query.token || ""); + const attendance = String(req.query.attendance || ""); + + // 1️⃣ Load the Event document + const event = await Event.findOne({ id: eventID }).exec(); + if (!event) { + return res.status(404).send("Event not found."); + } + + // 2️⃣ Find the matching attendee by removalPassword + const attendeeEntry = event.attendees?.find( + (a) => a.removalPassword === token, + ); + if (!attendeeEntry) { + return res.status(400).send("Invalid RSVP link or token."); + } + + // 3️⃣ Update their status (“accepted” → “attending”, otherwise “declined”) + if (attendance === "accepted") { + attendeeEntry.status = "attending"; + } else if (attendance === "declined") { + attendeeEntry.status = "declined"; + } else { + return res.status(400).send("Invalid attendance value."); + } + + // 4️⃣ Persist to the database + await event.save(); + + // 5️⃣ Notify the host via email + try { + const hostEmail = event.creatorEmail; + if (hostEmail) { + const eventUrl = `https://${config.general.domain}/${event.id}`; + const attendanceText = + attendeeEntry.status === "attending" + ? "accepted" + : "declined"; + + await req.emailService.sendEmailFromTemplate({ + to: hostEmail, + subject: `${attendeeEntry.email} has ${attendanceText} your invitation`, + templateName: "rsvpNotification", + templateData: { + hostName: event.hostName || hostEmail.split("@")[0], + attendeeEmail: attendeeEntry.email, + attendance: attendanceText, + eventTitle: event.name, + eventUrl: eventUrl, + siteName: config.general.site_name, + }, + }); + } + } catch (emailErr) { + console.error( + "Failed to send RSVP notification to host:", + emailErr, + ); + // Don’t block the RSVP flow if email sending fails + } + + // 6️⃣ Redirect back to the event page with a success flag + return res.redirect(`/${eventID}?rsvp=success`); + } catch (err) { + console.error("RSVP error:", err); + return res.status(500).json({ error: "Unexpected error during RSVP." }); + } +}); + router.put( "/event/:eventID", upload.single("imageUpload"), diff --git a/views/emails/addEventAttendee/addEventAttendeeHtml.handlebars b/views/emails/addEventAttendee/addEventAttendeeHtml.handlebars index fcd68b44..27cb1383 100644 --- a/views/emails/addEventAttendee/addEventAttendeeHtml.handlebars +++ b/views/emails/addEventAttendee/addEventAttendeeHtml.handlebars @@ -1,5 +1,41 @@
{{t "views.emails.addeventattendee.preface" }}
{{t "views.emails.addeventattendee.eventlink" }}: https://{{domain}}/{{eventID}}
+ +
+ RSVP:
+
+
+Accept
+
+
+Decline
+
+
{{t "views.emails.addeventattendee.toremove" }}: {{t "views.emails.addeventattendee.clicktocancel" }}.
{{{t "views.emails.addeventattendee.removapasswordhtml" }}}: {{removalPassword}}
{{t "views.emails.love" }}
diff --git a/views/emails/addEventAttendee/addEventAttendeeText.handlebars b/views/emails/addEventAttendee/addEventAttendeeText.handlebars index c264989d..e93815dd 100644 --- a/views/emails/addEventAttendee/addEventAttendeeText.handlebars +++ b/views/emails/addEventAttendee/addEventAttendeeText.handlebars @@ -2,6 +2,9 @@ {{t "views.emails.addeventattendee.eventlink" }}: https://{{domain}}/{{eventID}} +Accept: {{acceptUrl}} +Decline: {{declineUrl}} + {{t "views.emails.addeventattendee.removelink" }}: https://{{domain}}/event/{{eventID}}/unattend/{{removalPasswordHash}} {{t "views.emails.addeventattendee.removepassword" }}: {{removalPassword}} diff --git a/views/emails/rsvpNotification/rsvpNotificationHtml.handlebars b/views/emails/rsvpNotification/rsvpNotificationHtml.handlebars new file mode 100644 index 00000000..354fe06e --- /dev/null +++ b/views/emails/rsvpNotification/rsvpNotificationHtml.handlebars @@ -0,0 +1,18 @@ + + +Hi {{hostName}},
+ ++ {{attendeeEmail}} has {{attendance}} your invitation to + {{eventTitle}}. +
+ ++ You can view the event page here: + {{eventUrl}} +
+ +Thanks for using {{siteName}}!
+ + diff --git a/views/emails/rsvpNotification/rsvpNotificationText.handlebars b/views/emails/rsvpNotification/rsvpNotificationText.handlebars new file mode 100644 index 00000000..f61545ab --- /dev/null +++ b/views/emails/rsvpNotification/rsvpNotificationText.handlebars @@ -0,0 +1,7 @@ +Hi {{hostName}}, + +{{attendeeEmail}} has {{attendance}} your invitation to: + {{eventTitle}} + {{eventUrl}} + +Thanks for using {{siteName}}!