export.js 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /* global JsonRoutes */
  2. if(Meteor.isServer) {
  3. // todo XXX once we have a real API in place, move that route there
  4. /*
  5. * This route is used to export the board FROM THE APPLICATION.
  6. * We want to identify the logged-in user without asking for password again,
  7. * but the server-side API routing has no notion of "current user".
  8. * So we have to pass login information (id + token) to authenticate.
  9. *
  10. * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
  11. * for detailed explanations
  12. */
  13. JsonRoutes.add('get', '/api/b/:boardId/:userId/:loginToken', function (req, res) {
  14. const { userId, loginToken, boardId } = req.params;
  15. const hashToken = Accounts._hashLoginToken(loginToken);
  16. const user = Meteor.users.findOne({
  17. _id: userId,
  18. 'services.resume.loginTokens.hashedToken': hashToken,
  19. });
  20. const exporter = new Exporter(boardId);
  21. if(user && exporter.canExport(user)) {
  22. JsonRoutes.sendResult(res, 200, exporter.build());
  23. } else {
  24. // we could send an explicit error message, but on the other
  25. // hand the only way to get there is by hacking the UI so...
  26. JsonRoutes.sendResult(res, 403);
  27. }
  28. });
  29. }
  30. class Exporter {
  31. constructor(boardId) {
  32. this._boardId = boardId;
  33. }
  34. build() {
  35. const byBoard = {boardId: this._boardId};
  36. // we do not want to retrieve boardId in related elements
  37. const noBoardId = {fields: {boardId: 0}};
  38. const result = {
  39. _format: 'wekan-board-1.0.0',
  40. };
  41. _.extend(result, Boards.findOne(this._boardId, {fields: {stars: 0}}));
  42. result.lists = Lists.find(byBoard, noBoardId).fetch();
  43. result.cards = Cards.find(byBoard, noBoardId).fetch();
  44. result.comments = CardComments.find(byBoard, noBoardId).fetch();
  45. result.activities = Activities.find(byBoard, noBoardId).fetch();
  46. // for attachments we only export IDs and absolute url to original doc
  47. result.attachments = Attachments.find(byBoard).fetch().map((attachment) => { return {
  48. _id: attachment._id,
  49. cardId: attachment.cardId,
  50. url: Meteor.absoluteUrl(Utils.stripLeadingSlash(attachment.url())),
  51. };});
  52. // we also have to export some user data - as the other elements only include id
  53. // but we have to be careful:
  54. // 1- only exports users that are linked somehow to that board
  55. // 2- do not export any sensitive information
  56. const users = {};
  57. result.members.forEach((member) => {users[member.userId] = true;});
  58. result.lists.forEach((list) => {users[list.userId] = true;});
  59. result.cards.forEach((card) => {
  60. users[card.userId] = true;
  61. if (card.members) {
  62. card.members.forEach((memberId) => {users[memberId] = true;});
  63. }
  64. });
  65. result.comments.forEach((comment) => {users[comment.userId] = true;});
  66. result.activities.forEach((activity) => {users[activity.userId] = true;});
  67. const byUserIds = {_id: {$in: Object.getOwnPropertyNames(users)}};
  68. // we use whitelist to be sure we do not expose inadvertently
  69. // some secret fields that gets added to User later.
  70. const userFields = {fields: {
  71. _id: 1,
  72. username: 1,
  73. 'profile.fullname': 1,
  74. 'profile.initials': 1,
  75. 'profile.avatarUrl': 1,
  76. }};
  77. result.users = Users.find(byUserIds, userFields).fetch().map((user) => {
  78. // user avatar is stored as a relative url, we export absolute
  79. if(user.profile.avatarUrl) {
  80. user.profile.avatarUrl = Meteor.absoluteUrl(Utils.stripLeadingSlash(user.profile.avatarUrl));
  81. }
  82. return user;
  83. });
  84. return result;
  85. }
  86. canExport(user) {
  87. const board = Boards.findOne(this._boardId);
  88. return board && board.isVisibleBy(user);
  89. }
  90. }