customize-setting.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107
  1. /* eslint-disable no-unused-vars */
  2. import { GrowiPluginType } from '@growi/core';
  3. import { ErrorV3 } from '@growi/core/dist/models';
  4. import express from 'express';
  5. import { body } from 'express-validator';
  6. import multer from 'multer';
  7. import { GrowiPlugin } from '~/features/growi-plugin/server/models';
  8. import { SupportedAction } from '~/interfaces/activity';
  9. import { SCOPE } from '~/interfaces/scope';
  10. import { AttachmentType } from '~/server/interfaces/attachment';
  11. import { accessTokenParser } from '~/server/middlewares/access-token-parser';
  12. import { Attachment } from '~/server/models/attachment';
  13. import { configManager } from '~/server/service/config-manager';
  14. import loggerFactory from '~/utils/logger';
  15. import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
  16. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  17. const logger = loggerFactory('growi:routes:apiv3:customize-setting');
  18. const router = express.Router();
  19. /**
  20. * @swagger
  21. *
  22. * components:
  23. * schemas:
  24. * CustomizeLayout:
  25. * description: CustomizeLayout
  26. * type: object
  27. * properties:
  28. * isContainerFluid:
  29. * type: boolean
  30. * CustomizeTheme:
  31. * description: CustomizeTheme
  32. * type: object
  33. * properties:
  34. * theme:
  35. * type: string
  36. * CustomizeFunction:
  37. * description: CustomizeFunction
  38. * type: object
  39. * properties:
  40. * isEnabledTimeline:
  41. * type: boolean
  42. * isEnabledAttachTitleHeader:
  43. * type: boolean
  44. * pageLimitationS:
  45. * type: number
  46. * pageLimitationM:
  47. * type: number
  48. * isEnabledStaleNotification:
  49. * type: boolean
  50. * isAllReplyShown:
  51. * type: boolean
  52. * isSearchScopeChildrenAsDefault:
  53. * type: boolean
  54. * CustomizeHighlight:
  55. * description: CustomizeHighlight
  56. * type: object
  57. * properties:
  58. * highlightJsStyle:
  59. * type: string
  60. * highlightJsStyleBorder:
  61. * type: boolean
  62. * CustomizeHighlightResponse:
  63. * description: CustomizeHighlight Response
  64. * type: object
  65. * properties:
  66. * styleName:
  67. * type: string
  68. * styleBorder:
  69. * type: boolean
  70. * CustomizeTitle:
  71. * description: CustomizeTitle
  72. * type: object
  73. * properties:
  74. * customizeTitle:
  75. * type: string
  76. * CustomizeNoscript:
  77. * description: CustomizeNoscript
  78. * type: object
  79. * properties:
  80. * customizeNoscript:
  81. * type: string
  82. * CustomizeCss:
  83. * description: CustomizeCss
  84. * type: object
  85. * properties:
  86. * customizeCss:
  87. * type: string
  88. * CustomizeScript:
  89. * description: CustomizeScript
  90. * type: object
  91. * properties:
  92. * customizeScript:
  93. * type: string
  94. * CustomizeSetting:
  95. * description: Customize Setting
  96. * type: object
  97. * properties:
  98. * isEnabledTimeline:
  99. * type: boolean
  100. * isEnabledAttachTitleHeader:
  101. * type: boolean
  102. * pageLimitationS:
  103. * type: number
  104. * pageLimitationM:
  105. * type: number
  106. * pageLimitationL:
  107. * type: number
  108. * pageLimitationXL:
  109. * type: number
  110. * isEnabledStaleNotification:
  111. * type: boolean
  112. * isAllReplyShown:
  113. * type: boolean
  114. * isSearchScopeChildrenAsDefault:
  115. * type: boolean
  116. * isEnabledMarp:
  117. * type: boolean
  118. * styleName:
  119. * type: string
  120. * styleBorder:
  121. * type: string
  122. * customizeTitle:
  123. * type: string
  124. * customizeScript:
  125. * type: string
  126. * customizeCss:
  127. * type: string
  128. * customizeNoscript:
  129. * type: string
  130. * ThemesMetadata:
  131. * type: object
  132. * properties:
  133. * name:
  134. * type: string
  135. * description: The name of the plugin theme.
  136. * manifestKey:
  137. * type: string
  138. * description: Path to the theme manifest file.
  139. * schemeType:
  140. * type: string
  141. * description: The color scheme type (e.g., light or dark).
  142. * lightBg:
  143. * type: string
  144. * description: Light mode background color (hex).
  145. * darkBg:
  146. * type: string
  147. * description: Dark mode background color (hex).
  148. * lightSidebar:
  149. * type: string
  150. * description: Light mode sidebar color (hex).
  151. * darkSidebar:
  152. * type: string
  153. * description: Dark mode sidebar color (hex).
  154. * lightIcon:
  155. * type: string
  156. * description: Light mode icon color (hex).
  157. * darkIcon:
  158. * type: string
  159. * description: Dark mode icon color (hex).
  160. * createBtn:
  161. * type: string
  162. * description: Color of the create button (hex).
  163. * CustomizeSidebar:
  164. * description: Customize Sidebar
  165. * type: object
  166. * properties:
  167. * isSidebarCollapsedMode:
  168. * type: boolean
  169. * description: The flag whether sidebar is collapsed mode or not.
  170. * isSidebarClosedAtDockMode:
  171. * type: boolean
  172. * description: The flag whether sidebar is closed at dock mode or not.
  173. * CustomizePresentation:
  174. * description: Customize Presentation
  175. * type: object
  176. * properties:
  177. * isEnabledMarp:
  178. * type: boolean
  179. * description: The flag whether Marp is enabled or not.
  180. * CustomizeLogo:
  181. * description: Customize Logo
  182. * type: object
  183. * properties:
  184. * isDefaultLogo:
  185. * type: boolean
  186. * description: The flag whether the logo is default or not.
  187. */
  188. /** @param {import('~/server/crowi').default} crowi Crowi instance */
  189. module.exports = (crowi) => {
  190. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  191. const adminRequired = require('../../middlewares/admin-required')(crowi);
  192. const addActivity = generateAddActivityMiddleware(crowi);
  193. const activityEvent = crowi.event('activity');
  194. const { customizeService, attachmentService } = crowi;
  195. const uploads = multer({ dest: `${crowi.tmpDir}uploads` });
  196. const validator = {
  197. layout: [
  198. body('isContainerFluid').isBoolean(),
  199. ],
  200. theme: [
  201. body('theme').isString(),
  202. ],
  203. sidebar: [
  204. body('isSidebarCollapsedMode').isBoolean(),
  205. body('isSidebarClosedAtDockMode').optional().isBoolean(),
  206. ],
  207. function: [
  208. body('isEnabledTimeline').isBoolean(),
  209. body('isEnabledAttachTitleHeader').isBoolean(),
  210. body('pageLimitationS').isInt().isInt({ min: 1, max: 1000 }),
  211. body('pageLimitationM').isInt().isInt({ min: 1, max: 1000 }),
  212. body('pageLimitationL').isInt().isInt({ min: 1, max: 1000 }),
  213. body('pageLimitationXL').isInt().isInt({ min: 1, max: 1000 }),
  214. body('isEnabledStaleNotification').isBoolean(),
  215. body('isAllReplyShown').isBoolean(),
  216. body('isSearchScopeChildrenAsDefault').isBoolean(),
  217. body('showPageSideAuthors').isBoolean(),
  218. ],
  219. CustomizePresentation: [
  220. body('isEnabledMarp').isBoolean(),
  221. ],
  222. customizeTitle: [
  223. body('customizeTitle').isString(),
  224. ],
  225. highlight: [
  226. body('highlightJsStyle').isString().isIn([
  227. 'github', 'github-gist', 'atom-one-light', 'xcode', 'vs', 'atom-one-dark', 'hybrid', 'monokai', 'tomorrow-night', 'vs2015',
  228. ]),
  229. body('highlightJsStyleBorder').isBoolean(),
  230. ],
  231. customizeScript: [
  232. body('customizeScript').isString(),
  233. ],
  234. customizeCss: [
  235. body('customizeCss').isString(),
  236. ],
  237. customizeNoscript: [
  238. body('customizeNoscript').isString(),
  239. ],
  240. logo: [
  241. body('isDefaultLogo').isBoolean().optional({ nullable: true }),
  242. body('customizedLogoSrc').isString().optional({ nullable: true }),
  243. ],
  244. };
  245. /**
  246. * @swagger
  247. *
  248. * /customize-setting:
  249. * get:
  250. * tags: [CustomizeSetting]
  251. * security:
  252. * - cookieAuth: []
  253. * summary: /customize-setting
  254. * description: Get customize parameters
  255. * responses:
  256. * 200:
  257. * description: params of customize
  258. * content:
  259. * application/json:
  260. * schema:
  261. * properties:
  262. * customizeParams:
  263. * type: object
  264. * description: customize params
  265. * $ref: '#/components/schemas/CustomizeSetting'
  266. */
  267. router.get('/', accessTokenParser([SCOPE.READ.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, async(req, res) => {
  268. const customizeParams = {
  269. isEnabledTimeline: await configManager.getConfig('customize:isEnabledTimeline'),
  270. isEnabledAttachTitleHeader: await configManager.getConfig('customize:isEnabledAttachTitleHeader'),
  271. pageLimitationS: await configManager.getConfig('customize:showPageLimitationS'),
  272. pageLimitationM: await configManager.getConfig('customize:showPageLimitationM'),
  273. pageLimitationL: await configManager.getConfig('customize:showPageLimitationL'),
  274. pageLimitationXL: await configManager.getConfig('customize:showPageLimitationXL'),
  275. isEnabledStaleNotification: await configManager.getConfig('customize:isEnabledStaleNotification'),
  276. isAllReplyShown: await configManager.getConfig('customize:isAllReplyShown'),
  277. showPageSideAuthors: await configManager.getConfig('customize:showPageSideAuthors'),
  278. isSearchScopeChildrenAsDefault: await configManager.getConfig('customize:isSearchScopeChildrenAsDefault'),
  279. isEnabledMarp: await configManager.getConfig('customize:isEnabledMarp'),
  280. styleName: await configManager.getConfig('customize:highlightJsStyle'),
  281. styleBorder: await configManager.getConfig('customize:highlightJsStyleBorder'),
  282. customizeTitle: await configManager.getConfig('customize:title'),
  283. customizeScript: await configManager.getConfig('customize:script'),
  284. customizeCss: await configManager.getConfig('customize:css'),
  285. customizeNoscript: await configManager.getConfig('customize:noscript'),
  286. };
  287. return res.apiv3({ customizeParams });
  288. });
  289. /**
  290. * @swagger
  291. *
  292. * /customize-setting/layout:
  293. * get:
  294. * tags: [CustomizeSetting]
  295. * security:
  296. * - cookieAuth: []
  297. * summary: /customize-setting/layout
  298. * description: Get layout
  299. * responses:
  300. * 200:
  301. * description: Succeeded to get layout
  302. * content:
  303. * application/json:
  304. * schema:
  305. * $ref: '#/components/schemas/CustomizeLayout'
  306. */
  307. router.get('/layout', accessTokenParser([SCOPE.READ.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, async(req, res) => {
  308. try {
  309. const isContainerFluid = await configManager.getConfig('customize:isContainerFluid');
  310. return res.apiv3({ isContainerFluid });
  311. }
  312. catch (err) {
  313. const msg = 'Error occurred in getting layout';
  314. logger.error('Error', err);
  315. return res.apiv3Err(new ErrorV3(msg, 'get-layout-failed'));
  316. }
  317. });
  318. /**
  319. * @swagger
  320. *
  321. * /customize-setting/layout:
  322. * put:
  323. * tags: [CustomizeSetting]
  324. * summary: /customize-setting/layout
  325. * description: Update layout
  326. * requestBody:
  327. * required: true
  328. * content:
  329. * application/json:
  330. * schema:
  331. * $ref: '#/components/schemas/CustomizeLayout'
  332. * responses:
  333. * 200:
  334. * description: Succeeded to update layout
  335. * content:
  336. * application/json:
  337. * schema:
  338. * type: object
  339. * properties:
  340. * customizedParams:
  341. * type: object
  342. * description: customized params
  343. * $ref: '#/components/schemas/CustomizeLayout'
  344. */
  345. router.put('/layout', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, addActivity,
  346. validator.layout, apiV3FormValidator,
  347. async(req, res) => {
  348. const requestParams = {
  349. 'customize:isContainerFluid': req.body.isContainerFluid,
  350. };
  351. try {
  352. await configManager.updateConfigs(requestParams);
  353. const customizedParams = {
  354. isContainerFluid: await configManager.getConfig('customize:isContainerFluid'),
  355. };
  356. const parameters = { action: SupportedAction.ACTION_ADMIN_LAYOUT_UPDATE };
  357. activityEvent.emit('update', res.locals.activity._id, parameters);
  358. return res.apiv3({ customizedParams });
  359. }
  360. catch (err) {
  361. const msg = 'Error occurred in updating layout';
  362. logger.error('Error', err);
  363. return res.apiv3Err(new ErrorV3(msg, 'update-layout-failed'));
  364. }
  365. });
  366. /**
  367. * @swagger
  368. *
  369. * /customize-setting/theme:
  370. * get:
  371. * tags: [CustomizeSetting]
  372. * security:
  373. * - cookieAuth: []
  374. * summary: /customize-setting/theme
  375. * description: Get theme
  376. * responses:
  377. * 200:
  378. * description: Succeeded to get layout
  379. * content:
  380. * application/json:
  381. * schema:
  382. * type: object
  383. * properties:
  384. * currentTheme:
  385. * type: string
  386. * description: The current theme name.
  387. * pluginThemesMetadatas:
  388. * type: array
  389. * description: Metadata for available plugin themes.
  390. * items:
  391. * $ref: '#/components/schemas/ThemesMetadata'
  392. */
  393. router.get('/theme', accessTokenParser([SCOPE.READ.ADMIN.CUSTOMIZE]), loginRequiredStrictly, async(req, res) => {
  394. try {
  395. const currentTheme = await configManager.getConfig('customize:theme');
  396. // retrieve plugin manifests
  397. const themePlugins = await GrowiPlugin.findEnabledPluginsByType(GrowiPluginType.Theme);
  398. const pluginThemesMetadatas = themePlugins
  399. .map(themePlugin => themePlugin.meta.themes)
  400. .flat();
  401. return res.apiv3({ currentTheme, pluginThemesMetadatas });
  402. }
  403. catch (err) {
  404. const msg = 'Error occurred in getting theme';
  405. logger.error('Error', err);
  406. return res.apiv3Err(new ErrorV3(msg, 'get-theme-failed'));
  407. }
  408. });
  409. /**
  410. * @swagger
  411. *
  412. * /customize-setting/theme:
  413. * put:
  414. * tags: [CustomizeSetting]
  415. * security:
  416. * - cookieAuth: []
  417. * summary: /customize-setting/theme
  418. * description: Update theme
  419. * requestBody:
  420. * required: true
  421. * content:
  422. * application/json:
  423. * schema:
  424. * $ref: '#/components/schemas/CustomizeTheme'
  425. * responses:
  426. * 200:
  427. * description: Succeeded to update theme
  428. * content:
  429. * application/json:
  430. * schema:
  431. * type: object
  432. * properties:
  433. * customizedParams:
  434. * $ref: '#/components/schemas/CustomizeTheme'
  435. */
  436. router.put('/theme', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, addActivity, validator.theme, apiV3FormValidator,
  437. async(req, res) => {
  438. const requestParams = {
  439. 'customize:theme': req.body.theme,
  440. };
  441. try {
  442. await configManager.updateConfigs(requestParams);
  443. const customizedParams = {
  444. theme: await configManager.getConfig('customize:theme'),
  445. };
  446. customizeService.initGrowiTheme();
  447. const parameters = { action: SupportedAction.ACTION_ADMIN_THEME_UPDATE };
  448. activityEvent.emit('update', res.locals.activity._id, parameters);
  449. return res.apiv3({ customizedParams });
  450. }
  451. catch (err) {
  452. const msg = 'Error occurred in updating theme';
  453. logger.error('Error', err);
  454. return res.apiv3Err(new ErrorV3(msg, 'update-theme-failed'));
  455. }
  456. });
  457. /**
  458. * @swagger
  459. *
  460. * /customize-setting/sidebar:
  461. * get:
  462. * tags: [CustomizeSetting]
  463. * security:
  464. * - cookieAuth: []
  465. * summary: /customize-setting/sidebar
  466. * description: Get sidebar
  467. * responses:
  468. * 200:
  469. * description: Succeeded to get sidebar
  470. * content:
  471. * application/json:
  472. * schema:
  473. * $ref: '#/components/schemas/CustomizeSidebar'
  474. */
  475. router.get('/sidebar', accessTokenParser([SCOPE.READ.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, async(req, res) => {
  476. try {
  477. const isSidebarCollapsedMode = await configManager.getConfig('customize:isSidebarCollapsedMode');
  478. const isSidebarClosedAtDockMode = await configManager.getConfig('customize:isSidebarClosedAtDockMode');
  479. return res.apiv3({ isSidebarCollapsedMode, isSidebarClosedAtDockMode });
  480. }
  481. catch (err) {
  482. const msg = 'Error occurred in getting sidebar';
  483. logger.error('Error', err);
  484. return res.apiv3Err(new ErrorV3(msg, 'get-sidebar-failed'));
  485. }
  486. });
  487. /**
  488. * @swagger
  489. *
  490. * /customize-setting/sidebar:
  491. * put:
  492. * tags: [CustomizeSetting]
  493. * security:
  494. * - cookieAuth: []
  495. * summary: /customize-setting/sidebar
  496. * description: Update sidebar
  497. * requestBody:
  498. * required: true
  499. * content:
  500. * application/json:
  501. * schema:
  502. * $ref: '#/components/schemas/CustomizeSidebar'
  503. * responses:
  504. * 200:
  505. * description: Succeeded to update sidebar
  506. * content:
  507. * application/json:
  508. * schema:
  509. * type: object
  510. * properties:
  511. * customizedParams:
  512. * $ref: '#/components/schemas/CustomizeSidebar'
  513. */
  514. router.put('/sidebar', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired,
  515. validator.sidebar, apiV3FormValidator, addActivity,
  516. async(req, res) => {
  517. const requestParams = {
  518. 'customize:isSidebarCollapsedMode': req.body.isSidebarCollapsedMode,
  519. 'customize:isSidebarClosedAtDockMode': req.body.isSidebarClosedAtDockMode,
  520. };
  521. try {
  522. await configManager.updateConfigs(requestParams);
  523. const customizedParams = {
  524. isSidebarCollapsedMode: await configManager.getConfig('customize:isSidebarCollapsedMode'),
  525. isSidebarClosedAtDockMode: await configManager.getConfig('customize:isSidebarClosedAtDockMode'),
  526. };
  527. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SIDEBAR_UPDATE });
  528. return res.apiv3({ customizedParams });
  529. }
  530. catch (err) {
  531. const msg = 'Error occurred in updating sidebar';
  532. logger.error('Error', err);
  533. return res.apiv3Err(new ErrorV3(msg, 'update-sidebar-failed'));
  534. }
  535. });
  536. /**
  537. * @swagger
  538. *
  539. * /customize-setting/function:
  540. * put:
  541. * tags: [CustomizeSetting]
  542. * security:
  543. * - cookieAuth: []
  544. * summary: /customize-setting/function
  545. * description: Update function
  546. * requestBody:
  547. * required: true
  548. * content:
  549. * application/json:
  550. * schema:
  551. * $ref: '#/components/schemas/CustomizeFunction'
  552. * responses:
  553. * 200:
  554. * description: Succeeded to update function
  555. * content:
  556. * application/json:
  557. * schema:
  558. * type: object
  559. * properties:
  560. * customizedParams:
  561. * $ref: '#/components/schemas/CustomizeFunction'
  562. */
  563. router.put('/function', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, addActivity,
  564. validator.function, apiV3FormValidator,
  565. async(req, res) => {
  566. const requestParams = {
  567. 'customize:isEnabledTimeline': req.body.isEnabledTimeline,
  568. 'customize:isEnabledAttachTitleHeader': req.body.isEnabledAttachTitleHeader,
  569. 'customize:showPageLimitationS': req.body.pageLimitationS,
  570. 'customize:showPageLimitationM': req.body.pageLimitationM,
  571. 'customize:showPageLimitationL': req.body.pageLimitationL,
  572. 'customize:showPageLimitationXL': req.body.pageLimitationXL,
  573. 'customize:isEnabledStaleNotification': req.body.isEnabledStaleNotification,
  574. 'customize:isAllReplyShown': req.body.isAllReplyShown,
  575. 'customize:isSearchScopeChildrenAsDefault': req.body.isSearchScopeChildrenAsDefault,
  576. 'customize:showPageSideAuthors': req.body.showPageSideAuthors,
  577. };
  578. try {
  579. await configManager.updateConfigs(requestParams);
  580. const customizedParams = {
  581. isEnabledTimeline: await configManager.getConfig('customize:isEnabledTimeline'),
  582. isEnabledAttachTitleHeader: await configManager.getConfig('customize:isEnabledAttachTitleHeader'),
  583. pageLimitationS: await configManager.getConfig('customize:showPageLimitationS'),
  584. pageLimitationM: await configManager.getConfig('customize:showPageLimitationM'),
  585. pageLimitationL: await configManager.getConfig('customize:showPageLimitationL'),
  586. pageLimitationXL: await configManager.getConfig('customize:showPageLimitationXL'),
  587. isEnabledStaleNotification: await configManager.getConfig('customize:isEnabledStaleNotification'),
  588. isAllReplyShown: await configManager.getConfig('customize:isAllReplyShown'),
  589. isSearchScopeChildrenAsDefault: await configManager.getConfig('customize:isSearchScopeChildrenAsDefault'),
  590. showPageSideAuthors: await configManager.getConfig('customize:showPageSideAuthors'),
  591. };
  592. const parameters = { action: SupportedAction.ACTION_ADMIN_FUNCTION_UPDATE };
  593. activityEvent.emit('update', res.locals.activity._id, parameters);
  594. return res.apiv3({ customizedParams });
  595. }
  596. catch (err) {
  597. const msg = 'Error occurred in updating function';
  598. logger.error('Error', err);
  599. return res.apiv3Err(new ErrorV3(msg, 'update-function-failed'));
  600. }
  601. });
  602. /**
  603. * @swagger
  604. *
  605. * /customize-setting/presentation:
  606. * put:
  607. * tags: [CustomizeSetting]
  608. * security:
  609. * - cookieAuth: []
  610. * summary: /customize-setting/presentation
  611. * description: Update presentation
  612. * requestBody:
  613. * required: true
  614. * content:
  615. * application/json:
  616. * schema:
  617. * $ref: '#/components/schemas/CustomizePresentation'
  618. * responses:
  619. * 200:
  620. * description: Succeeded to update presentation
  621. * content:
  622. * application/json:
  623. * schema:
  624. * type: object
  625. * properties:
  626. * customizedParams:
  627. * $ref: '#/components/schemas/CustomizePresentation'
  628. */
  629. router.put('/presentation', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, addActivity,
  630. validator.CustomizePresentation, apiV3FormValidator,
  631. async(req, res) => {
  632. const requestParams = {
  633. 'customize:isEnabledMarp': req.body.isEnabledMarp,
  634. };
  635. try {
  636. await configManager.updateConfigs(requestParams);
  637. const customizedParams = {
  638. isEnabledMarp: await configManager.getConfig('customize:isEnabledMarp'),
  639. };
  640. const parameters = { action: SupportedAction.ACTION_ADMIN_FUNCTION_UPDATE };
  641. activityEvent.emit('update', res.locals.activity._id, parameters);
  642. return res.apiv3({ customizedParams });
  643. }
  644. catch (err) {
  645. const msg = 'Error occurred in updating presentaion';
  646. logger.error('Error', err);
  647. return res.apiv3Err(new ErrorV3(msg, 'update-presentation-failed'));
  648. }
  649. });
  650. /**
  651. * @swagger
  652. *
  653. * /customize-setting/highlight:
  654. * put:
  655. * tags: [CustomizeSetting]
  656. * security:
  657. * - cookieAuth: []
  658. * summary: /customize-setting/highlight
  659. * description: Update highlight
  660. * requestBody:
  661. * required: true
  662. * content:
  663. * application/json:
  664. * schema:
  665. * $ref: '#/components/schemas/CustomizeHighlight'
  666. * responses:
  667. * 200:
  668. * description: Succeeded to update highlight
  669. * content:
  670. * application/json:
  671. * schema:
  672. * type: object
  673. * properties:
  674. * customizedParams:
  675. * $ref: '#/components/schemas/CustomizeHighlightResponse'
  676. */
  677. router.put('/highlight', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, addActivity,
  678. validator.highlight, apiV3FormValidator,
  679. async(req, res) => {
  680. const requestParams = {
  681. 'customize:highlightJsStyle': req.body.highlightJsStyle,
  682. 'customize:highlightJsStyleBorder': req.body.highlightJsStyleBorder,
  683. };
  684. try {
  685. await configManager.updateConfigs(requestParams);
  686. const customizedParams = {
  687. styleName: await configManager.getConfig('customize:highlightJsStyle'),
  688. styleBorder: await configManager.getConfig('customize:highlightJsStyleBorder'),
  689. };
  690. const parameters = { action: SupportedAction.ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE };
  691. activityEvent.emit('update', res.locals.activity._id, parameters);
  692. return res.apiv3({ customizedParams });
  693. }
  694. catch (err) {
  695. const msg = 'Error occurred in updating highlight';
  696. logger.error('Error', err);
  697. return res.apiv3Err(new ErrorV3(msg, 'update-highlight-failed'));
  698. }
  699. });
  700. /**
  701. * @swagger
  702. *
  703. * /customize-setting/customizeTitle:
  704. * put:
  705. * tags: [CustomizeSetting]
  706. * security:
  707. * - cookieAuth: []
  708. * summary: /customize-setting/customizeTitle
  709. * description: Update title
  710. * requestBody:
  711. * required: true
  712. * content:
  713. * application/json:
  714. * schema:
  715. * $ref: '#/components/schemas/CustomizeTitle'
  716. * responses:
  717. * 200:
  718. * description: Succeeded to update customizeTitle
  719. * content:
  720. * application/json:
  721. * schema:
  722. * type: object
  723. * properties:
  724. * customizedParams:
  725. * $ref: '#/components/schemas/CustomizeTitle'
  726. */
  727. router.put('/customize-title', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, addActivity,
  728. validator.customizeTitle, apiV3FormValidator,
  729. async(req, res) => {
  730. const requestParams = {
  731. 'customize:title': req.body.customizeTitle,
  732. };
  733. try {
  734. await configManager.updateConfigs(requestParams, { skipPubsub: true });
  735. crowi.customizeService.publishUpdatedMessage();
  736. const customizedParams = {
  737. customizeTitle: await configManager.getConfig('customize:title'),
  738. };
  739. customizeService.initCustomTitle();
  740. const parameters = { action: SupportedAction.ACTION_ADMIN_CUSTOM_TITLE_UPDATE };
  741. activityEvent.emit('update', res.locals.activity._id, parameters);
  742. return res.apiv3({ customizedParams });
  743. }
  744. catch (err) {
  745. const msg = 'Error occurred in updating customizeTitle';
  746. logger.error('Error', err);
  747. return res.apiv3Err(new ErrorV3(msg, 'update-customizeTitle-failed'));
  748. }
  749. });
  750. /**
  751. * @swagger
  752. *
  753. * /customize-setting/customize-noscript:
  754. * put:
  755. * tags: [CustomizeSetting]
  756. * security:
  757. * - cookieAuth: []
  758. * summary: /customize-setting/customize-noscript
  759. * description: Update noscript
  760. * requestBody:
  761. * required: true
  762. * content:
  763. * application/json:
  764. * schema:
  765. * $ref: '#/components/schemas/CustomizeNoscript'
  766. * responses:
  767. * 200:
  768. * description: Succeeded to update customize header
  769. * content:
  770. * application/json:
  771. * schema:
  772. * type: object
  773. * properties:
  774. * customizedParams:
  775. * $ref: '#/components/schemas/CustomizeNoscript'
  776. */
  777. router.put('/customize-noscript', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, addActivity,
  778. validator.customizeNoscript, apiV3FormValidator,
  779. async(req, res) => {
  780. const requestParams = {
  781. 'customize:noscript': req.body.customizeNoscript,
  782. };
  783. try {
  784. await configManager.updateConfigs(requestParams);
  785. const customizedParams = {
  786. customizeNoscript: await configManager.getConfig('customize:noscript'),
  787. };
  788. const parameters = { action: SupportedAction.ACTION_ADMIN_CUSTOM_NOSCRIPT_UPDATE };
  789. activityEvent.emit('update', res.locals.activity._id, parameters);
  790. return res.apiv3({ customizedParams });
  791. }
  792. catch (err) {
  793. const msg = 'Error occurred in updating customizeNoscript';
  794. logger.error('Error', err);
  795. return res.apiv3Err(new ErrorV3(msg, 'update-customizeNoscript-failed'));
  796. }
  797. });
  798. /**
  799. * @swagger
  800. *
  801. * /customize-setting/customize-css:
  802. * put:
  803. * tags: [CustomizeSetting]
  804. * security:
  805. * - cookieAuth: []
  806. * summary: /customize-setting/customize-css
  807. * description: Update customize css
  808. * requestBody:
  809. * required: true
  810. * content:
  811. * application/json:
  812. * schema:
  813. * $ref: '#/components/schemas/CustomizeCss'
  814. * responses:
  815. * 200:
  816. * description: Succeeded to update customize css
  817. * content:
  818. * application/json:
  819. * schema:
  820. * type: object
  821. * properties:
  822. * customizedParams:
  823. * $ref: '#/components/schemas/CustomizeCss'
  824. */
  825. router.put('/customize-css', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, addActivity,
  826. validator.customizeCss, apiV3FormValidator,
  827. async(req, res) => {
  828. const requestParams = {
  829. 'customize:css': req.body.customizeCss,
  830. };
  831. try {
  832. await configManager.updateConfigs(requestParams, { skipPubsub: true });
  833. crowi.customizeService.publishUpdatedMessage();
  834. const customizedParams = {
  835. customizeCss: await configManager.getConfig('customize:css'),
  836. };
  837. customizeService.initCustomCss();
  838. const parameters = { action: SupportedAction.ACTION_ADMIN_CUSTOM_CSS_UPDATE };
  839. activityEvent.emit('update', res.locals.activity._id, parameters);
  840. return res.apiv3({ customizedParams });
  841. }
  842. catch (err) {
  843. const msg = 'Error occurred in updating customizeCss';
  844. logger.error('Error', err);
  845. return res.apiv3Err(new ErrorV3(msg, 'update-customizeCss-failed'));
  846. }
  847. });
  848. /**
  849. * @swagger
  850. *
  851. * /customize-setting/customize-script:
  852. * put:
  853. * tags: [CustomizeSetting]
  854. * security:
  855. * - cookieAuth: []
  856. * summary: /customize-setting/customize-script
  857. * description: Update customize script
  858. * requestBody:
  859. * required: true
  860. * content:
  861. * application/json:
  862. * schema:
  863. * $ref: '#/components/schemas/CustomizeScript'
  864. * responses:
  865. * 200:
  866. * description: Succeeded to update customize script
  867. * content:
  868. * application/json:
  869. * schema:
  870. * type: object
  871. * properties:
  872. * customizedParams:
  873. * $ref: '#/components/schemas/CustomizeScript'
  874. */
  875. router.put('/customize-script', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, addActivity,
  876. validator.customizeScript, apiV3FormValidator,
  877. async(req, res) => {
  878. const requestParams = {
  879. 'customize:script': req.body.customizeScript,
  880. };
  881. try {
  882. await configManager.updateConfigs(requestParams);
  883. const customizedParams = {
  884. customizeScript: await configManager.getConfig('customize:script'),
  885. };
  886. const parameters = { action: SupportedAction.ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE };
  887. activityEvent.emit('update', res.locals.activity._id, parameters);
  888. return res.apiv3({ customizedParams });
  889. }
  890. catch (err) {
  891. const msg = 'Error occurred in updating customizeScript';
  892. logger.error('Error', err);
  893. return res.apiv3Err(new ErrorV3(msg, 'update-customizeScript-failed'));
  894. }
  895. });
  896. /**
  897. * @swagger
  898. *
  899. * /customize-setting/customize-logo:
  900. * put:
  901. * tags: [CustomizeSetting]
  902. * security:
  903. * - cookieAuth: []
  904. * summary: /customize-setting/customize-logo
  905. * description: Update customize logo
  906. * requestBody:
  907. * required: true
  908. * content:
  909. * application/json:
  910. * schema:
  911. * $ref: '#/components/schemas/CustomizeLogo'
  912. * responses:
  913. * 200:
  914. * description: Succeeded to update customize logo
  915. * content:
  916. * application/json:
  917. * schema:
  918. * type: object
  919. * properties:
  920. * customizedParams:
  921. * $ref: '#/components/schemas/CustomizeLogo'
  922. */
  923. router.put('/customize-logo', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired,
  924. validator.logo, apiV3FormValidator,
  925. async(req, res) => {
  926. const {
  927. isDefaultLogo,
  928. } = req.body;
  929. const requestParams = {
  930. 'customize:isDefaultLogo': isDefaultLogo,
  931. };
  932. try {
  933. await configManager.updateConfigs(requestParams);
  934. const customizedParams = {
  935. isDefaultLogo: await configManager.getConfig('customize:isDefaultLogo'),
  936. };
  937. return res.apiv3({ customizedParams });
  938. }
  939. catch (err) {
  940. const msg = 'Error occurred in updating customizeLogo';
  941. logger.error('Error', err);
  942. return res.apiv3Err(new ErrorV3(msg, 'update-customizeLogo-failed'));
  943. }
  944. });
  945. /**
  946. * @swagger
  947. *
  948. * /customize-setting/upload-brand-logo:
  949. * put:
  950. * tags: [CustomizeSetting]
  951. * security:
  952. * - cookieAuth: []
  953. * summary: /customize-setting/upload-brand-logo
  954. * description: Upload brand logo
  955. * requestBody:
  956. * required: true
  957. * content:
  958. * multipart/form-data:
  959. * schema:
  960. * type: object
  961. * properties:
  962. * file:
  963. * type: string
  964. * format: binary
  965. * responses:
  966. * 200:
  967. * description: Succeeded to upload brand logo
  968. * content:
  969. * application/json:
  970. * schema:
  971. * type: object
  972. * properties:
  973. * attachment:
  974. * allOf:
  975. * - $ref: '#/components/schemas/Attachment'
  976. * - type: object
  977. * properties:
  978. * creator:
  979. * type: string
  980. * page: {}
  981. * temporaryUrlExpiredAt: {}
  982. * temporaryUrlCached: {}
  983. */
  984. router.post('/upload-brand-logo',
  985. uploads.single('file'), accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, uploads.single('file'), validator.logo, apiV3FormValidator,
  986. async(req, res) => {
  987. if (req.file == null) {
  988. return res.apiv3Err(new ErrorV3('File error.', 'upload-brand-logo-failed'));
  989. }
  990. if (req.user == null) {
  991. return res.apiv3Err(new ErrorV3('param "user" must be set.', 'upload-brand-logo-failed'));
  992. }
  993. const file = req.file;
  994. // check type
  995. const acceptableFileType = /image\/.+/;
  996. if (!file.mimetype.match(acceptableFileType)) {
  997. const msg = 'File type error. Only image files is allowed to set as user picture.';
  998. return res.apiv3Err(new ErrorV3(msg, 'upload-brand-logo-failed'));
  999. }
  1000. // Check if previous attachment exists and remove it
  1001. const attachments = await Attachment.find({ attachmentType: AttachmentType.BRAND_LOGO });
  1002. if (attachments != null) {
  1003. await attachmentService.removeAllAttachments(attachments);
  1004. }
  1005. let attachment;
  1006. try {
  1007. attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.BRAND_LOGO);
  1008. }
  1009. catch (err) {
  1010. logger.error(err);
  1011. return res.apiv3Err(new ErrorV3(err.message, 'upload-brand-logo-failed'));
  1012. }
  1013. attachment.toObject({ virtuals: true });
  1014. return res.apiv3({ attachment });
  1015. });
  1016. /**
  1017. * @swagger
  1018. *
  1019. * /customize-setting/delete-brand-logo:
  1020. * delete:
  1021. * tags: [CustomizeSetting]
  1022. * security:
  1023. * - cookieAuth: []
  1024. * summary: /customize-setting/delete-brand-logo
  1025. * description: Delete brand logo
  1026. * responses:
  1027. * 200:
  1028. * description: Succeeded to delete brand logo
  1029. * content:
  1030. * application/json:
  1031. * schema:
  1032. * additionalProperties: false
  1033. */
  1034. router.delete('/delete-brand-logo', accessTokenParser([SCOPE.WRITE.ADMIN.CUSTOMIZE]), loginRequiredStrictly, adminRequired, async(req, res) => {
  1035. const attachments = await Attachment.find({ attachmentType: AttachmentType.BRAND_LOGO });
  1036. if (attachments == null) {
  1037. return res.apiv3Err(new ErrorV3('attachment not found', 'delete-brand-logo-failed'));
  1038. }
  1039. try {
  1040. await attachmentService.removeAllAttachments(attachments);
  1041. }
  1042. catch (err) {
  1043. logger.error(err);
  1044. return res.status(500).apiv3Err(new ErrorV3('Error while deleting logo', 'delete-brand-logo-failed'));
  1045. }
  1046. return res.apiv3({});
  1047. });
  1048. return router;
  1049. };