summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xmodels/Event.js9
-rw-r--r--package-lock.json49
-rw-r--r--package.json1
-rw-r--r--public/js/util.js3
-rwxr-xr-xroutes.js158
-rwxr-xr-xviews/event.handlebars37
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 @@
<h5 class="card-header">Attendees {{#if eventAttendees}}({{eventAttendees.length}}){{/if}}
<div class="btn-group" role="group" aria-label="Attendance controls">
{{#unless noMoreSpots}}
- <button type="button" id="attendEvent" class="btn btn-success" data-toggle="modal" data-target="#attendModal"><i class="fas fa-user-plus"></i> Add me</button>
+ <button type="button" id="attendEvent" class="btn btn-success" data-event-id="{{eventData.id}}"><i class="fas fa-user-plus"></i> Add me</button>
{{/unless}}
<button type="button" id="unattendEvent" class="btn btn-secondary" data-toggle="modal" data-target="#unattendModal"><i class="fas fa-user-times"></i> Remove me</button>
</div>
@@ -147,7 +147,7 @@
{{#if eventAttendees}}
<ul class="attendeesList">
{{#each eventAttendees}}
- <li{{#if ../editingEnabled}} data-attendee-name="{{this.name}}" data-attendee-id="{{this._id}}"{{/if}}>{{#if this.email}}<span class="attendee-name">{{this.name}}</span>{{else}}<a href="{{this.id}}"><span class="attendee-name">{{this.name}}</span></a>{{/if}}{{#if ../editingEnabled}} <a href="#" class="remove-attendee" data-toggle="modal" data-target="#removeAttendeeModal" title="Remove user from event"><i class="fas fa-user-times"></i></a>{{/if}}</li>
+ <li{{#if ../editingEnabled}} data-attendee-name="{{this.name}}" data-attendee-id="{{this._id}}"{{/if}}><span class="attendee-name">{{this.name}}</span>{{#if ../editingEnabled}} <a href="#" class="remove-attendee" data-toggle="modal" data-target="#removeAttendeeModal" title="Remove user from event"><i class="fas fa-user-times"></i></a>{{/if}}</li>
{{/each}}
</ul>
{{else}}
@@ -181,9 +181,10 @@
</div>
</div>
<div class="form-group">
- <label for="removeAttendancePassword">Deletion password</label>
+ <label for="removalPassword">Deletion password</label>
<p class="form-text small">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 <strong>not be shown again</strong>.</p>
- <input type="text" class="form-control" readonly id="removeAttendancePassword" name="removeAttendancePassword">
+ <input type="text" class="form-control" readonly id="removalPassword"
+ name="removalPassword">
</div>
</div>
<div class="modal-footer">
@@ -207,9 +208,10 @@
<form id="unattendEventForm" action="/unattendevent/{{eventData.id}}" method="post">
<div class="modal-body">
<div class="form-group">
- <label for="removeAttendancePassword" class="form-label">Your deletion password</label>
+ <label for="removalPassword" class="form-label">Your deletion password</label>
<p class="form-text small">Lost your password? Get in touch with the event organiser.</p>
- <input type="text" class="form-control" id="removeAttendancePassword" name="removeAttendancePassword">
+ <input type="text" class="form-control" id="removalPassword"
+ name="removalPassword">
</div>
</div>
<div class="modal-footer">
@@ -383,10 +385,11 @@
</div>
{{/if}}
-
+<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
{{#unless eventHasConcluded}}
+{{#if editingEnabled}}
<script type="text/javascript" src="/js/generate-timezones.js"></script>
-<script src='/js/niceware.js'></script>
+{{/if}}
{{/unless}}
<script>
$.validate({
@@ -509,8 +512,10 @@
$("#image-preview").css("background-image", "url('/events/{{eventData.image}}')");
$("#image-preview").css("background-size", "cover");
$("#image-preview").css("background-position", "center center");
+ {{#if editingEnabled}}
$('#eventStart').datepicker().data('datepicker').selectDate(moment('{{parsedStart}}', 'YYYYMMDD[T]HHmmss').toDate());
$('#eventEnd').datepicker().data('datepicker').selectDate(moment('{{parsedEnd}}', 'YYYYMMDD[T]HHmmss').toDate());
+ {{/if}}
new ClipboardJS('#copyEventLink');
autosize($('textarea'));
$("#exportICS").click(function(){
@@ -565,10 +570,18 @@
$("#eventGroupEditToken").removeAttr("data-validation").attr("data-validation-optional","true").val("");
}
});
- $('#attendModal').on('show.bs.modal', function (event) {
- var modal = $(this);
- const passphrase = window.niceware.generatePassphrase(6).join('-');
- modal.find('#removeAttendancePassword').val(passphrase);
+ $('#attendEvent').on('click', function(event) {
+ const modal = $('#attendModal');
+ const eventID = $(this).attr('data-event-id');
+ axios.post('/attendee/provision', {}, { params: { eventID }})
+ .then((response) => {
+ modal.find('#removalPassword').val(response.data.removalPassword);
+ modal.modal();
+ })
+ .catch((error) => {
+ console.error(error);
+ return false;
+ });
});
$('#verifyTokenForm').on('submit', function(e) {