export.js 3.1 KB

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