summaryrefslogtreecommitdiff
path: root/src/routes
diff options
context:
space:
mode:
authorRaphael Kabo <raphaelkabo@hey.com>2024-02-25 17:56:25 +0000
committerRaphael Kabo <raphaelkabo@hey.com>2024-02-25 17:56:25 +0000
commitcd0f291eb1a608589fcc2c1875fa7099ed8e2c51 (patch)
tree05b1d8b1d63baed174883cc96807051e530969a2 /src/routes
parentb17238eb2840553c69fc2dae168be557afbcee9c (diff)
feat: optionally restrict event creation to specific emails
Diffstat (limited to 'src/routes')
-rw-r--r--src/routes/event.ts3
-rw-r--r--src/routes/frontend.ts32
-rw-r--r--src/routes/group.ts2
-rw-r--r--src/routes/magicLink.ts70
4 files changed, 107 insertions, 0 deletions
diff --git a/src/routes/event.ts b/src/routes/event.ts
index cfd877e..fb9d8c7 100644
--- a/src/routes/event.ts
+++ b/src/routes/event.ts
@@ -26,6 +26,7 @@ import { sendEmailFromTemplate } from "../lib/email.js";
import crypto from "crypto";
import ical from "ical";
import { markdownToSanitizedHTML } from "../util/markdown.js";
+import { checkMagicLink } from "../lib/middleware.js";
const config = getConfig();
@@ -60,6 +61,7 @@ const router = Router();
router.post(
"/event",
upload.single("imageUpload"),
+ checkMagicLink,
async (req: Request, res: Response) => {
const { data: eventData, errors } = validateEventData(req.body);
if (errors && errors.length > 0) {
@@ -527,6 +529,7 @@ router.put(
router.post(
"/import/event",
icsUpload.single("icsImportControl"),
+ checkMagicLink,
async (req: Request, res: Response) => {
if (!req.file) {
return res.status(400).json({
diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts
index 8ddfbf6..0d8793a 100644
--- a/src/routes/frontend.ts
+++ b/src/routes/frontend.ts
@@ -10,6 +10,7 @@ import {
acceptsActivityPub,
activityPubContentType,
} from "../lib/activitypub.js";
+import MagicLink from "../models/MagicLink.js";
const config = getConfig();
@@ -19,9 +20,40 @@ router.get("/", (_: Request, res: Response) => {
});
router.get("/new", (_: Request, res: Response) => {
+ if (config.general.creator_email_addresses?.length) {
+ return res.render("createEventMagicLink", frontendConfig());
+ }
+ return res.render("newevent", {
+ title: "New event",
+ ...frontendConfig(),
+ });
+});
+
+router.get("/new/:magicLinkToken", async (req: Request, res: Response) => {
+ // If we don't have any creator email addresses, we don't need to check the magic link
+ // so we can just redirect to the new event page
+ if (!config.general.creator_email_addresses?.length) {
+ return res.redirect("/new");
+ }
+ const magicLink = await MagicLink.findOne({
+ token: req.params.magicLinkToken,
+ expiryTime: { $gt: new Date() },
+ permittedActions: "createEvent",
+ });
+ if (!magicLink) {
+ return res.render("createEventMagicLink", {
+ ...frontendConfig(),
+ message: {
+ type: "danger",
+ text: "This magic link is invalid or has expired. Please request a new one here.",
+ },
+ });
+ }
res.render("newevent", {
title: "New event",
...frontendConfig(),
+ magicLinkToken: req.params.magicLinkToken,
+ creatorEmail: magicLink.email,
});
});
diff --git a/src/routes/group.ts b/src/routes/group.ts
index 40dcccb..34377b0 100644
--- a/src/routes/group.ts
+++ b/src/routes/group.ts
@@ -9,6 +9,7 @@ import EventGroup from "../models/EventGroup.js";
import { sendEmailFromTemplate } from "../lib/email.js";
import { marked } from "marked";
import { renderPlain } from "../util/markdown.js";
+import { checkMagicLink } from "../lib/middleware.js";
const config = getConfig();
@@ -32,6 +33,7 @@ const router = Router();
router.post(
"/group",
upload.single("imageUpload"),
+ checkMagicLink,
async (req: Request, res: Response) => {
const { data: groupData, errors } = validateGroupData(req.body);
if (errors && errors.length > 0) {
diff --git a/src/routes/magicLink.ts b/src/routes/magicLink.ts
new file mode 100644
index 0000000..24f0667
--- /dev/null
+++ b/src/routes/magicLink.ts
@@ -0,0 +1,70 @@
+import { Router, Request, Response } from "express";
+import getConfig, { frontendConfig } from "../lib/config.js";
+import { sendEmailFromTemplate } from "../lib/email.js";
+import { generateMagicLinkToken } from "../util/generator.js";
+import MagicLink from "../models/MagicLink.js";
+
+const router = Router();
+const config = getConfig();
+
+router.post("/magic-link/event/create", async (req: Request, res: Response) => {
+ const { email } = req.body;
+ if (!email) {
+ res.render("createEventMagicLink", {
+ ...frontendConfig(),
+ message: {
+ type: "danger",
+ text: "Please provide an email address.",
+ },
+ });
+ return;
+ }
+ const allowedEmails = config.general.creator_email_addresses;
+ if (!allowedEmails?.length) {
+ // No creator email addresses are configured, so skip the magic link check
+ return res.redirect("/new");
+ }
+ if (!allowedEmails.includes(email)) {
+ res.render("createEventMagicLink", {
+ ...frontendConfig(),
+ message: {
+ type: "success",
+ text: "Thanks! If this email address can create events, you should receive an email with a magic link.",
+ },
+ });
+ return;
+ }
+ const token = generateMagicLinkToken();
+ const magicLink = new MagicLink({
+ email,
+ token,
+ expiryTime: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
+ permittedActions: ["createEvent"],
+ });
+ await magicLink.save();
+
+ // Take this opportunity to delete any expired magic links
+ await MagicLink.deleteMany({ expiryTime: { $lt: new Date() } });
+
+ sendEmailFromTemplate(
+ email,
+ `Magic link to create an event`,
+ "createEventMagicLink",
+ {
+ token,
+ siteName: config.general.site_name,
+ siteLogo: config.general.email_logo_url,
+ domain: config.general.domain,
+ },
+ req,
+ );
+ res.render("createEventMagicLink", {
+ ...frontendConfig(),
+ message: {
+ type: "success",
+ text: "Thanks! If this email address can create events, you should receive an email with a magic link.",
+ },
+ });
+});
+
+export default router;