share-links.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. // TODO remove this setting after implemented all
  2. import { SCOPE } from '@growi/core/dist/interfaces';
  3. import { ErrorV3 } from '@growi/core/dist/models';
  4. import express from 'express';
  5. import { SupportedAction } from '~/interfaces/activity';
  6. import { accessTokenParser } from '~/server/middlewares/access-token-parser';
  7. import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
  8. import adminRequiredFactory from '~/server/middlewares/admin-required';
  9. import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
  10. import { excludeReadOnlyUser } from '~/server/middlewares/exclude-read-only-user';
  11. import loginRequiredFactory from '~/server/middlewares/login-required';
  12. import ShareLink from '~/server/models/share-link';
  13. import loggerFactory from '~/utils/logger';
  14. const logger = loggerFactory('growi:routes:apiv3:share-links');
  15. const router = express.Router();
  16. const { body, query, param } = require('express-validator');
  17. const validator = {};
  18. const today = new Date();
  19. /**
  20. * @swagger
  21. *
  22. * components:
  23. * schemas:
  24. * ShareLink:
  25. * type: object
  26. * properties:
  27. * _id:
  28. * type: string
  29. * description: The unique identifier of the share link
  30. * relatedPage:
  31. * type: object
  32. * properties:
  33. * _id:
  34. * type: string
  35. * description: The unique identifier of the related page
  36. * path:
  37. * type: string
  38. * description: The path of the related page
  39. * expiredAt:
  40. * type: string
  41. * format: date-time
  42. * description: The expiration date of the share link
  43. * description:
  44. * type: string
  45. * description: The description of the share link
  46. * createdAt:
  47. * type: string
  48. * format: date-time
  49. * description: The creation date of the share link
  50. * __v:
  51. * type: integer
  52. * description: The version key
  53. * ShareLinkSimple:
  54. * type: object
  55. * properties:
  56. * relatedPage:
  57. * type: string
  58. * description: The unique identifier of the related page
  59. * expiredAt:
  60. * type: string
  61. * format: date-time
  62. * description: The expiration date of the share link
  63. * description:
  64. * type: string
  65. * description: The description of the share link
  66. * createdAt:
  67. * type: string
  68. * format: date-time
  69. * description: The creation date of the share link
  70. * __v:
  71. * type: integer
  72. * description: The version key
  73. * _id:
  74. * type: string
  75. * description: The unique identifier of the share link
  76. */
  77. /** @param {import('~/server/crowi').default} crowi Crowi instance */
  78. module.exports = (crowi) => {
  79. const loginRequired = loginRequiredFactory(crowi);
  80. const adminRequired = adminRequiredFactory(crowi);
  81. const addActivity = generateAddActivityMiddleware(crowi);
  82. const { Page } = crowi.models;
  83. const activityEvent = crowi.events.activity;
  84. /**
  85. * middleware to limit link sharing
  86. */
  87. const linkSharingRequired = (req, res, next) => {
  88. const isLinkSharingDisabled = crowi.configManager.getConfig(
  89. 'security:disableLinkSharing',
  90. );
  91. logger.debug(`isLinkSharingDisabled: ${isLinkSharingDisabled}`);
  92. if (isLinkSharingDisabled) {
  93. return res.apiv3Err(
  94. new ErrorV3('Link sharing is disabled', 'link-sharing-disabled'),
  95. );
  96. }
  97. next();
  98. };
  99. validator.getShareLinks = [
  100. // validate the page id is MongoId
  101. query('relatedPage').isMongoId().withMessage('Page Id is required'),
  102. ];
  103. /**
  104. * @swagger
  105. *
  106. * paths:
  107. * /share-links/:
  108. * get:
  109. * tags: [ShareLinks]
  110. * security:
  111. * - cookieAuth: []
  112. * description: get share links
  113. * parameters:
  114. * - name: relatedPage
  115. * in: query
  116. * required: true
  117. * description: page id of share link
  118. * schema:
  119. * type: string
  120. * responses:
  121. * 200:
  122. * description: Succeeded to get share links
  123. * content:
  124. * application/json:
  125. * schema:
  126. * properties:
  127. * shareLinksResult:
  128. * type: array
  129. * items:
  130. * $ref: '#/components/schemas/ShareLink'
  131. */
  132. router.get(
  133. '/',
  134. accessTokenParser([SCOPE.READ.FEATURES.SHARE_LINK]),
  135. loginRequired,
  136. linkSharingRequired,
  137. validator.getShareLinks,
  138. apiV3FormValidator,
  139. async (req, res) => {
  140. const { relatedPage } = req.query;
  141. const page = await Page.findByIdAndViewer(relatedPage, req.user);
  142. if (page == null) {
  143. const msg = 'Page is not found or forbidden';
  144. logger.error('Error', msg);
  145. return res.apiv3Err(new ErrorV3(msg, 'get-shareLink-failed'));
  146. }
  147. try {
  148. const shareLinksResult = await ShareLink.find({
  149. relatedPage: { $eq: relatedPage },
  150. }).populate({ path: 'relatedPage', select: 'path' });
  151. return res.apiv3({ shareLinksResult });
  152. } catch (err) {
  153. const msg = 'Error occurred in get share link';
  154. logger.error('Error', err);
  155. return res.apiv3Err(new ErrorV3(msg, 'get-shareLink-failed'));
  156. }
  157. },
  158. );
  159. validator.shareLinkStatus = [
  160. // validate the page id is MongoId
  161. body('relatedPage').isMongoId().withMessage('Page Id is required'),
  162. // validate expireation date is not empty, is not before today and is date.
  163. body('expiredAt')
  164. .if((value) => value != null)
  165. .isAfter(today.toString())
  166. .withMessage('Your Selected date is past'),
  167. // validate the length of description is max 100.
  168. body('description')
  169. .isLength({ min: 0, max: 100 })
  170. .withMessage('Max length is 100'),
  171. ];
  172. /**
  173. * @swagger
  174. *
  175. * paths:
  176. * /share-links/:
  177. * post:
  178. * tags: [ShareLinks]
  179. * security:
  180. * - cookieAuth: []
  181. * description: Create new share link
  182. * requestBody:
  183. * content:
  184. * application/json:
  185. * schema:
  186. * required:
  187. * - relatedPage
  188. * properties:
  189. * relatedPage:
  190. * description: page id of share link
  191. * type: string
  192. * expiredAt:
  193. * description: expiration date of share link
  194. * type: string
  195. * description:
  196. * description: description of share link
  197. * type: string
  198. * responses:
  199. * 200:
  200. * description: Succeeded to create one share link
  201. * content:
  202. * application/json:
  203. * schema:
  204. * $ref: '#/components/schemas/ShareLinkSimple'
  205. */
  206. router.post(
  207. '/',
  208. accessTokenParser([SCOPE.WRITE.FEATURES.SHARE_LINK]),
  209. loginRequired,
  210. excludeReadOnlyUser,
  211. linkSharingRequired,
  212. addActivity,
  213. validator.shareLinkStatus,
  214. apiV3FormValidator,
  215. async (req, res) => {
  216. const { relatedPage, expiredAt, description } = req.body;
  217. const page = await Page.findByIdAndViewer(relatedPage, req.user);
  218. if (page == null) {
  219. const msg = 'Page is not found or forbidden';
  220. logger.error('Error', msg);
  221. return res.apiv3Err(new ErrorV3(msg, 'post-shareLink-failed'));
  222. }
  223. try {
  224. const postedShareLink = await ShareLink.create({
  225. relatedPage,
  226. expiredAt,
  227. description,
  228. });
  229. activityEvent.emit('update', res.locals.activity._id, {
  230. action: SupportedAction.ACTION_SHARE_LINK_CREATE,
  231. });
  232. return res.apiv3(postedShareLink, 201);
  233. } catch (err) {
  234. const msg = 'Error occured in post share link';
  235. logger.error('Error', err);
  236. return res.apiv3Err(new ErrorV3(msg, 'post-shareLink-failed'));
  237. }
  238. },
  239. );
  240. validator.deleteShareLinks = [
  241. // validate the page id is MongoId
  242. query('relatedPage').isMongoId().withMessage('Page Id is required'),
  243. ];
  244. /**
  245. * @swagger
  246. *
  247. * /share-links/:
  248. * delete:
  249. * tags: [ShareLinks]
  250. * security:
  251. * - cookieAuth: []
  252. * summary: delete all share links related one page
  253. * description: delete all share links related one page
  254. * parameters:
  255. * - name: relatedPage
  256. * in: query
  257. * required: true
  258. * description: page id of share link
  259. * schema:
  260. * type: string
  261. * responses:
  262. * 200:
  263. * description: Succeeded to delete o all share links related one page
  264. * content:
  265. * application/json:
  266. * schema:
  267. * $ref: '#/components/schemas/ShareLinkSimple'
  268. */
  269. router.delete(
  270. '/',
  271. accessTokenParser([SCOPE.WRITE.FEATURES.SHARE_LINK]),
  272. loginRequired,
  273. excludeReadOnlyUser,
  274. addActivity,
  275. validator.deleteShareLinks,
  276. apiV3FormValidator,
  277. async (req, res) => {
  278. const { relatedPage } = req.query;
  279. const page = await Page.findByIdAndViewer(relatedPage, req.user);
  280. if (page == null) {
  281. const msg = 'Page is not found or forbidden';
  282. logger.error('Error', msg);
  283. return res.apiv3Err(
  284. new ErrorV3(msg, 'delete-shareLinks-for-page-failed'),
  285. );
  286. }
  287. try {
  288. const deletedShareLink = await ShareLink.deleteMany({
  289. relatedPage: { $eq: relatedPage },
  290. });
  291. activityEvent.emit('update', res.locals.activity._id, {
  292. action: SupportedAction.ACTION_SHARE_LINK_DELETE_BY_PAGE,
  293. });
  294. return res.apiv3(deletedShareLink);
  295. } catch (err) {
  296. const msg = 'Error occured in delete share link';
  297. logger.error('Error', err);
  298. return res.apiv3Err(new ErrorV3(msg, 'delete-shareLink-failed'));
  299. }
  300. },
  301. );
  302. /**
  303. * @swagger
  304. *
  305. * /share-links/all:
  306. * delete:
  307. * tags: [ShareLink Management]
  308. * security:
  309. * - cookieAuth: []
  310. * summary: delete all share links
  311. * description: delete all share links
  312. * responses:
  313. * 200:
  314. * description: Succeeded to remove all share links
  315. * content:
  316. * application/json:
  317. * schema:
  318. * properties:
  319. * deletedCount:
  320. * type: integer
  321. * description: The number of share links deleted
  322. */
  323. router.delete(
  324. '/all',
  325. accessTokenParser([SCOPE.WRITE.FEATURES.SHARE_LINK]),
  326. loginRequired,
  327. adminRequired,
  328. addActivity,
  329. async (req, res) => {
  330. try {
  331. const deletedShareLink = await ShareLink.deleteMany({});
  332. const { deletedCount } = deletedShareLink;
  333. activityEvent.emit('update', res.locals.activity._id, {
  334. action: SupportedAction.ACTION_SHARE_LINK_ALL_DELETE,
  335. });
  336. return res.apiv3({ deletedCount });
  337. } catch (err) {
  338. const msg = 'Error occurred in delete all share link';
  339. logger.error('Error', err);
  340. return res.apiv3Err(new ErrorV3(msg, 'delete-all-shareLink-failed'));
  341. }
  342. },
  343. );
  344. validator.deleteShareLink = [
  345. param('id').isMongoId().withMessage('ShareLink Id is required'),
  346. ];
  347. /**
  348. * @swagger
  349. *
  350. * /share-links/{id}:
  351. * delete:
  352. * tags: [ShareLinks]
  353. * security:
  354. * - cookieAuth: []
  355. * description: delete one share link related one page
  356. * parameters:
  357. * - name: id
  358. * in: path
  359. * required: true
  360. * description: id of share link
  361. * schema:
  362. * type: string
  363. * responses:
  364. * 200:
  365. * description: Succeeded to delete one share link
  366. */
  367. router.delete(
  368. '/:id',
  369. accessTokenParser([SCOPE.WRITE.FEATURES.SHARE_LINK]),
  370. loginRequired,
  371. excludeReadOnlyUser,
  372. addActivity,
  373. validator.deleteShareLink,
  374. apiV3FormValidator,
  375. async (req, res) => {
  376. const { id } = req.params;
  377. const { user } = req;
  378. try {
  379. const shareLinkToDelete = await ShareLink.findOne({ _id: id });
  380. // check permission
  381. if (!user.isAdmin) {
  382. const page = await Page.findByIdAndViewer(
  383. shareLinkToDelete.relatedPage,
  384. user,
  385. );
  386. const isPageExists =
  387. (await Page.count({ _id: shareLinkToDelete.relatedPage })) > 0;
  388. if (page == null && isPageExists) {
  389. const msg = 'Page is not found or forbidden';
  390. logger.error('Error', msg);
  391. return res.apiv3Err(new ErrorV3(msg, 'delete-shareLink-failed'));
  392. }
  393. }
  394. // remove
  395. await shareLinkToDelete.remove();
  396. activityEvent.emit('update', res.locals.activity._id, {
  397. action: SupportedAction.ACTION_SHARE_LINK_DELETE,
  398. });
  399. return res.apiv3({ deletedShareLink: shareLinkToDelete });
  400. } catch (err) {
  401. const msg = 'Error occurred in delete share link';
  402. logger.error('Error', err);
  403. return res.apiv3Err(new ErrorV3(msg, 'delete-shareLink-failed'));
  404. }
  405. },
  406. );
  407. return router;
  408. };