From 01b9d0bcec8480b358f040ccf8b7d4b489156d11 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Wed, 27 Apr 2022 21:34:20 +0100 Subject: fix: Move attendee password generation serverside --- models/Event.js | 9 ++- package-lock.json | 49 ++++++++++++++- package.json | 1 + public/js/util.js | 3 - routes.js | 158 ++++++++++++++++++++++++++++++------------------- views/event.handlebars | 37 ++++++++---- 6 files changed, 176 insertions(+), 81 deletions(-) diff --git a/models/Event.js b/models/Event.js index 64cf398..60fd65f 100755 --- a/models/Event.js +++ b/models/Event.js @@ -15,12 +15,15 @@ const Attendees = new mongoose.Schema({ }, removalPassword: { type: String, - trim: true + trim: true, + unique: true, }, id: { type: String, - trim: true - } + trim: true, + unique: true, + }, + created: Date, }) const Followers = new mongoose.Schema({ diff --git a/package-lock.json b/package-lock.json index c6fa3f2..90ef2d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gathio", - "version": "1.1.1", + "version": "1.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gathio", - "version": "1.1.1", + "version": "1.2.1", "license": "GPL-3.0-or-later", "dependencies": { "@sendgrid/mail": "^6.5.5", @@ -31,6 +31,7 @@ "mongoose": "^5.9.18", "multer": "^1.4.1", "nanoid": "^3.1.9", + "niceware": "^3.0.0", "node-schedule": "^1.3.1", "nodemailer": "^6.4.8", "randomstring": "^1.1.5", @@ -908,6 +909,11 @@ "node": ">=8" } }, + "node_modules/binary-search": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", + "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" + }, "node_modules/bl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", @@ -3692,6 +3698,15 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, + "node_modules/niceware": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/niceware/-/niceware-3.0.0.tgz", + "integrity": "sha512-DbeDuqe836Ba4S9vjim4jTbbqmjCMwuAXFCVdh4QAvbmLOhmIQs84IakYrcXd/87VCsj1XKhSmmg7bAmwAEh5A==", + "dependencies": { + "binary-search": "^1.3.6", + "randombytes": "^2.0.6" + } + }, "node_modules/node-schedule": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.2.tgz", @@ -4154,6 +4169,14 @@ "node": ">= 0.8" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/randomstring": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/randomstring/-/randomstring-1.1.5.tgz", @@ -5903,6 +5926,11 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "binary-search": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", + "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" + }, "bl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", @@ -8049,6 +8077,15 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, + "niceware": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/niceware/-/niceware-3.0.0.tgz", + "integrity": "sha512-DbeDuqe836Ba4S9vjim4jTbbqmjCMwuAXFCVdh4QAvbmLOhmIQs84IakYrcXd/87VCsj1XKhSmmg7bAmwAEh5A==", + "requires": { + "binary-search": "^1.3.6", + "randombytes": "^2.0.6" + } + }, "node-schedule": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.2.tgz", @@ -8404,6 +8441,14 @@ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, "randomstring": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/randomstring/-/randomstring-1.1.5.tgz", diff --git a/package.json b/package.json index d662249..4d8c4e7 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "mongoose": "^5.9.18", "multer": "^1.4.1", "nanoid": "^3.1.9", + "niceware": "^3.0.0", "node-schedule": "^1.3.1", "nodemailer": "^6.4.8", "randomstring": "^1.1.5", diff --git a/public/js/util.js b/public/js/util.js index e2e9938..cbfd239 100644 --- a/public/js/util.js +++ b/public/js/util.js @@ -3,7 +3,6 @@ const getStoredToken = function(eventID) { let editTokens = JSON.parse(localStorage.getItem('editTokens')); return editTokens[eventID]; } catch(e) { - console.error(e); localStorage.setItem('editTokens', JSON.stringify({})); return false; } @@ -15,7 +14,6 @@ const addStoredToken = function(eventID, token) { editTokens[eventID] = token; localStorage.setItem('editTokens', JSON.stringify(editTokens)); } catch(e) { - console.error(e); localStorage.setItem('editTokens', JSON.stringify({ [eventID]: token })); return false; } @@ -27,7 +25,6 @@ const removeStoredToken = function(eventID) { delete editTokens[eventID]; localStorage.setItem('editTokens', JSON.stringify(editTokens)); } catch(e) { - console.error(e); localStorage.setItem('editTokens', JSON.stringify({})); return false; } diff --git a/routes.js b/routes.js index 542cd65..7e582b4 100755 --- a/routes.js +++ b/routes.js @@ -26,6 +26,7 @@ const marked = require('marked'); const generateRSAKeypair = require('generate-rsa-keypair'); const crypto = require('crypto'); const request = require('request'); +const niceware = require('niceware'); const domain = require('./config/domain.js').domain; const contactEmail = require('./config/domain.js').email; @@ -172,6 +173,9 @@ schedule.scheduleJob('59 23 * * *', function (fireDate) { }).catch((err) => { addToLog("deleteOldEvents", "error", "Attempt to delete old event " + event.id + " failed with error: " + err); }); + + // TODO: While we're here, also remove all provisioned event attendees over a day + // old (they're not going to become active) }); // FRONTEND ROUTES @@ -377,7 +381,7 @@ router.get('/:eventID', (req, res) => { return el; }) .filter((obj, pos, arr) => { - return arr.map(mapObj => mapObj.id).indexOf(obj.id) === pos; + return obj.status === 'attending' && arr.map(mapObj => mapObj.id).indexOf(obj.id) === pos; }); let spotsRemaining, noMoreSpots; @@ -1081,10 +1085,11 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { } } }) + // Send update to all attendees if (sendEmails) { - Event.findOne({ id: req.params.eventID }).distinct('attendees.email', function (error, ids) { - let attendeeEmails = ids; - if (!error && attendeeEmails !== "") { + Event.findOne({ id: req.params.eventID }).then((event) => { + const attendeeEmails = event.attendees.filter(o => o.status === 'attending' && o.email).map(o => o.email); + if (attendeeEmails.length) { console.log("Sending emails to: " + attendeeEmails); req.app.get('hbsInstance').renderView('./views/emails/editevent.handlebars', { diffText, eventID: req.params.eventID, siteName, siteLogo, domain, cache: true, layout: 'email.handlebars' }, function (err, html) { const msg = { @@ -1269,9 +1274,9 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { }); // Send emails here otherwise they don't exist lol if (sendEmails) { - Event.findOne({ id: req.params.eventID }).distinct('attendees.email', function (error, ids) { - attendeeEmails = ids; - if (!error) { + Event.findOne({ id: req.params.eventID }).then((event) => { + const attendeeEmails = event.attendees.filter(o => o.status === 'attending' && o.email).map(o => o.email); + if (attendeeEmails.length) { console.log("Sending emails to: " + attendeeEmails); req.app.get('hbsInstance').renderView('./views/emails/deleteevent.handlebars', { siteName, siteLogo, domain, eventName: event.name, cache: true, layout: 'email.handlebars' }, function (err, html) { const msg = { @@ -1370,64 +1375,95 @@ router.post('/deleteeventgroup/:eventGroupID/:editToken', (req, res) => { .catch((err) => { res.send('Sorry! Something went wrong: ' + err); addToLog("deleteEventGroup", "error", "Attempt to delete event group " + req.params.eventGroupID + " failed with error: " + err); }); }); -router.post('/attendevent/:eventID', (req, res) => { +router.post('/attendee/provision', async (req, res) => { + const removalPassword = niceware.generatePassphrase(6).join('-'); const newAttendee = { - name: req.body.attendeeName, - status: 'attending', - email: req.body.attendeeEmail, - removalPassword: req.body.removeAttendancePassword + status: 'provisioned', + removalPassword, + created: Date.now(), }; - Event.findOne({ - id: req.params.eventID, - }, function (err, event) { - if (!event) return; - event.attendees.push(newAttendee); - event.save() - .then(() => { - addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID); - if (sendEmails) { - if (req.body.attendeeEmail) { - req.app.get('hbsInstance').renderView('./views/emails/addeventattendee.handlebars', { eventID: req.params.eventID, siteName, siteLogo, domain, removalPassword: req.body.removeAttendancePassword, cache: true, layout: 'email.handlebars' }, function (err, html) { - const msg = { - to: req.body.attendeeEmail, - from: { - name: siteName, - email: contactEmail, - }, - subject: `${siteName}: You're RSVPed to ${event.name}`, - 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; - } - }); + const event = await Event.findOne({ id: req.query.eventID }).catch(e => { + addToLog("provisionEventAttendee", "error", "Attempt to provision attendee in event " + req.query.eventID + " failed with error: " + e); + return res.sendStatus(500); + }); + + if (!event) { + return res.sendStatus(404); + } + + event.attendees.push(newAttendee); + await event.save().catch(e => { + console.log(e); + addToLog("provisionEventAttendee", "error", "Attempt to provision attendee in event " + req.query.eventID + " failed with error: " + e); + return res.sendStatus(500); + }); + addToLog("provisionEventAttendee", "success", "Attendee provisioned in event " + req.query.eventID); + + return res.json({ removalPassword }); +}); + +router.post('/attendevent/:eventID', async (req, res) => { + // Do not allow empty removal passwords + if (!req.body.removalPassword) { + return res.sendStatus(500); + } + + Event.findOneAndUpdate({ id: req.params.eventID, 'attendees.removalPassword': req.body.removalPassword }, { + "$set": { + "attendees.$.status": "attending", + "attendees.$.name": req.body.attendeeName, + "attendees.$.email": req.body.attendeeEmail, + } + }).then((event) => { + addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID); + if (sendEmails) { + if (req.body.attendeeEmail) { + req.app.get('hbsInstance').renderView('./views/emails/addeventattendee.handlebars', { eventID: req.params.eventID, siteName, siteLogo, domain, removalPassword: req.body.removalPassword, cache: true, layout: 'email.handlebars' }, function (err, html) { + const msg = { + to: req.body.attendeeEmail, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: You're RSVPed to ${event.name}`, + 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': '/' + req.params.eventID }); - res.end(); - }) - .catch((err) => { res.send('Database error, please try again :('); addToLog("addEventAttendee", "error", "Attempt to add attendee to event " + req.params.eventID + " failed with error: " + err); }); - }); + } + } + res.redirect(`/${req.params.eventID}`); + }) + .catch((error) => { + res.send('Database error, please try again :('); + addToLog("addEventAttendee", "error", "Attempt to add attendee to event " + req.params.eventID + " failed with error: " + err); + }); }); router.post('/unattendevent/:eventID', (req, res) => { + const removalPassword = req.body.removalPassword; + // Don't allow blank removal passwords! + if (!removalPassword) { + return res.sendStatus(500); + } + Event.update( { id: req.params.eventID }, - { $pull: { attendees: { removalPassword: req.body.removeAttendancePassword } } } + { $pull: { attendees: { removalPassword } } } ) .then(response => { console.log(response) @@ -1681,9 +1717,9 @@ router.post('/post/comment/:eventID', (req, res) => { } ap.broadcastCreateMessage(jsonObject, event.followers, req.params.eventID) if (sendEmails) { - Event.findOne({ id: req.params.eventID }).distinct('attendees.email', function (error, ids) { - let attendeeEmails = ids; - if (!error) { + Event.findOne({ id: req.params.eventID }).then((event) => { + const attendeeEmails = event.attendees.filter(o => o.status === 'attending' && o.email).map(o => o.email); + if (attendeeEmails.length) { console.log("Sending emails to: " + attendeeEmails); req.app.get('hbsInstance').renderView('./views/emails/addeventcomment.handlebars', { siteName, siteLogo, domain, eventID: req.params.eventID, commentAuthor: req.body.commentAuthor, cache: true, layout: 'email.handlebars' }, function (err, html) { const msg = { @@ -1755,9 +1791,9 @@ router.post('/post/reply/:eventID/:commentID', (req, res) => { } ap.broadcastCreateMessage(jsonObject, event.followers, req.params.eventID) if (sendEmails) { - Event.findOne({ id: req.params.eventID }).distinct('attendees.email', function (error, ids) { - let attendeeEmails = ids; - if (!error) { + Event.findOne({ id: req.params.eventID }).then((event) => { + const attendeeEmails = event.attendees.filter(o => o.status === 'attending' && o.email).map(o => o.email); + if (attendeeEmails.length) { console.log("Sending emails to: " + attendeeEmails); req.app.get('hbsInstance').renderView('./views/emails/addeventcomment.handlebars', { siteName, siteLogo, domain, eventID: req.params.eventID, commentAuthor: req.body.replyAuthor, cache: true, layout: 'email.handlebars' }, function (err, html) { const msg = { diff --git a/views/event.handlebars b/views/event.handlebars index 1a3fb17..9b5f3e2 100755 --- a/views/event.handlebars +++ b/views/event.handlebars @@ -131,7 +131,7 @@
Attendees {{#if eventAttendees}}({{eventAttendees.length}}){{/if}}
{{#unless noMoreSpots}} - + {{/unless}}
@@ -147,7 +147,7 @@ {{#if eventAttendees}} {{else}} @@ -181,9 +181,10 @@
- +

You will need this password if you want to remove yourself from the list of event attendees. If you provided your email, you'll receive it by email. Otherwise, write it down now because it will not be shown again.

- +