From ab5cb69ed5c9af0c3ba0a8ae95ce2d61c8ed400c Mon Sep 17 00:00:00 2001 From: Richard Smith-Unna Date: Sat, 13 Feb 2016 18:39:56 +0000 Subject: [PATCH] Permissions handling for slides (towards #3) --- slidewinder/.meteor/packages | 1 + slidewinder/.meteor/versions | 2 + slidewinder/client/js/slides.js | 62 +++++--- slidewinder/client/js/slidewinder.js | 1 - slidewinder/lib/slides.js | 208 +++++++++++++++++++++++++++ slidewinder/server/slides.js | 13 ++ slidewinder/server/slidewinder.js | 49 ++----- 7 files changed, 280 insertions(+), 56 deletions(-) create mode 100644 slidewinder/lib/slides.js create mode 100644 slidewinder/server/slides.js diff --git a/slidewinder/.meteor/packages b/slidewinder/.meteor/packages index 4117d9b..da24d04 100644 --- a/slidewinder/.meteor/packages +++ b/slidewinder/.meteor/packages @@ -46,3 +46,4 @@ mquandalle:bower jparker:crypto-sha1 meteorhacks:picker easysearch:elasticsearch +aldeed:simple-schema diff --git a/slidewinder/.meteor/versions b/slidewinder/.meteor/versions index 505e779..c890286 100644 --- a/slidewinder/.meteor/versions +++ b/slidewinder/.meteor/versions @@ -6,6 +6,7 @@ accounts-password@1.1.4 accounts-ui@1.1.6 accounts-ui-unstyled@1.1.8 ahref:dragula@3.5.4 +aldeed:simple-schema@1.5.3 autoupdate@1.2.4 babel-compiler@5.8.24_1 babel-runtime@0.1.4 @@ -67,6 +68,7 @@ localstorage@1.0.5 logging@1.0.8 lyuyea:md-editor@0.4.0 materialize:materialize@0.97.5 +mdg:validation-error@0.2.0 meteor@1.1.10 meteor-base@1.0.1 meteorhacks:async@1.0.0 diff --git a/slidewinder/client/js/slides.js b/slidewinder/client/js/slides.js index 5db86f1..e748e74 100644 --- a/slidewinder/client/js/slides.js +++ b/slidewinder/client/js/slides.js @@ -2,13 +2,18 @@ Template.slides.helpers({ slidesIndex: function() { return SlideIndex; } }); - Template.slides.events({ 'click #new-slide-btn': function() { FlowRouter.go('/slides/create'); }, 'click .edit-slide-btn': function() { - FlowRouter.go('/slides/edit/' + this.__originalId); + // only the owner can edit a slide - also enforced in the router, + // and on the server + if (Meteor.userId() === this.owner) { + FlowRouter.go('/slides/edit/' + this.__originalId); + } else { + Materialize.toast("You can only edit slides that belong to you.", 4000, 'flash-err'); + } }, 'click .delete-slide-btn': function() { var div = $('
') @@ -26,21 +31,22 @@ Template.slides.events({ div.appendTo(card); }, 'click .fave-slide-btn': function() { - var filter = { _id: this.__originalId, }; - var update = {}; - if (!(_.isArray(this.faves))) { - this.faves = []; - update.$set = { faves: [Meteor.userId()] }; - } else if (_.include(this.faves, Meteor.userId())) { - update.$pull = { faves: Meteor.userId() }; - } else { - update.$push = { faves: Meteor.userId() }; - } - Slides.update(filter, update); + Slides.methods.toggleFave.call({ + slideId: this.__originalId + }, (err, res) => { + if (err) { + Materialize.toast(err, 4000, 'flash-err'); + } + }); }, 'click .confirm-slide-del-btn': function() { - var filter = { _id: this.__originalId, }; - Slides.remove(filter); + Slides.methods.removeSlide.call({ + slideId: this.__originalId + }, (err, res) => { + if (err) { + Materialize.toast(err, 4000, 'flash-err'); + } + }); }, 'click .cancel-slide-del-btn': function() { $('#' + this.__originalId).find('.confirm-delete').remove(); @@ -87,7 +93,6 @@ Template.slide_image_card.helpers({ } }) - Template.create_slide_sidebar.helpers({ templates: function() { return templates; @@ -206,14 +211,25 @@ Template.create_slide.events({ if ($('a.fa.fa-eye').hasClass('active')) { var slidedata = getSlideData(); var author = Meteor.user().profile.name; - Meteor.call('renderSlide', author, slidedata, showSlidePreview); + Slides.methods.renderSlide.call( + { author: author, slidedata: slidedata }, + showSlidePreview + ); } return false; }, 'click #save_slide_btn': function(e) { var slidedata = getSlideData(); - Meteor.call('saveSlide', slidedata); - FlowRouter.go('/slides'); + Slides.methods.saveSlide.call({ + slidedata: slidedata + }, (err, res) => { + if (err) { + Materialize.toast(err, 4000, 'flash-err'); + setTimeout(function(){ FlowRouter.go('/slides') }, 2000); + } else { + FlowRouter.go('/slides'); + } + }); } }) @@ -238,6 +254,7 @@ Template.edit_slide.onRendered(function() { // load the card for editing var slide_id = FlowRouter.getParam("slideId"); var res = Slides.find(slide_id); + if (res.count() == 0) { Materialize.toast("The slide you're trying to edit doesn't exist :(", 4000, 'flash-err'); setTimeout(function(){ FlowRouter.go('/slides') }, 2000); @@ -245,6 +262,13 @@ Template.edit_slide.onRendered(function() { } var slide = res.fetch()[0]; + + if (Meteor.userId() != slide.owner) { + Materialize.toast("You can only edit slides that belong to you.", 4000, 'flash-err'); + setTimeout(function(){ FlowRouter.go('/slides') }, 2000); + return; + } + setSlideData(slide); }); diff --git a/slidewinder/client/js/slidewinder.js b/slidewinder/client/js/slidewinder.js index a9cb7d7..4369378 100644 --- a/slidewinder/client/js/slidewinder.js +++ b/slidewinder/client/js/slidewinder.js @@ -17,7 +17,6 @@ Deps.autorun(function(){ Session.set('pagetitle', 'slidewinder') -Slides = new Mongo.Collection('slides'); Decks = new Mongo.Collection('decks'); Tracker.autorun(function () { diff --git a/slidewinder/lib/slides.js b/slidewinder/lib/slides.js new file mode 100644 index 0000000..c10ebbd --- /dev/null +++ b/slidewinder/lib/slides.js @@ -0,0 +1,208 @@ +Slides = new Mongo.Collection('slides'); + +var slidewinder = {} + +if (Meteor.isServer) { + slidewinder = Meteor.npmRequire('slidewinder'); +} else { + slidewinder = { + slide: function(a) { + return {}; + } + } +} + +// Deny all client-side updates on all collections +Slides.deny({ + insert() { return true; }, + update() { return true; }, + remove() { return true; }, +}); + +// Define a namespace for Methods related to the Slides collection +// Allows overriding for tests by replacing the implementation (2) +Slides.methods = {}; + +// Render a slide as a preview. Given a username and a slide data object, +// create a full slide and use it to populate a dummy deck. Finally, +// render the dummy deck and pass the rendered HTML back to the client. +Slides.methods.renderSlide = { + name: 'Slides.methods.renderSlide', + + validate(args) { + return true; + }, + + run({ username, slidedata }) { + if (Meteor.isServer) { + // create a full slide object from the slide data + var s = new slidewinder.slide(slidedata); + // and a dummy deck to render it in + var d = new slidewinder.deck(null, { + author: username, + title: 'slide preview', + tags: [] + }, 'remark'); + + // store the rendered deck HTML + html = '' + d.preprocess.call(d, [s], function() { + d.render.call(d, function(){ + html = d.renderedDeck; + }); + }); + + // pass it back to the client + return html; + } + }, + + // this is part of the Meteor Methods advanced boilerplate + // see http://guide.meteor.com/methods.html#advanced-boilerplate + call(args, callback) { + const options = { + returnStubValue: true, + throwStubExceptions: true + } + + Meteor.apply(this.name, [args], options, callback); + } +}; + +// Save a slide that has either been newly created or edited. If this is +// an edit, we only proceed if the user is the owner of the slide. +Slides.methods.saveSlide = { + name: 'Slides.methods.saveSlide', + + validate(args) { + return true; + }, + + run({ slidedata }) { + if (Meteor.isServer) { + + // create a full slide object from the data + // this takes care of basic housekeeping like ensuring the slide + // has an ID + var s = new slidewinder.slide(slidedata); + + // because some slides are public, we need to make sure that if this + // is an edit operation, the user trying to edit is the owner. + var res = Slides.find(s._id); + if (res.count() > 0) { + var isOwner = res.fetch()[0].owner == this.userId; + if (!isOwner) { + throw new Meteor.Error( + 'Slides.methods.saveSlide.unauthorized', + "Cannot edit slides that don't belong to you" + ); + } + } + + // update the Slides collection with this slide. We use the + // upsert operation to handle both edits and insertions in a single + // command. + Slides.update({ _id: s._id }, s, { upsert: true }); + + } + + return slidedata.owner === this.userId; + }, + + // this is part of the Meteor Methods advanced boilerplate + // see http://guide.meteor.com/methods.html#advanced-boilerplate + call(args, callback) { + const options = { + returnStubValue: true, + throwStubExceptions: true + } + + Meteor.apply(this.name, [args], options, callback); + } +}; + +// Remove a slide, only if it belongs to this user +Slides.methods.removeSlide = { + name: 'Slides.methods.removeSlide', + + validate(args) { + new SimpleSchema({ + slideId: { type: String } + }).validate(args) + }, + + run({ slideId }) { + + Slides.remove({ _id: slideId, owner: this.userId }); + + }, + + // this is part of the Meteor Methods advanced boilerplate + // see http://guide.meteor.com/methods.html#advanced-boilerplate + call(args, callback) { + const options = { + returnStubValue: true, + throwStubExceptions: true + } + + Meteor.apply(this.name, [args], options, callback); + } +}; + +// Toggle whether a given slide is favourited by this user +Slides.methods.toggleFave = { + name: 'Slides.methods.toggleFave', + + validate(args) { + new SimpleSchema({ + slideId: { type: String } + }).validate(args) + }, + + run({ slideId }) { + + var slide = Slides.findOne(slideId); + var update = {}; + if (!(_.isArray(slide.faves))) { + slide.faves = []; + update.$set = { faves: [this.userId] }; + } else if (_.include(slide.faves, this.userId)) { + update.$pull = { faves: this.userId }; + } else { + update.$push = { faves: this.userId }; + } + + Slides.update({ _id: slideId }, update); + }, + + // this is part of the Meteor Methods advanced boilerplate + // see http://guide.meteor.com/methods.html#advanced-boilerplate + call(args, callback) { + const options = { + returnStubValue: true, + throwStubExceptions: true + } + + Meteor.apply(this.name, [args], options, callback); + } +}; + +// Register the methods with Meteor's DDP system +Meteor.methods({ + [Slides.methods.renderSlide.name]: function (args) { + Slides.methods.renderSlide.validate.call(this, args); + Slides.methods.renderSlide.run.call(this, args); + }, + [Slides.methods.saveSlide.name]: function (args) { + Slides.methods.saveSlide.validate.call(this, args); + Slides.methods.saveSlide.run.call(this, args); + }, + [Slides.methods.removeSlide.name]: function (args) { + Slides.methods.removeSlide.validate.call(this, args); + Slides.methods.removeSlide.run.call(this, args); + }, + [Slides.methods.toggleFave.name]: function (args) { + Slides.methods.toggleFave.validate.call(this, args); + Slides.methods.toggleFave.run.call(this, args); + } +}); diff --git a/slidewinder/server/slides.js b/slidewinder/server/slides.js new file mode 100644 index 0000000..7dc5c05 --- /dev/null +++ b/slidewinder/server/slides.js @@ -0,0 +1,13 @@ +// Publish the collection, optionally giving access to all public slides +// or only the user's own slides. +Meteor.publish("slides", function (everyone) { + var cond = [ + { owner: this.userId } + ] + if (everyone) { + cond.push({ private: false }); + } + return Slides.find({ + $or: cond + }); +}); diff --git a/slidewinder/server/slidewinder.js b/slidewinder/server/slidewinder.js index 8d42891..4b11619 100644 --- a/slidewinder/server/slidewinder.js +++ b/slidewinder/server/slidewinder.js @@ -1,22 +1,7 @@ -var slidewinder = Meteor.npmRequire('slidewinder'); -Slides = new Mongo.Collection('slides'); Decks = new Mongo.Collection('decks'); Presentations = new Mongo.Collection('presentations'); -Meteor.publish("slides", function (everyone) { - var cond = [ - { owner: this.userId } - ] - if (everyone) { - cond.push({ private: false }); - } - console.log('Slide subscription:', cond); - return Slides.find({ - $or: cond - }); -}); - Meteor.publish("decks", function (everyone) { var cond = [ { owner: this.userId } @@ -24,34 +9,26 @@ Meteor.publish("decks", function (everyone) { if (everyone) { cond.push({ private: false }); } - console.log('Deck subscription:', cond); return Decks.find({ $or: cond }); }); +// Deny all client-side updates on all collections +Decks.deny({ + insert() { return true; }, + update() { return true; }, + remove() { return true; }, +}); +Presentations.deny({ + insert() { return true; }, + update() { return true; }, + remove() { return true; }, +}); + Meteor.methods({ - renderSlide: function(username, slidedata) { - var s = new slidewinder.slide(slidedata); - var d = new slidewinder.deck(null, { - author: username, - title: 'slide preview', - tags: [] - }, 'remark'); - html = '' - d.preprocess.call(d, [s], function() { - d.render.call(d, function(){ - html = d.renderedDeck; - }); - }); - return html; - }, - saveSlide: function(slidedata) { - var s = new slidewinder.slide(slidedata); - Slides.update({ _id: s._id }, s, { upsert: true }); - }, saveDeck: function(deckdata) { - Decks.insert(deckdata); + Decks.update({ _id: deckdata._id }, deckdata, { upsert: true }); }, renderDeck: function(deckdata) { var query = deckdata.slides.map(function(s) { return { _id: s }; });