summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--pnpm-lock.yaml22
-rw-r--r--public/js/modules/new.js44
-rwxr-xr-xsrc/routes.js115
-rw-r--r--src/routes/event.ts126
-rw-r--r--views/partials/importeventform.handlebars24
6 files changed, 212 insertions, 120 deletions
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 @@
<img class="img-thumbnail mb-3 d-block mx-auto" src="/images/facebook-export.png" alt="Image showing the location of the export option on Facebook" />
-<form id="icsImportForm" action="/importevent" method="post" enctype="multipart/form-data">
+<form id="icsImportForm" enctype="multipart/form-data" x-data="importEventForm()" @submit.prevent="submitForm">
<div class="form-group">
<div class="custom-file" id="icsImportContainer">
- <input required name="icsImportControl" type="file" class="custom-file-input" id="icsImportControl" aria-describedby="fileHelp" accept="text/calendar">
+ <input required name="icsImportControl" type="file" class="custom-file-input" id="icsImportControl" aria-describedby="fileHelp" accept="text/calendar" x-ref="icsImportControl"/>
<label name="icsImportLabel" class="custom-file-label" id="icsImportLabel" for="icsImportControl">
<i class="far fa-file-alt"></i> Select file
</label>
@@ -17,8 +17,24 @@
<div class="form-group">
<label for="creatorEmail" class="form-label">Your email</label>
<div class="form-group">
- <input type="email" class="form-control" id="creatorEmail" name="creatorEmail" placeholder="We won't spam you <3" data-validation="email" data-validation-optional="true">
- <small class="form-text">We will send your secret editing link to this email address.</small>
+ <input type="email" class="form-control" id="importCreatorEmail" name="creatorEmail" placeholder="Will not be shown anywhere (optional)." x-model="data.creatorEmail" >
+ <small class="form-text">If you provide your email, we will send your secret editing password here, and use it to notify you of updates to the event.</small>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-12">
+ <div
+ class="alert alert-danger"
+ role="alert"
+ x-show="errors.length > 0"
+ >
+ <p><i class="fas fa-exclamation-triangle"></i> Please fix these errors:</p>
+ <ul>
+ <template x-for="error in errors">
+ <li x-text="error.message"></li>
+ </template>
+ </ul>
+ </div>
</div>
</div>
<button type="submit" class="d-block mt-3 mx-auto btn btn-primary w-50">Import</button>