From f20b5d04f5e095de3348251ba669c5b13789298f Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 9 Dec 2015 00:35:45 +0100 Subject: [PATCH 01/10] export board to Wekan JSON --- .meteor/packages | 1 + .meteor/versions | 1 + client/components/boards/boardHeader.jade | 1 + client/components/boards/boardHeader.js | 6 +++ i18n/en.i18n.json | 1 + models/export.js | 51 +++++++++++++++++++++++ 6 files changed, 61 insertions(+) create mode 100644 models/export.js diff --git a/.meteor/packages b/.meteor/packages index 98c06cc9d..a868ec753 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -73,3 +73,4 @@ perak:markdown seriousm:emoji-continued templates:tabs verron:autosize +simple:json-routes diff --git a/.meteor/versions b/.meteor/versions index 9d7fe1b3a..61df2c729 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -125,6 +125,7 @@ seriousm:emoji-continued@1.4.0 service-configuration@1.0.5 session@1.1.1 sha@1.0.4 +simple:json-routes@1.0.4 softwarerero:accounts-t9n@1.1.7 spacebars@1.0.7 spacebars-compiler@1.0.7 diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index a0160382e..3e608d4a2 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -56,6 +56,7 @@ template(name="boardMenuPopup") if currentUser.isBoardAdmin hr ul.pop-over-list + li: a.js-export-board(href="{{urlExport}}", download) {{_ 'export-board'}} li: a.js-archive-board {{_ 'archive-board'}} template(name="boardVisibilityList") diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index 3dc6d7541..3503cbfb7 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -15,6 +15,12 @@ Template.boardMenuPopup.events({ }), }); +Template.boardMenuPopup.helpers({ + urlExport() { + return Meteor.absoluteUrl(`api/b/${Session.get('currentBoard')}`); + }, +}); + Template.boardChangeTitlePopup.events({ submit(evt, tpl) { const newTitle = tpl.$('.js-board-name').val().trim(); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 04c0959f7..238f99642 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -147,6 +147,7 @@ "error-user-doesNotExist": "This user does not exist", "error-user-notAllowSelf": "This action on self is not allowed", "error-user-notCreated": "This user is not created", + "export-board": "Export board", "filter": "Filter", "filter-cards": "Filter Cards", "filter-clear": "Clear filter", diff --git a/models/export.js b/models/export.js new file mode 100644 index 000000000..bc7cb8f94 --- /dev/null +++ b/models/export.js @@ -0,0 +1,51 @@ +/* global JsonRoutes */ +JsonRoutes.add('get', '/api/b/:id', function (req, res) { + const id = req.params.id; + const exporter = new Exporter(id); + JsonRoutes.sendResult(res, 200, exporter.build()); +}); + +class Exporter { + constructor(boardId) { + this._boardId = boardId; + } + + build() { + const byBoard = {boardId: this._boardId}; + const fields = {fields: {boardId: 0}}; + const result = Boards.findOne(this._boardId); + result.lists = Lists.find(byBoard, fields).fetch(); + result.cards = Cards.find(byBoard, fields).fetch(); + result.comments = CardComments.find(byBoard, fields).fetch(); + result.activities = Activities.find(byBoard, fields).fetch(); + + // we also have to export some user data - as the other elements only include id + // but we have to be careful: + // 1- only exports users that are linked somehow to that board + // 2- do not export any sensitive information + const users = {}; + result.members.forEach((member) => {users[member.userId] = true;}); + result.lists.forEach((list) => {users[list.userId] = true;}); + result.cards.forEach((card) => { + users[card.userId] = true; + if (card.members) { + card.members.forEach((memberId) => {users[memberId] = true;}); + } + }); + result.comments.forEach((comment) => {users[comment.userId] = true;}); + result.activities.forEach((activity) => {users[activity.userId] = true;}); + const byUserIds = {_id: {$in: Object.getOwnPropertyNames(users)}}; + // we use whitelist to be sure we do not expose inadvertently + // some secret fields that gets added to User later. + const userFields = {fields: { + _id: 1, + username: 1, + 'profile.fullname': 1, + 'profile.initials': 1, + 'profile.avatarUrl': 1, + }}; + result.users = Users.find(byUserIds, userFields).fetch(); + + return result; + } +} From 7cfc72da995a247b77d24dca215e59af2f5ed5f0 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Fri, 11 Dec 2015 19:20:19 +0100 Subject: [PATCH 02/10] export works but no authentication --- models/export.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/models/export.js b/models/export.js index bc7cb8f94..7be979869 100644 --- a/models/export.js +++ b/models/export.js @@ -1,8 +1,26 @@ /* global JsonRoutes */ -JsonRoutes.add('get', '/api/b/:id', function (req, res) { - const id = req.params.id; - const exporter = new Exporter(id); - JsonRoutes.sendResult(res, 200, exporter.build()); +if(Meteor.isServer) { + console.log(`userId is ${this.userId}`); + JsonRoutes.add('get', '/api/b/:id', function (req, res) { + const id = req.params.id; + const board = Boards.findOne(id); + //if(Meteor.userId() && allowIsBoardMember(Meteor.userId(), board)) { + const exporter = new Exporter(id); + JsonRoutes.sendResult(res, 200, exporter.build()); + //} else { + // // 403 = forbidden + // JsonRoutes.sendError(res, 403); + //} + }); +} + +Meteor.methods({ + exportBoard(boardId) { + const board = Boards.findOne(boardId); +// //if(Meteor.userId() && allowIsBoardMember(Meteor.userId(), board)) { + const exporter = new Exporter(boardId); + return exporter.build(); + } }); class Exporter { @@ -45,7 +63,7 @@ class Exporter { 'profile.avatarUrl': 1, }}; result.users = Users.find(byUserIds, userFields).fetch(); - + //return JSON.stringify(result); return result; } } From 18697d45f652a119ba21b0cef42fbf732902bfa9 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Sun, 13 Dec 2015 20:02:34 +0100 Subject: [PATCH 03/10] board export now checks authentication --- .meteor/packages | 2 +- .meteor/versions | 2 +- client/components/boards/boardHeader.jade | 2 +- client/components/boards/boardHeader.js | 14 ++++++++++++ models/boards.js | 27 +++++++++++++++++++++++ models/export.js | 26 +++++++--------------- 6 files changed, 52 insertions(+), 21 deletions(-) diff --git a/.meteor/packages b/.meteor/packages index a868ec753..7de4e0631 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -73,4 +73,4 @@ perak:markdown seriousm:emoji-continued templates:tabs verron:autosize -simple:json-routes +pfafman:filesaver diff --git a/.meteor/versions b/.meteor/versions index 61df2c729..36029fcfd 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -110,6 +110,7 @@ peerlibrary:blaze-components@0.15.1 peerlibrary:computed-field@0.3.1 peerlibrary:reactive-field@0.1.0 perak:markdown@1.0.5 +pfafman:filesaver@0.2.2 promise@0.5.1 raix:eventemitter@0.1.3 raix:handlebar-helpers@0.2.5 @@ -125,7 +126,6 @@ seriousm:emoji-continued@1.4.0 service-configuration@1.0.5 session@1.1.1 sha@1.0.4 -simple:json-routes@1.0.4 softwarerero:accounts-t9n@1.1.7 spacebars@1.0.7 spacebars-compiler@1.0.7 diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 3e608d4a2..f264a6d98 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -56,7 +56,7 @@ template(name="boardMenuPopup") if currentUser.isBoardAdmin hr ul.pop-over-list - li: a.js-export-board(href="{{urlExport}}", download) {{_ 'export-board'}} + li: a.js-export-board {{_ 'export-board'}} li: a.js-archive-board {{_ 'archive-board'}} template(name="boardVisibilityList") diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index 3503cbfb7..34204a469 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -13,6 +13,20 @@ Template.boardMenuPopup.events({ // confirm that the board was successfully archived. FlowRouter.go('home'); }), + 'click .js-export-board'() { + const boardId = Session.get('currentBoard'); + Meteor.call('exportBoard', boardId, (error, response) => { + if(error) { + // the only error we can anticipate is accessing a non-authorized board + // and this should have been caugh by UI before. + // So no treatment here for the time being. + } else { + const dataToSave = new Blob([JSON.stringify(response)], {type: 'application/json;charset=utf-8'}); + const filename = `wekan-export-board-${boardId}.json`; + saveAs(dataToSave, filename); + } + }); + } }); Template.boardMenuPopup.helpers({ diff --git a/models/boards.js b/models/boards.js index 6aba0b1ef..b3d5b0d0a 100644 --- a/models/boards.js +++ b/models/boards.js @@ -79,6 +79,33 @@ Boards.attachSchema(new SimpleSchema({ Boards.helpers({ + /** + * Is current logged-in user authorized to view this board? + */ + isVisibleByUser() { + if(this.isPublic()) { + // public boards are visible to everyone + return true; + } else { + // otherwise you have to be logged-in and active member + return this.isActiveMember(Meteor.userId()); + } + }, + + /** + * Is the user one of the active members of the board? + * + * @param userId + * @returns {boolean} the member that matches, or undefined/false + */ + isActiveMember(userId) { + if(userId) { + return this.members.find((member) => (member.userId === userId && member.isActive)); + } else { + return false; + } + }, + isPublic() { return this.permission === 'public'; }, diff --git a/models/export.js b/models/export.js index 7be979869..20b1186ab 100644 --- a/models/export.js +++ b/models/export.js @@ -1,25 +1,15 @@ -/* global JsonRoutes */ -if(Meteor.isServer) { - console.log(`userId is ${this.userId}`); - JsonRoutes.add('get', '/api/b/:id', function (req, res) { - const id = req.params.id; - const board = Boards.findOne(id); - //if(Meteor.userId() && allowIsBoardMember(Meteor.userId(), board)) { - const exporter = new Exporter(id); - JsonRoutes.sendResult(res, 200, exporter.build()); - //} else { - // // 403 = forbidden - // JsonRoutes.sendError(res, 403); - //} - }); -} + Meteor.methods({ exportBoard(boardId) { + check(boardId, String); const board = Boards.findOne(boardId); -// //if(Meteor.userId() && allowIsBoardMember(Meteor.userId(), board)) { - const exporter = new Exporter(boardId); - return exporter.build(); + if(board.isVisibleByUser()) { + const exporter = new Exporter(boardId); + return exporter.build(); + } else { + throw new Meteor.Error('error-board-notAMember'); + } } }); From f5eba16a50056c20d86044f7fa025bf21eef1264 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Sun, 13 Dec 2015 21:38:22 +0100 Subject: [PATCH 04/10] fix eslint --- client/components/boards/boardHeader.js | 3 ++- models/export.js | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index b5a31754e..b5bb0dbba 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -1,3 +1,4 @@ +/* global saveAs */ Template.boardMenuPopup.events({ 'click .js-rename-board': Popup.open('boardChangeTitle'), 'click .js-open-archives'() { @@ -26,7 +27,7 @@ Template.boardMenuPopup.events({ saveAs(dataToSave, filename); } }); - } + }, }); Template.boardMenuPopup.helpers({ diff --git a/models/export.js b/models/export.js index 20b1186ab..efff76423 100644 --- a/models/export.js +++ b/models/export.js @@ -10,7 +10,7 @@ Meteor.methods({ } else { throw new Meteor.Error('error-board-notAMember'); } - } + }, }); class Exporter { @@ -53,7 +53,6 @@ class Exporter { 'profile.avatarUrl': 1, }}; result.users = Users.find(byUserIds, userFields).fetch(); - //return JSON.stringify(result); return result; } } From efe7c21d579a0cffe682741d2daf832062001a3a Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 16 Dec 2015 16:30:48 +0100 Subject: [PATCH 05/10] Export wekan: do not export board.stars --- models/export.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/models/export.js b/models/export.js index efff76423..aab81c64c 100644 --- a/models/export.js +++ b/models/export.js @@ -20,12 +20,13 @@ class Exporter { build() { const byBoard = {boardId: this._boardId}; - const fields = {fields: {boardId: 0}}; - const result = Boards.findOne(this._boardId); - result.lists = Lists.find(byBoard, fields).fetch(); - result.cards = Cards.find(byBoard, fields).fetch(); - result.comments = CardComments.find(byBoard, fields).fetch(); - result.activities = Activities.find(byBoard, fields).fetch(); + // we do not want to retrieve boardId in related elements + const noBoardId = {fields: {boardId: 0}}; + const result = Boards.findOne(this._boardId, {fields: {stars: 0}}); + result.lists = Lists.find(byBoard, noBoardId).fetch(); + result.cards = Cards.find(byBoard, noBoardId).fetch(); + result.comments = CardComments.find(byBoard, noBoardId).fetch(); + result.activities = Activities.find(byBoard, noBoardId).fetch(); // we also have to export some user data - as the other elements only include id // but we have to be careful: From d08e1cc45b7f894f360f3a8a89e235ccc47b8f96 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 16 Dec 2015 21:54:35 +0100 Subject: [PATCH 06/10] Export Wekan now server-based with proper auth --- .meteor/packages | 2 +- .meteor/versions | 2 +- client/components/boards/boardHeader.jade | 2 +- client/components/boards/boardHeader.js | 26 +++++++------------- models/boards.js | 6 ++--- models/export.js | 30 ++++++++++++++++++++--- 6 files changed, 42 insertions(+), 26 deletions(-) diff --git a/.meteor/packages b/.meteor/packages index c9d810827..ad6ddf0b3 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -73,4 +73,4 @@ perak:markdown seriousm:emoji-continued templates:tabs verron:autosize -pfafman:filesaver +simple:json-routes diff --git a/.meteor/versions b/.meteor/versions index 6c0486b6d..65c43d860 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -111,7 +111,6 @@ peerlibrary:blaze-components@0.15.1 peerlibrary:computed-field@0.3.1 peerlibrary:reactive-field@0.1.0 perak:markdown@1.0.5 -pfafman:filesaver@0.2.2 promise@0.5.1 raix:eventemitter@0.1.3 raix:handlebar-helpers@0.2.5 @@ -126,6 +125,7 @@ seriousm:emoji-continued@1.4.0 service-configuration@1.0.5 session@1.1.1 sha@1.0.4 +simple:json-routes@1.0.4 softwarerero:accounts-t9n@1.1.7 spacebars@1.0.7 spacebars-compiler@1.0.7 diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index eb7ca984e..a743311bd 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -56,7 +56,7 @@ template(name="boardMenuPopup") if currentUser.isBoardAdmin hr ul.pop-over-list - li: a.js-export-board {{_ 'export-board'}} + li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}} li: a.js-archive-board {{_ 'archive-board'}} template(name="boardVisibilityList") diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index b5bb0dbba..0c9b57945 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -1,4 +1,3 @@ -/* global saveAs */ Template.boardMenuPopup.events({ 'click .js-rename-board': Popup.open('boardChangeTitle'), 'click .js-open-archives'() { @@ -14,25 +13,18 @@ Template.boardMenuPopup.events({ // confirm that the board was successfully archived. FlowRouter.go('home'); }), - 'click .js-export-board'() { - const boardId = Session.get('currentBoard'); - Meteor.call('exportBoard', boardId, (error, response) => { - if(error) { - // the only error we can anticipate is accessing a non-authorized board - // and this should have been caugh by UI before. - // So no treatment here for the time being. - } else { - const dataToSave = new Blob([JSON.stringify(response)], {type: 'application/json;charset=utf-8'}); - const filename = `wekan-export-board-${boardId}.json`; - saveAs(dataToSave, filename); - } - }); - }, }); Template.boardMenuPopup.helpers({ - urlExport() { - return Meteor.absoluteUrl(`api/b/${Session.get('currentBoard')}`); + exportUrl() { + const boardId = Session.get('currentBoard'); + const userId = Meteor.userId(); + const loginToken = Accounts._storedLoginToken(); + return Meteor.absoluteUrl(`api/b/${boardId}/${userId}/${loginToken}`); + }, + exportFilename() { + const boardId = Session.get('currentBoard'); + return `wekan-export-board-${boardId}.json`; }, }); diff --git a/models/boards.js b/models/boards.js index cdf83ce0f..d5363f4e7 100644 --- a/models/boards.js +++ b/models/boards.js @@ -80,15 +80,15 @@ Boards.attachSchema(new SimpleSchema({ Boards.helpers({ /** - * Is current logged-in user authorized to view this board? + * Is supplied user authorized to view this board? */ - isVisibleByUser() { + isVisibleBy(user) { if(this.isPublic()) { // public boards are visible to everyone return true; } else { // otherwise you have to be logged-in and active member - return this.isActiveMember(Meteor.userId()); + return this.isActiveMember(user._id); } }, diff --git a/models/export.js b/models/export.js index aab81c64c..8d1be64ed 100644 --- a/models/export.js +++ b/models/export.js @@ -1,11 +1,30 @@ +/* global JsonRoutes */ +if(Meteor.isServer) { + JsonRoutes.add('get', '/api/b/:boardId/:userId/:loginToken', function (req, res) { + const { userId, loginToken, boardId } = req.params; + const hashToken = Accounts._hashLoginToken(loginToken); + const user = Meteor.users.findOne({ + _id: userId, + 'services.resume.loginTokens.hashedToken': hashToken, + }); + + const exporter = new Exporter(boardId); + if(user && exporter.canExport(user)) { + JsonRoutes.sendResult(res, 200, exporter.build()); + } else { + // we could send an explicit error message, but on the other + // hand the only way to get there is by hacking the UI so... + JsonRoutes.sendResult(res, 403); + } + }); +} Meteor.methods({ exportBoard(boardId) { check(boardId, String); - const board = Boards.findOne(boardId); - if(board.isVisibleByUser()) { - const exporter = new Exporter(boardId); + const exporter = new Exporter(boardId); + if(exporter.canExport(Meteor.user())) { return exporter.build(); } else { throw new Meteor.Error('error-board-notAMember'); @@ -56,4 +75,9 @@ class Exporter { result.users = Users.find(byUserIds, userFields).fetch(); return result; } + + canExport(user) { + const board = Boards.findOne(this._boardId); + return board && board.isVisibleBy(user); + } } From 3a52d7d7af838037b6e933203ffb661ff2e13b5e Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Thu, 17 Dec 2015 10:22:48 +0100 Subject: [PATCH 07/10] Export: add _format field --- models/export.js | 1 + 1 file changed, 1 insertion(+) diff --git a/models/export.js b/models/export.js index 8d1be64ed..c20053ad3 100644 --- a/models/export.js +++ b/models/export.js @@ -42,6 +42,7 @@ class Exporter { // we do not want to retrieve boardId in related elements const noBoardId = {fields: {boardId: 0}}; const result = Boards.findOne(this._boardId, {fields: {stars: 0}}); + result._format = 'wekan-board-1.0.0'; result.lists = Lists.find(byBoard, noBoardId).fetch(); result.cards = Cards.find(byBoard, noBoardId).fetch(); result.comments = CardComments.find(byBoard, noBoardId).fetch(); From 4cea6fca908b4f9acd8687293041ebee86284883 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Thu, 17 Dec 2015 11:58:55 +0100 Subject: [PATCH 08/10] Export: include attachments --- models/export.js | 33 +++++++++++++++++---------------- server/lib/utils.js | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/models/export.js b/models/export.js index c20053ad3..9fbcbcef2 100644 --- a/models/export.js +++ b/models/export.js @@ -19,19 +19,6 @@ if(Meteor.isServer) { }); } - -Meteor.methods({ - exportBoard(boardId) { - check(boardId, String); - const exporter = new Exporter(boardId); - if(exporter.canExport(Meteor.user())) { - return exporter.build(); - } else { - throw new Meteor.Error('error-board-notAMember'); - } - }, -}); - class Exporter { constructor(boardId) { this._boardId = boardId; @@ -41,12 +28,20 @@ class Exporter { const byBoard = {boardId: this._boardId}; // we do not want to retrieve boardId in related elements const noBoardId = {fields: {boardId: 0}}; - const result = Boards.findOne(this._boardId, {fields: {stars: 0}}); - result._format = 'wekan-board-1.0.0'; + const result = { + _format: 'wekan-board-1.0.0', + }; + _.extend(result, Boards.findOne(this._boardId, {fields: {stars: 0}})); result.lists = Lists.find(byBoard, noBoardId).fetch(); result.cards = Cards.find(byBoard, noBoardId).fetch(); result.comments = CardComments.find(byBoard, noBoardId).fetch(); result.activities = Activities.find(byBoard, noBoardId).fetch(); + // for attachments we only export IDs and absolute url to original doc + result.attachments = Attachments.find(byBoard).fetch().map((attachment) => { return { + _id: attachment._id, + cardId: attachment.cardId, + url: Meteor.absoluteUrl(Utils.stripLeadingSlash(attachment.url())), + };}); // we also have to export some user data - as the other elements only include id // but we have to be careful: @@ -73,7 +68,13 @@ class Exporter { 'profile.initials': 1, 'profile.avatarUrl': 1, }}; - result.users = Users.find(byUserIds, userFields).fetch(); + result.users = Users.find(byUserIds, userFields).fetch().map((user) => { + // user avatar is stored as a relative url, we export absolute + if(user.profile.avatarUrl) { + user.profile.avatarUrl = Meteor.absoluteUrl(Utils.stripLeadingSlash(user.profile.avatarUrl)); + } + return user; + }); return result; } diff --git a/server/lib/utils.js b/server/lib/utils.js index b59671fba..a6a84f90d 100644 --- a/server/lib/utils.js +++ b/server/lib/utils.js @@ -5,3 +5,24 @@ allowIsBoardAdmin = function(userId, board) { allowIsBoardMember = function(userId, board) { return board && board.hasMember(userId); }; + +// todo XXX not really server-specific, +// so move it to a common (client+server) lib? +Utils = { + /** + * If text starts with a / will remove it. + * @param text + */ + stripLeadingSlash(text) { + // we need an actual text string + if (!text) { + return text; + } + // if starting with slash + if (text[0] === '/') { + return text.slice(1); + } + // otherwise leave untouched + return text; + }, +}; From a45a899137b23f95c2f58025e3e06109bdf82a94 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Thu, 17 Dec 2015 13:11:33 +0100 Subject: [PATCH 09/10] Improved doc on server-side export route --- models/export.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/models/export.js b/models/export.js index 9fbcbcef2..e250d9350 100644 --- a/models/export.js +++ b/models/export.js @@ -1,5 +1,15 @@ /* global JsonRoutes */ if(Meteor.isServer) { + // todo XXX once we have a real API in place, move that route there + /* + * This route is used to export the board FROM THE APPLICATION. + * We want to identify the logged-in user without asking for password again, + * but the server-side API routing has no notion of "current user". + * So we have to pass login information (id + token) to authenticate. + * + * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/ + * for detailed explanations + */ JsonRoutes.add('get', '/api/b/:boardId/:userId/:loginToken', function (req, res) { const { userId, loginToken, boardId } = req.params; const hashToken = Accounts._hashLoginToken(loginToken); From 115ea533f621dcc51543f131f05ea9276e9aabc5 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Thu, 17 Dec 2015 23:57:28 +0100 Subject: [PATCH 10/10] Export: improved API routes - use an explicit "boards" domain: /api/boards/:boardId - pass authToken as a request parameter: /api/boards/:boardId?authToken=:token - in the future, same route can be used with authToken set in the Authenticate: header easily --- client/components/boards/boardHeader.js | 3 +-- models/boards.js | 2 +- models/export.js | 33 +++++++++++++++---------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index 0c9b57945..2bf68a41e 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -18,9 +18,8 @@ Template.boardMenuPopup.events({ Template.boardMenuPopup.helpers({ exportUrl() { const boardId = Session.get('currentBoard'); - const userId = Meteor.userId(); const loginToken = Accounts._storedLoginToken(); - return Meteor.absoluteUrl(`api/b/${boardId}/${userId}/${loginToken}`); + return Meteor.absoluteUrl(`api/boards/${boardId}?authToken=${loginToken}`); }, exportFilename() { const boardId = Session.get('currentBoard'); diff --git a/models/boards.js b/models/boards.js index d5363f4e7..e20ca8ce1 100644 --- a/models/boards.js +++ b/models/boards.js @@ -88,7 +88,7 @@ Boards.helpers({ return true; } else { // otherwise you have to be logged-in and active member - return this.isActiveMember(user._id); + return user && this.isActiveMember(user._id); } }, diff --git a/models/export.js b/models/export.js index e250d9350..3d8ee99e4 100644 --- a/models/export.js +++ b/models/export.js @@ -1,29 +1,36 @@ /* global JsonRoutes */ if(Meteor.isServer) { // todo XXX once we have a real API in place, move that route there + // todo XXX also share the route definition between the client and the server + // so that we could use something like ApiRoutes.path('boards/export', boardId) + // on the client instead of copy/pasting the route path manually between the client and the server. /* * This route is used to export the board FROM THE APPLICATION. - * We want to identify the logged-in user without asking for password again, - * but the server-side API routing has no notion of "current user". - * So we have to pass login information (id + token) to authenticate. + * If user is already logged-in, pass loginToken as param "authToken": + * '/api/boards/:boardId?authToken=:token' * * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/ * for detailed explanations */ - JsonRoutes.add('get', '/api/b/:boardId/:userId/:loginToken', function (req, res) { - const { userId, loginToken, boardId } = req.params; - const hashToken = Accounts._hashLoginToken(loginToken); - const user = Meteor.users.findOne({ - _id: userId, - 'services.resume.loginTokens.hashedToken': hashToken, - }); + JsonRoutes.add('get', '/api/boards/:boardId', function (req, res) { + const boardId = req.params.boardId; + let user = null; + // todo XXX for real API, first look for token in Authentication: header + // then fallback to parameter + const loginToken = req.query.authToken; + if (loginToken) { + const hashToken = Accounts._hashLoginToken(loginToken); + user = Meteor.users.findOne({ + 'services.resume.loginTokens.hashedToken': hashToken, + }); + } const exporter = new Exporter(boardId); - if(user && exporter.canExport(user)) { + if(exporter.canExport(user)) { JsonRoutes.sendResult(res, 200, exporter.build()); } else { - // we could send an explicit error message, but on the other - // hand the only way to get there is by hacking the UI so... + // we could send an explicit error message, but on the other hand the only way to + // get there is by hacking the UI so let's keep it raw. JsonRoutes.sendResult(res, 403); } });