export.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { Exporter } from './exporter';
  3. import { Meteor } from 'meteor/meteor';
  4. /* global JsonRoutes */
  5. if (Meteor.isServer) {
  6. import { Picker } from 'meteor/communitypackages:picker';
  7. // todo XXX once we have a real API in place, move that route there
  8. // todo XXX also share the route definition between the client and the server
  9. // so that we could use something like
  10. // `ApiRoutes.path('boards/export', boardId)``
  11. // on the client instead of copy/pasting the route path manually between the
  12. // client and the server.
  13. /**
  14. * @operation exportJson
  15. * @tag Boards
  16. *
  17. * @summary This route is used to export the board to a json file format.
  18. *
  19. * @description If user is already logged-in, pass loginToken as param
  20. * "authToken": '/api/boards/:boardId/export?authToken=:token'
  21. *
  22. * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
  23. * for detailed explanations
  24. *
  25. * @param {string} boardId the ID of the board we are exporting
  26. * @param {string} authToken the loginToken
  27. */
  28. JsonRoutes.add('get', '/api/boards/:boardId/export', function (req, res) {
  29. const boardId = req.params.boardId;
  30. let user = null;
  31. let impersonateDone = false;
  32. let adminId = null;
  33. // First check if board exists and is public to avoid unnecessary authentication
  34. const board = ReactiveCache.getBoard(boardId);
  35. if (!board) {
  36. JsonRoutes.sendResult(res, 404);
  37. return;
  38. }
  39. // If board is public, skip expensive authentication operations
  40. if (board.isPublic()) {
  41. // Public boards don't require authentication - skip hash operations
  42. const exporter = new Exporter(boardId);
  43. JsonRoutes.sendResult(res, {
  44. code: 200,
  45. data: exporter.build(),
  46. });
  47. return;
  48. }
  49. // Only perform expensive authentication for private boards
  50. const loginToken = req.query.authToken;
  51. if (loginToken) {
  52. // Validate token length to prevent resource abuse
  53. if (loginToken.length > 10000) {
  54. if (process.env.DEBUG === 'true') {
  55. console.warn('Suspiciously long auth token received, rejecting to prevent resource abuse');
  56. }
  57. JsonRoutes.sendResult(res, 400);
  58. return;
  59. }
  60. const hashToken = Accounts._hashLoginToken(loginToken);
  61. user = ReactiveCache.getUser({
  62. 'services.resume.loginTokens.hashedToken': hashToken,
  63. });
  64. adminId = user._id.toString();
  65. impersonateDone = ReactiveCache.getImpersonatedUser({ adminId: adminId });
  66. } else if (!Meteor.settings.public.sandstorm) {
  67. Authentication.checkUserId(req.userId);
  68. user = ReactiveCache.getUser({ _id: req.userId, isAdmin: true });
  69. }
  70. const exporter = new Exporter(boardId);
  71. if (exporter.canExport(user) || impersonateDone) {
  72. if (impersonateDone) {
  73. ImpersonatedUsers.insert({
  74. adminId: adminId,
  75. boardId: boardId,
  76. reason: 'exportJSON',
  77. });
  78. }
  79. JsonRoutes.sendResult(res, {
  80. code: 200,
  81. data: exporter.build(),
  82. });
  83. } else {
  84. // we could send an explicit error message, but on the other hand the only
  85. // way to get there is by hacking the UI so let's keep it raw.
  86. JsonRoutes.sendResult(res, 403);
  87. }
  88. });
  89. // todo XXX once we have a real API in place, move that route there
  90. // todo XXX also share the route definition between the client and the server
  91. // so that we could use something like
  92. // `ApiRoutes.path('boards/export', boardId)``
  93. // on the client instead of copy/pasting the route path manually between the
  94. // client and the server.
  95. /**
  96. * @operation exportJson
  97. * @tag Boards
  98. *
  99. * @summary This route is used to export a attachement to a json file format.
  100. *
  101. * @description If user is already logged-in, pass loginToken as param
  102. * "authToken": '/api/boards/:boardId/attachments/:attachmentId/export?authToken=:token'
  103. *
  104. *
  105. * @param {string} boardId the ID of the board we are exporting
  106. * @param {string} attachmentId the ID of the attachment we are exporting
  107. * @param {string} authToken the loginToken
  108. */
  109. JsonRoutes.add(
  110. 'get',
  111. '/api/boards/:boardId/attachments/:attachmentId/export',
  112. function (req, res) {
  113. const boardId = req.params.boardId;
  114. const attachmentId = req.params.attachmentId;
  115. let user = null;
  116. let impersonateDone = false;
  117. let adminId = null;
  118. // First check if board exists and is public to avoid unnecessary authentication
  119. const board = ReactiveCache.getBoard(boardId);
  120. if (!board) {
  121. JsonRoutes.sendResult(res, 404);
  122. return;
  123. }
  124. // If board is public, skip expensive authentication operations
  125. if (board.isPublic()) {
  126. // Public boards don't require authentication - skip hash operations
  127. const exporter = new Exporter(boardId, attachmentId);
  128. JsonRoutes.sendResult(res, {
  129. code: 200,
  130. data: exporter.build(),
  131. });
  132. return;
  133. }
  134. // Only perform expensive authentication for private boards
  135. const loginToken = req.query.authToken;
  136. if (loginToken) {
  137. // Validate token length to prevent resource abuse
  138. if (loginToken.length > 10000) {
  139. if (process.env.DEBUG === 'true') {
  140. console.warn('Suspiciously long auth token received, rejecting to prevent resource abuse');
  141. }
  142. JsonRoutes.sendResult(res, 400);
  143. return;
  144. }
  145. const hashToken = Accounts._hashLoginToken(loginToken);
  146. user = ReactiveCache.getUser({
  147. 'services.resume.loginTokens.hashedToken': hashToken,
  148. });
  149. adminId = user._id.toString();
  150. impersonateDone = ReactiveCache.getImpersonatedUser({ adminId: adminId });
  151. } else if (!Meteor.settings.public.sandstorm) {
  152. Authentication.checkUserId(req.userId);
  153. user = ReactiveCache.getUser({ _id: req.userId, isAdmin: true });
  154. }
  155. const exporter = new Exporter(boardId, attachmentId);
  156. if (exporter.canExport(user) || impersonateDone) {
  157. if (impersonateDone) {
  158. ImpersonatedUsers.insert({
  159. adminId: adminId,
  160. boardId: boardId,
  161. attachmentId: attachmentId,
  162. reason: 'exportJSONattachment',
  163. });
  164. }
  165. JsonRoutes.sendResult(res, {
  166. code: 200,
  167. data: exporter.build(),
  168. });
  169. } else {
  170. // we could send an explicit error message, but on the other hand the only
  171. // way to get there is by hacking the UI so let's keep it raw.
  172. JsonRoutes.sendResult(res, 403);
  173. }
  174. },
  175. );
  176. /**
  177. * @operation exportCSV/TSV
  178. * @tag Boards
  179. *
  180. * @summary This route is used to export the board to a CSV or TSV file format.
  181. *
  182. * @description If user is already logged-in, pass loginToken as param
  183. *
  184. * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
  185. * for detailed explanations
  186. *
  187. * @param {string} boardId the ID of the board we are exporting
  188. * @param {string} authToken the loginToken
  189. * @param {string} delimiter delimiter to use while building export. Default is comma ','
  190. */
  191. Picker.route('/api/boards/:boardId/export/csv', function (params, req, res) {
  192. const boardId = params.boardId;
  193. let user = null;
  194. let impersonateDone = false;
  195. let adminId = null;
  196. // First check if board exists and is public to avoid unnecessary authentication
  197. const board = ReactiveCache.getBoard(boardId);
  198. if (!board) {
  199. res.writeHead(404);
  200. res.end('Board not found');
  201. return;
  202. }
  203. // If board is public, skip expensive authentication operations
  204. if (board.isPublic()) {
  205. // Public boards don't require authentication - skip hash operations
  206. const exporter = new Exporter(boardId);
  207. if( params.query.delimiter == "\t" ) {
  208. // TSV file
  209. res.writeHead(200, {
  210. 'Content-Type': 'text/tsv',
  211. });
  212. }
  213. else {
  214. // CSV file (comma or semicolon)
  215. res.writeHead(200, {
  216. 'Content-Type': 'text/csv; charset=utf-8',
  217. });
  218. // Adding UTF8 BOM to quick fix MS Excel issue
  219. // use Uint8Array to prevent from converting bytes to string
  220. res.write(new Uint8Array([0xEF, 0xBB, 0xBF]));
  221. }
  222. res.write(exporter.buildCsv(params.query.delimiter, 'en'));
  223. res.end();
  224. return;
  225. }
  226. // Only perform expensive authentication for private boards
  227. const loginToken = params.query.authToken;
  228. if (loginToken) {
  229. // Validate token length to prevent resource abuse
  230. if (loginToken.length > 10000) {
  231. if (process.env.DEBUG === 'true') {
  232. console.warn('Suspiciously long auth token received, rejecting to prevent resource abuse');
  233. }
  234. res.writeHead(400);
  235. res.end('Invalid token');
  236. return;
  237. }
  238. const hashToken = Accounts._hashLoginToken(loginToken);
  239. user = ReactiveCache.getUser({
  240. 'services.resume.loginTokens.hashedToken': hashToken,
  241. });
  242. adminId = user._id.toString();
  243. impersonateDone = ReactiveCache.getImpersonatedUser({ adminId: adminId });
  244. } else if (!Meteor.settings.public.sandstorm) {
  245. Authentication.checkUserId(req.userId);
  246. user = ReactiveCache.getUser({
  247. _id: req.userId,
  248. isAdmin: true,
  249. });
  250. }
  251. const exporter = new Exporter(boardId);
  252. if (exporter.canExport(user) || impersonateDone) {
  253. if (impersonateDone) {
  254. let exportType = 'exportCSV';
  255. if( params.query.delimiter == "\t" ) {
  256. exportType = 'exportTSV';
  257. }
  258. ImpersonatedUsers.insert({
  259. adminId: adminId,
  260. boardId: boardId,
  261. reason: exportType,
  262. });
  263. }
  264. let userLanguage = 'en';
  265. if (user && user.profile) {
  266. userLanguage = user.profile.language
  267. }
  268. if( params.query.delimiter == "\t" ) {
  269. // TSV file
  270. res.writeHead(200, {
  271. 'Content-Type': 'text/tsv',
  272. });
  273. }
  274. else {
  275. // CSV file (comma or semicolon)
  276. res.writeHead(200, {
  277. 'Content-Type': 'text/csv; charset=utf-8',
  278. });
  279. // Adding UTF8 BOM to quick fix MS Excel issue
  280. // use Uint8Array to prevent from converting bytes to string
  281. res.write(new Uint8Array([0xEF, 0xBB, 0xBF]));
  282. }
  283. res.write(exporter.buildCsv(params.query.delimiter, userLanguage));
  284. res.end();
  285. } else {
  286. res.writeHead(403);
  287. res.end('Permission Error');
  288. }
  289. });
  290. }