Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/lib/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ type EmailTemplateName =
| "eventGroupUpdated"
| "removeEventAttendee"
| "subscribed"
| "unattendEvent";
| "unattendEvent"
| "rsvpNotification";

export class EmailService {
nodemailerTransporter: Transporter | undefined = undefined;
Expand Down
46 changes: 30 additions & 16 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
})
Expand Down
72 changes: 72 additions & 0 deletions src/routes/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
36 changes: 36 additions & 0 deletions views/emails/addEventAttendee/addEventAttendeeHtml.handlebars
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.addeventattendee.preface" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.addeventattendee.eventlink" }}: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
<!-- ────── NEW: Accept / Decline buttons ────── -->
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">
<strong>RSVP:</strong>
<br/>
<a
href="{{acceptUrl}}"
style="
display: inline-block;
margin-right: 8px;
padding: 8px 12px;
color: #ffffff;
background-color: #28a745;
text-decoration: none;
border-radius: 4px;
font-weight: bold;
"
>
Accept
</a>
<a
href="{{declineUrl}}"
style="
display: inline-block;
padding: 8px 12px;
color: #ffffff;
background-color: #dc3545;
text-decoration: none;
border-radius: 4px;
font-weight: bold;
"
>
Decline
</a>
</p>
<!-- ──────────────────────────────────────────── -->

<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.addeventattendee.toremove" }}: <a href="https://{{domain}}/event/{{eventID}}/unattend/{{removalPasswordHash}}">{{t "views.emails.addeventattendee.clicktocancel" }}</a>.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{{t "views.emails.addeventattendee.removapasswordhtml" }}}: {{removalPassword}}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.love" }}</p>
Expand Down
3 changes: 3 additions & 0 deletions views/emails/addEventAttendee/addEventAttendeeText.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
18 changes: 18 additions & 0 deletions views/emails/rsvpNotification/rsvpNotificationHtml.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<html>
<body>
<p>Hi {{hostName}},</p>

<p>
{{attendeeEmail}} has <strong>{{attendance}}</strong> your invitation to
<a href="{{eventUrl}}">{{eventTitle}}</a>.
</p>

<p>
You can view the event page here:
<a href="{{eventUrl}}">{{eventUrl}}</a>
</p>

<hr>
<p>Thanks for using {{siteName}}!</p>
</body>
</html>
7 changes: 7 additions & 0 deletions views/emails/rsvpNotification/rsvpNotificationText.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Hi {{hostName}},

{{attendeeEmail}} has {{attendance}} your invitation to:
{{eventTitle}}
{{eventUrl}}

Thanks for using {{siteName}}!