customize-setting.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. /* eslint-disable no-unused-vars */
  2. import { SupportedAction } from '~/interfaces/activity';
  3. import loggerFactory from '~/utils/logger';
  4. import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
  5. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  6. const logger = loggerFactory('growi:routes:apiv3:customize-setting');
  7. const express = require('express');
  8. const router = express.Router();
  9. const { body, query } = require('express-validator');
  10. const ErrorV3 = require('../../models/vo/error-apiv3');
  11. /**
  12. * @swagger
  13. * tags:
  14. * name: CustomizeSetting
  15. */
  16. /**
  17. * @swagger
  18. *
  19. * components:
  20. * schemas:
  21. * CustomizeLayout:
  22. * description: CustomizeLayout
  23. * type: object
  24. * properties:
  25. * isContainerFluid:
  26. * type: boolean
  27. * CustomizeTheme:
  28. * description: CustomizeTheme
  29. * type: object
  30. * properties:
  31. * themeType:
  32. * type: string
  33. * CustomizeFunction:
  34. * description: CustomizeFunction
  35. * type: object
  36. * properties:
  37. * isEnabledTimeline:
  38. * type: boolean
  39. * isSavedStatesOfTabChanges:
  40. * type: boolean
  41. * isEnabledAttachTitleHeader:
  42. * type: boolean
  43. * pageLimitationS:
  44. * type: number
  45. * pageLimitationM:
  46. * type: number
  47. * isEnabledStaleNotification:
  48. * type: boolean
  49. * isAllReplyShown:
  50. * type: boolean
  51. * isSearchScopeChildrenAsDefault:
  52. * type: boolean
  53. * CustomizeHighlight:
  54. * description: CustomizeHighlight
  55. * type: object
  56. * properties:
  57. * styleName:
  58. * type: string
  59. * styleBorder:
  60. * type: boolean
  61. * CustomizeTitle:
  62. * description: CustomizeTitle
  63. * type: object
  64. * properties:
  65. * customizeTitle:
  66. * type: string
  67. * CustomizeHeader:
  68. * description: CustomizeHeader
  69. * type: object
  70. * properties:
  71. * customizeHeader:
  72. * type: string
  73. * CustomizeCss:
  74. * description: CustomizeCss
  75. * type: object
  76. * properties:
  77. * customizeCss:
  78. * type: string
  79. * CustomizeScript:
  80. * description: CustomizeScript
  81. * type: object
  82. * properties:
  83. * customizeScript:
  84. * type: string
  85. */
  86. module.exports = (crowi) => {
  87. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  88. const adminRequired = require('../../middlewares/admin-required')(crowi);
  89. const csrf = require('../../middlewares/csrf')(crowi);
  90. const addActivity = generateAddActivityMiddleware(crowi);
  91. const activityEvent = crowi.event('activity');
  92. const { customizeService } = crowi;
  93. const validator = {
  94. layout: [
  95. body('isContainerFluid').isBoolean(),
  96. ],
  97. themeAssetPath: [
  98. query('themeName').isString(),
  99. ],
  100. theme: [
  101. body('themeType').isString(),
  102. ],
  103. function: [
  104. body('isEnabledTimeline').isBoolean(),
  105. body('isSavedStatesOfTabChanges').isBoolean(),
  106. body('isEnabledAttachTitleHeader').isBoolean(),
  107. body('pageLimitationS').isInt().isInt({ min: 1, max: 1000 }),
  108. body('pageLimitationM').isInt().isInt({ min: 1, max: 1000 }),
  109. body('pageLimitationL').isInt().isInt({ min: 1, max: 1000 }),
  110. body('pageLimitationXL').isInt().isInt({ min: 1, max: 1000 }),
  111. body('isEnabledStaleNotification').isBoolean(),
  112. body('isAllReplyShown').isBoolean(),
  113. body('isSearchScopeChildrenAsDefault').isBoolean(),
  114. ],
  115. customizeTitle: [
  116. body('customizeTitle').isString(),
  117. ],
  118. customizeHeader: [
  119. body('customizeHeader').isString(),
  120. ],
  121. highlight: [
  122. body('highlightJsStyle').isString().isIn([
  123. 'github', 'github-gist', 'atom-one-light', 'xcode', 'vs', 'atom-one-dark', 'hybrid', 'monokai', 'tomorrow-night', 'vs2015',
  124. ]),
  125. body('highlightJsStyleBorder').isBoolean(),
  126. ],
  127. customizeCss: [
  128. body('customizeCss').isString(),
  129. ],
  130. customizeScript: [
  131. body('customizeScript').isString(),
  132. ],
  133. };
  134. /**
  135. * @swagger
  136. *
  137. * /customize-setting:
  138. * get:
  139. * tags: [CustomizeSetting]
  140. * operationId: getCustomizeSetting
  141. * summary: /customize-setting
  142. * description: Get customize parameters
  143. * responses:
  144. * 200:
  145. * description: params of customize
  146. * content:
  147. * application/json:
  148. * schema:
  149. * properties:
  150. * customizeParams:
  151. * type: object
  152. * description: customize params
  153. */
  154. router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
  155. const customizeParams = {
  156. themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
  157. isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
  158. isSavedStatesOfTabChanges: await crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
  159. isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
  160. pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
  161. pageLimitationM: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM'),
  162. pageLimitationL: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
  163. pageLimitationXL: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
  164. isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
  165. isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
  166. isSearchScopeChildrenAsDefault: await crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
  167. styleName: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyle'),
  168. styleBorder: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
  169. customizeTitle: await crowi.configManager.getConfig('crowi', 'customize:title'),
  170. customizeHeader: await crowi.configManager.getConfig('crowi', 'customize:header'),
  171. customizeCss: await crowi.configManager.getConfig('crowi', 'customize:css'),
  172. customizeScript: await crowi.configManager.getConfig('crowi', 'customize:script'),
  173. };
  174. return res.apiv3({ customizeParams });
  175. });
  176. /**
  177. * @swagger
  178. *
  179. * /customize-setting/layout:
  180. * get:
  181. * tags: [CustomizeSetting]
  182. * operationId: getLayoutCustomizeSetting
  183. * summary: /customize-setting/layout
  184. * description: Get layout
  185. * responses:
  186. * 200:
  187. * description: Succeeded to get layout
  188. * content:
  189. * application/json:
  190. * schema:
  191. * $ref: '#/components/schemas/CustomizeLayout'
  192. */
  193. router.get('/layout', loginRequiredStrictly, adminRequired, async(req, res) => {
  194. try {
  195. const isContainerFluid = await crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
  196. return res.apiv3({ isContainerFluid });
  197. }
  198. catch (err) {
  199. const msg = 'Error occurred in getting layout';
  200. logger.error('Error', err);
  201. return res.apiv3Err(new ErrorV3(msg, 'get-layout-failed'));
  202. }
  203. });
  204. /**
  205. * @swagger
  206. *
  207. * /customize-setting/layout:
  208. * put:
  209. * tags: [CustomizeSetting]
  210. * operationId: updateLayoutCustomizeSetting
  211. * summary: /customize-setting/layout
  212. * description: Update layout
  213. * requestBody:
  214. * required: true
  215. * content:
  216. * application/json:
  217. * schema:
  218. * $ref: '#/components/schemas/CustomizeLayout'
  219. * responses:
  220. * 200:
  221. * description: Succeeded to update layout
  222. * content:
  223. * application/json:
  224. * schema:
  225. * $ref: '#/components/schemas/CustomizeLayout'
  226. */
  227. router.put('/layout', loginRequiredStrictly, adminRequired, csrf, addActivity, validator.layout, apiV3FormValidator, async(req, res) => {
  228. const requestParams = {
  229. 'customize:isContainerFluid': req.body.isContainerFluid,
  230. };
  231. try {
  232. await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
  233. const customizedParams = {
  234. isContainerFluid: await crowi.configManager.getConfig('crowi', 'customize:isContainerFluid'),
  235. };
  236. const parameters = { action: SupportedAction.ACTION_ADMIN_LAYOUT_UPDATE };
  237. activityEvent.emit('update', res.locals.activity._id, parameters);
  238. return res.apiv3({ customizedParams });
  239. }
  240. catch (err) {
  241. const msg = 'Error occurred in updating layout';
  242. logger.error('Error', err);
  243. return res.apiv3Err(new ErrorV3(msg, 'update-layout-failed'));
  244. }
  245. });
  246. /**
  247. * @swagger
  248. *
  249. * /customize-setting/theme/asset-path:
  250. * put:
  251. * tags: [CustomizeSetting]
  252. * operationId: getThemeAssetPath
  253. * summary: /customize-setting/theme/asset-path
  254. * description: Get theme asset path
  255. * parameters:
  256. * - name: themeName
  257. * in: query
  258. * required: true
  259. * schema:
  260. * type: string
  261. * responses:
  262. * 200:
  263. * description: Succeeded to get theme asset path
  264. * content:
  265. * application/json:
  266. * schema:
  267. * properties:
  268. * assetPath:
  269. * type: string
  270. */
  271. router.get('/theme/asset-path', loginRequiredStrictly, adminRequired, validator.themeAssetPath, apiV3FormValidator, async(req, res) => {
  272. const { themeName } = req.query;
  273. const webpackAssetKey = `styles/theme-${themeName}.css`;
  274. const assetPath = res.locals.webpack_asset(webpackAssetKey);
  275. if (assetPath == null) {
  276. return res.apiv3Err(new ErrorV3(`The asset for '${webpackAssetKey}' is undefined.`, 'invalid-asset'));
  277. }
  278. return res.apiv3({ assetPath });
  279. });
  280. /**
  281. * @swagger
  282. *
  283. * /customize-setting/theme:
  284. * put:
  285. * tags: [CustomizeSetting]
  286. * operationId: updateThemeCustomizeSetting
  287. * summary: /customize-setting/theme
  288. * description: Update theme
  289. * requestBody:
  290. * required: true
  291. * content:
  292. * application/json:
  293. * schema:
  294. * $ref: '#/components/schemas/CustomizeTheme'
  295. * responses:
  296. * 200:
  297. * description: Succeeded to update theme
  298. * content:
  299. * application/json:
  300. * schema:
  301. * $ref: '#/components/schemas/CustomizeTheme'
  302. */
  303. router.put('/theme', loginRequiredStrictly, adminRequired, csrf, validator.theme, apiV3FormValidator, async(req, res) => {
  304. const requestParams = {
  305. 'customize:theme': req.body.themeType,
  306. };
  307. try {
  308. await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
  309. const customizedParams = {
  310. themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
  311. };
  312. return res.apiv3({ customizedParams });
  313. }
  314. catch (err) {
  315. const msg = 'Error occurred in updating theme';
  316. logger.error('Error', err);
  317. return res.apiv3Err(new ErrorV3(msg, 'update-theme-failed'));
  318. }
  319. });
  320. /**
  321. * @swagger
  322. *
  323. * /customize-setting/function:
  324. * put:
  325. * tags: [CustomizeSetting]
  326. * operationId: updateFunctionCustomizeSetting
  327. * summary: /customize-setting/function
  328. * description: Update function
  329. * requestBody:
  330. * required: true
  331. * content:
  332. * application/json:
  333. * schema:
  334. * $ref: '#/components/schemas/CustomizeFunction'
  335. * responses:
  336. * 200:
  337. * description: Succeeded to update function
  338. * content:
  339. * application/json:
  340. * schema:
  341. * $ref: '#/components/schemas/CustomizeFunction'
  342. */
  343. router.put('/function', loginRequiredStrictly, adminRequired, csrf, validator.function, apiV3FormValidator, async(req, res) => {
  344. const requestParams = {
  345. 'customize:isEnabledTimeline': req.body.isEnabledTimeline,
  346. 'customize:isSavedStatesOfTabChanges': req.body.isSavedStatesOfTabChanges,
  347. 'customize:isEnabledAttachTitleHeader': req.body.isEnabledAttachTitleHeader,
  348. 'customize:showPageLimitationS': req.body.pageLimitationS,
  349. 'customize:showPageLimitationM': req.body.pageLimitationM,
  350. 'customize:showPageLimitationL': req.body.pageLimitationL,
  351. 'customize:showPageLimitationXL': req.body.pageLimitationXL,
  352. 'customize:isEnabledStaleNotification': req.body.isEnabledStaleNotification,
  353. 'customize:isAllReplyShown': req.body.isAllReplyShown,
  354. 'customize:isSearchScopeChildrenAsDefault': req.body.isSearchScopeChildrenAsDefault,
  355. };
  356. try {
  357. await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
  358. const customizedParams = {
  359. isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
  360. isSavedStatesOfTabChanges: await crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
  361. isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
  362. pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
  363. pageLimitationM: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM'),
  364. pageLimitationL: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
  365. pageLimitationXL: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
  366. isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
  367. isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
  368. isSearchScopeChildrenAsDefault: await crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
  369. };
  370. return res.apiv3({ customizedParams });
  371. }
  372. catch (err) {
  373. const msg = 'Error occurred in updating function';
  374. logger.error('Error', err);
  375. return res.apiv3Err(new ErrorV3(msg, 'update-function-failed'));
  376. }
  377. });
  378. /**
  379. * @swagger
  380. *
  381. * /customize-setting/highlight:
  382. * put:
  383. * tags: [CustomizeSetting]
  384. * operationId: updateHighlightCustomizeSetting
  385. * summary: /customize-setting/highlight
  386. * description: Update highlight
  387. * requestBody:
  388. * required: true
  389. * content:
  390. * application/json:
  391. * schema:
  392. * $ref: '#/components/schemas/CustomizeHighlight'
  393. * responses:
  394. * 200:
  395. * description: Succeeded to update highlight
  396. * content:
  397. * application/json:
  398. * schema:
  399. * $ref: '#/components/schemas/CustomizeHighlight'
  400. */
  401. router.put('/highlight', loginRequiredStrictly, adminRequired, csrf, validator.highlight, apiV3FormValidator, async(req, res) => {
  402. const requestParams = {
  403. 'customize:highlightJsStyle': req.body.highlightJsStyle,
  404. 'customize:highlightJsStyleBorder': req.body.highlightJsStyleBorder,
  405. };
  406. try {
  407. await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
  408. const customizedParams = {
  409. styleName: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyle'),
  410. styleBorder: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
  411. };
  412. return res.apiv3({ customizedParams });
  413. }
  414. catch (err) {
  415. const msg = 'Error occurred in updating highlight';
  416. logger.error('Error', err);
  417. return res.apiv3Err(new ErrorV3(msg, 'update-highlight-failed'));
  418. }
  419. });
  420. /**
  421. * @swagger
  422. *
  423. * /customize-setting/customizeTitle:
  424. * put:
  425. * tags: [CustomizeSetting]
  426. * operationId: updateCustomizeTitleCustomizeSetting
  427. * summary: /customize-setting/customizeTitle
  428. * description: Update customizeTitle
  429. * requestBody:
  430. * required: true
  431. * content:
  432. * application/json:
  433. * schema:
  434. * $ref: '#/components/schemas/CustomizeTitle'
  435. * responses:
  436. * 200:
  437. * description: Succeeded to update customizeTitle
  438. * content:
  439. * application/json:
  440. * schema:
  441. * $ref: '#/components/schemas/CustomizeTitle'
  442. */
  443. router.put('/customize-title', loginRequiredStrictly, adminRequired, csrf, validator.customizeTitle, apiV3FormValidator, async(req, res) => {
  444. const requestParams = {
  445. 'customize:title': req.body.customizeTitle,
  446. };
  447. try {
  448. await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams, true);
  449. crowi.customizeService.publishUpdatedMessage();
  450. const customizedParams = {
  451. customizeTitle: await crowi.configManager.getConfig('crowi', 'customize:title'),
  452. };
  453. customizeService.initCustomTitle();
  454. return res.apiv3({ customizedParams });
  455. }
  456. catch (err) {
  457. const msg = 'Error occurred in updating customizeTitle';
  458. logger.error('Error', err);
  459. return res.apiv3Err(new ErrorV3(msg, 'update-customizeTitle-failed'));
  460. }
  461. });
  462. /**
  463. * @swagger
  464. *
  465. * /customize-setting/customizeHeader:
  466. * put:
  467. * tags: [CustomizeSetting]
  468. * operationId: updateCustomizeHeaderCustomizeSetting
  469. * summary: /customize-setting/customizeHeader
  470. * description: Update customizeHeader
  471. * requestBody:
  472. * required: true
  473. * content:
  474. * application/json:
  475. * schema:
  476. * $ref: '#/components/schemas/CustomizeHeader'
  477. * responses:
  478. * 200:
  479. * description: Succeeded to update customize header
  480. * content:
  481. * application/json:
  482. * schema:
  483. * $ref: '#/components/schemas/CustomizeHeader'
  484. */
  485. router.put('/customize-header', loginRequiredStrictly, adminRequired, csrf, validator.customizeHeader, apiV3FormValidator, async(req, res) => {
  486. const requestParams = {
  487. 'customize:header': req.body.customizeHeader,
  488. };
  489. try {
  490. await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
  491. const customizedParams = {
  492. customizeHeader: await crowi.configManager.getConfig('crowi', 'customize:header'),
  493. };
  494. return res.apiv3({ customizedParams });
  495. }
  496. catch (err) {
  497. const msg = 'Error occurred in updating customizeHeader';
  498. logger.error('Error', err);
  499. return res.apiv3Err(new ErrorV3(msg, 'update-customizeHeader-failed'));
  500. }
  501. });
  502. /**
  503. * @swagger
  504. *
  505. * /customize-setting/customizeCss:
  506. * put:
  507. * tags: [CustomizeSetting]
  508. * operationId: updateCustomizeCssCustomizeSetting
  509. * summary: /customize-setting/customizeCss
  510. * description: Update customizeCss
  511. * requestBody:
  512. * required: true
  513. * content:
  514. * application/json:
  515. * schema:
  516. * $ref: '#/components/schemas/CustomizeCss'
  517. * responses:
  518. * 200:
  519. * description: Succeeded to update customize css
  520. * content:
  521. * application/json:
  522. * schema:
  523. * $ref: '#/components/schemas/CustomizeCss'
  524. */
  525. router.put('/customize-css', loginRequiredStrictly, adminRequired, csrf, validator.customizeCss, apiV3FormValidator, async(req, res) => {
  526. const requestParams = {
  527. 'customize:css': req.body.customizeCss,
  528. };
  529. try {
  530. await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams, true);
  531. crowi.customizeService.publishUpdatedMessage();
  532. const customizedParams = {
  533. customizeCss: await crowi.configManager.getConfig('crowi', 'customize:css'),
  534. };
  535. customizeService.initCustomCss();
  536. return res.apiv3({ customizedParams });
  537. }
  538. catch (err) {
  539. const msg = 'Error occurred in updating customizeCss';
  540. logger.error('Error', err);
  541. return res.apiv3Err(new ErrorV3(msg, 'update-customizeCss-failed'));
  542. }
  543. });
  544. /**
  545. * @swagger
  546. *
  547. * /customize-setting/customizeScript:
  548. * put:
  549. * tags: [CustomizeSetting]
  550. * operationId: updateCustomizeScriptCustomizeSetting
  551. * summary: /customize-setting/customizeScript
  552. * description: Update customizeScript
  553. * requestBody:
  554. * required: true
  555. * content:
  556. * application/json:
  557. * schema:
  558. * $ref: '#/components/schemas/CustomizeScript'
  559. * responses:
  560. * 200:
  561. * description: Succeeded to update customize script
  562. * content:
  563. * application/json:
  564. * schema:
  565. * $ref: '#/components/schemas/CustomizeScript'
  566. */
  567. router.put('/customize-script', loginRequiredStrictly, adminRequired, csrf, validator.customizeScript, apiV3FormValidator, async(req, res) => {
  568. const requestParams = {
  569. 'customize:script': req.body.customizeScript,
  570. };
  571. try {
  572. await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
  573. const customizedParams = {
  574. customizeScript: await crowi.configManager.getConfig('crowi', 'customize:script'),
  575. };
  576. return res.apiv3({ customizedParams });
  577. }
  578. catch (err) {
  579. const msg = 'Error occurred in updating customizeScript';
  580. logger.error('Error', err);
  581. return res.apiv3Err(new ErrorV3(msg, 'update-customizeScript-failed'));
  582. }
  583. });
  584. return router;
  585. };