diff --git a/package.json b/package.json index 1570fb0..5ec07bc 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ }, "devDependencies": { "@types/express": "^4.17.18", + "@types/ical": "^0.8.1", "@types/multer": "^1.4.8", "@types/node": "^20.8.2", "@types/nodemailer": "^6.4.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9871cee..9c1c8e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,9 @@ devDependencies: '@types/express': specifier: ^4.17.18 version: 4.17.18 + '@types/ical': + specifier: ^0.8.1 + version: 0.8.1 '@types/multer': specifier: ^1.4.8 version: 1.4.8 @@ -712,6 +715,12 @@ packages: resolution: {integrity: sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==} dev: true + /@types/ical@0.8.1: + resolution: {integrity: sha512-JQyqcdMGEa0aUaZPablO5okXvrAspGMzQYriYUV0C5RjDOk/7dqFklvl9yA1uidc0qtrZu4VBFgF0LXhPGPAJw==} + dependencies: + rrule: 2.6.4 + dev: true + /@types/mime@1.3.3: resolution: {integrity: sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==} dev: true @@ -2470,7 +2479,6 @@ packages: /luxon@1.28.1: resolution: {integrity: sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==} requiresBuild: true - dev: false optional: true /marked@9.1.0: @@ -3192,6 +3200,14 @@ packages: luxon: 1.28.1 dev: false + /rrule@2.6.4: + resolution: {integrity: sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==} + dependencies: + tslib: 1.14.1 + optionalDependencies: + luxon: 1.28.1 + dev: true + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -3536,6 +3552,10 @@ packages: url-parse: 1.5.10 dev: true + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} diff --git a/public/js/modules/new.js b/public/js/modules/new.js index fbb99ed..84391b7 100644 --- a/public/js/modules/new.js +++ b/public/js/modules/new.js @@ -199,3 +199,47 @@ function newEventGroupForm() { }, }; } + +function importEventForm() { + return { + data: { + creatorEmail: "", + }, + errors: [], + submitting: false, + async submitForm() { + this.submitting = true; + this.errors = []; + const formData = new FormData(); + for (const [key, value] of Object.entries(this.data)) { + formData.append(key, value); + } + formData.append( + "icsImportControl", + this.$refs.icsImportControl.files[0], + ); + try { + const response = await fetch("/import/event", { + method: "POST", + body: formData, + }); + this.submitting = false; + if (!response.ok) { + if (response.status !== 400) { + this.errors = unexpectedError; + return; + } + const json = await response.json(); + this.errors = json.errors; + return; + } + const json = await response.json(); + window.location.assign(json.url); + } catch (error) { + console.log(error); + this.errors = unexpectedError; + this.submitting = false; + } + }, + }; +} diff --git a/src/routes.js b/src/routes.js index 96420c7..65a2934 100755 --- a/src/routes.js +++ b/src/routes.js @@ -183,121 +183,6 @@ schedule.scheduleJob("59 23 * * *", function (fireDate) { }); // BACKEND ROUTES -router.post("/importevent", (req, res) => { - let eventID = nanoid(); - let editToken = randomstring.generate(); - if (req.files && Object.keys(req.files).length !== 0) { - let iCalObject = ical.parseICS( - req.files.icsImportControl.data.toString("utf8"), - ); - let importedEventData = iCalObject[Object.keys(iCalObject)]; - - let creatorEmail; - if (req.body.creatorEmail) { - creatorEmail = req.body.creatorEmail; - } else if (importedEventData.organizer) { - creatorEmail = importedEventData.organizer.val.replace( - "MAILTO:", - "", - ); - } - - const event = new Event({ - id: eventID, - type: "public", - name: importedEventData.summary, - location: importedEventData.location, - start: importedEventData.start, - end: importedEventData.end, - timezone: - typeof importedEventData.start.tz !== "undefined" - ? importedEventData.start.tz - : "Etc/UTC", - description: importedEventData.description, - image: "", - creatorEmail: creatorEmail, - url: "", - hostName: importedEventData.organizer - ? importedEventData.organizer.params.CN.replace(/["]+/g, "") - : "", - viewPassword: "", - editPassword: "", - editToken: editToken, - usersCanAttend: false, - showUsersList: false, - usersCanComment: false, - firstLoad: true, - }); - event - .save() - .then(() => { - addToLog( - "createEvent", - "success", - "Event " + eventID + " created", - ); - // Send email with edit link - if (creatorEmail && sendEmails) { - req.app.get("hbsInstance").renderView( - "./views/emails/createevent.handlebars", - { - eventID, - editToken, - siteName, - siteLogo, - domain, - cache: true, - layout: "email.handlebars", - }, - function (err, html) { - const msg = { - to: req.body.creatorEmail, - from: { - name: siteName, - email: contactEmail, - address: contactEmail, - }, - subject: `${siteName}: ${importedEventData.summary}`, - html, - }; - switch (mailService) { - case "sendgrid": - sgMail.send(msg).catch((e) => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - case "nodemailer": - nodemailerTransporter - .sendMail(msg) - .catch((e) => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - } - }, - ); - } - res.writeHead(302, { - Location: "/" + eventID + "?e=" + editToken, - }); - res.end(); - }) - .catch((err) => { - res.send("Database error, please try again :("); - addToLog( - "createEvent", - "error", - "Attempt to create event failed with error: " + err, - ); - }); - } else { - console.log("Files array is empty!"); - res.status(500).end(); - } -}); - router.post("/verifytoken/event/:eventID", (req, res) => { Event.findOne({ id: req.params.eventID, diff --git a/src/routes/event.ts b/src/routes/event.ts index be27fd4..2245009 100644 --- a/src/routes/event.ts +++ b/src/routes/event.ts @@ -25,6 +25,7 @@ import { import getConfig from "../lib/config.js"; import { sendEmailFromTemplate } from "../lib/email.js"; import crypto from "crypto"; +import ical from "ical"; const config = getConfig(); @@ -42,6 +43,17 @@ const upload = multer({ cb(null, true); }, }); +const icsUpload = multer({ + storage: storage, + limits: { fileSize: 10 * 1024 * 1024 }, + fileFilter: function (_, file, cb) { + const filetype = "text/calendar"; + if (file.mimetype !== filetype) { + return cb(new Error("Only ICS files are allowed.")); + } + cb(null, true); + }, +}); const router = Router(); @@ -84,6 +96,7 @@ router.post( ); }); } + const startUTC = moment.tz(eventData.eventStart, eventData.timezone); const endUTC = moment.tz(eventData.eventEnd, eventData.timezone); let eventGroup; @@ -511,4 +524,117 @@ router.put( }, ); +router.post( + "/import/event", + icsUpload.single("icsImportControl"), + async (req: Request, res: Response) => { + if (!req.file) { + return res.status(400).json({ + errors: [ + { + message: "No file was provided.", + }, + ], + }); + } + + let eventID = generateEventID(); + let editToken = generateEditToken(); + + let iCalObject = ical.parseICS(req.file.buffer.toString("utf8")); + + let importedEventData = iCalObject[Object.keys(iCalObject)[0]]; + + let creatorEmail: string | undefined; + if (req.body.creatorEmail) { + creatorEmail = req.body.creatorEmail; + } else if (importedEventData.organizer) { + if (typeof importedEventData.organizer === "string") { + creatorEmail = importedEventData.organizer.replace( + "MAILTO:", + "", + ); + } else { + creatorEmail = importedEventData.organizer.val.replace( + "MAILTO:", + "", + ); + } + } + + let hostName: string | undefined; + if (importedEventData.organizer) { + if (typeof importedEventData.organizer === "string") { + hostName = importedEventData.organizer.replace(/["]+/g, ""); + } else { + hostName = importedEventData.organizer.params.CN.replace( + /["]+/g, + "", + ); + } + } + + const event = new Event({ + id: eventID, + type: "public", + name: importedEventData.summary, + location: importedEventData.location, + start: importedEventData.start, + end: importedEventData.end, + timezone: "Etc/UTC", // TODO: get timezone from ics file + description: importedEventData.description, + image: "", + creatorEmail, + url: "", + hostName, + viewPassword: "", + editPassword: "", + editToken: editToken, + usersCanAttend: false, + showUsersList: false, + usersCanComment: false, + firstLoad: true, + }); + try { + await event.save(); + addToLog("createEvent", "success", `Event ${eventID} created`); + // Send email with edit link + if (creatorEmail && req.app.locals.sendEmails) { + sendEmailFromTemplate( + creatorEmail, + `${importedEventData.summary}`, + "createEvent", + { + eventID, + editToken, + siteName: config.general.site_name, + siteLogo: config.general.email_logo_url, + domain: config.general.domain, + }, + req, + ); + } + return res.json({ + eventID: eventID, + editToken: editToken, + url: `/${eventID}?e=${editToken}`, + }); + } catch (err) { + console.error(err); + addToLog( + "createEvent", + "error", + "Attempt to create event failed with error: " + err, + ); + return res.status(500).json({ + errors: [ + { + message: err, + }, + ], + }); + } + }, +); + export default router; diff --git a/views/partials/importeventform.handlebars b/views/partials/importeventform.handlebars index 9ad038a..83bd6c4 100644 --- a/views/partials/importeventform.handlebars +++ b/views/partials/importeventform.handlebars @@ -5,10 +5,10 @@ Image showing the location of the export option on Facebook -
+
- + @@ -17,8 +17,24 @@
- - We will send your secret editing link to this email address. + + If you provide your email, we will send your secret editing password here, and use it to notify you of updates to the event. +
+
+
+
+