app-settings.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  1. import {
  2. ConfigSource, toNonBlankString, toNonBlankStringOrUndefined,
  3. } from '@growi/core/dist/interfaces';
  4. import { ErrorV3 } from '@growi/core/dist/models';
  5. import { body } from 'express-validator';
  6. import { i18n } from '^/config/next-i18next.config';
  7. import { SupportedAction } from '~/interfaces/activity';
  8. import { accessTokenParser } from '~/server/middlewares/access-token-parser';
  9. import { configManager } from '~/server/service/config-manager';
  10. import { getTranslation } from '~/server/service/i18next';
  11. import loggerFactory from '~/utils/logger';
  12. import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
  13. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  14. const logger = loggerFactory('growi:routes:apiv3:app-settings');
  15. const { pathUtils } = require('@growi/core/dist/utils');
  16. const express = require('express');
  17. const router = express.Router();
  18. /**
  19. * @swagger
  20. *
  21. * components:
  22. * schemas:
  23. * AppSettingParams:
  24. * description: AppSettingParams
  25. * type: object
  26. * properties:
  27. * azureReferenceFileWithRelayMode:
  28. * type: boolean
  29. * example: false
  30. * azureUseOnlyEnvVars:
  31. * type: boolean
  32. * example: false
  33. * confidential:
  34. * type: string
  35. * description: confidential show on page header
  36. * example: 'GROWI'
  37. * envAzureClientId:
  38. * type: string
  39. * example: 'AZURE_CLIENT_ID'
  40. * envAzureClientSecret:
  41. * type: string
  42. * example: 'AZURE_CLIENT_SECRET'
  43. * envAzureStorageAccountName:
  44. * type: string
  45. * example: 'AZURE_STORAGE_ACCOUNT_NAME'
  46. * envAzureStorageContainerName:
  47. * type: string
  48. * example: 'AZURE_STORAGE_CONTAINER_NAME'
  49. * envFileUploadType:
  50. * type: string
  51. * example: 'mongodb'
  52. * envGcsApiKeyJsonPath:
  53. * type: string
  54. * example: 'GCS_API_KEY_JSON_PATH'
  55. * envGcsBucket:
  56. * type: string
  57. * example: 'GCS_BUCKET'
  58. * envGcsUploadNamespace:
  59. * type: string
  60. * example: 'GCS_UPLOAD_NAMESPACE'
  61. * envSiteUrl:
  62. * type: string
  63. * example: 'http://localhost:3000'
  64. * fileUpload:
  65. * type: boolean
  66. * example: true
  67. * fileUploadType:
  68. * type: string
  69. * example: 'local'
  70. * fromAddress:
  71. * type: string
  72. * example: info@growi.org
  73. * gcsApiKeyJsonPath:
  74. * type: string
  75. * example: 'GCS_API_KEY_JSON_PATH'
  76. * gcsBucket:
  77. * type: string
  78. * example: 'GCS_BUCKET'
  79. * gcsReferenceFileWithRelayMode:
  80. * type: boolean
  81. * example: false
  82. * gcsUploadNamespace:
  83. * type: string
  84. * example: 'GCS_UPLOAD_NAMESPACE'
  85. * gcsUseOnlyEnvVars:
  86. * type: boolean
  87. * example: false
  88. * globalLang:
  89. * type: string
  90. * example: 'ja_JP'
  91. * isAppSiteUrlHashed:
  92. * type: boolean
  93. * example: false
  94. * isEmailPublishedForNewUser:
  95. * type: boolean
  96. * example: true
  97. * isMaintenanceMode:
  98. * type: boolean
  99. * example: false
  100. * isQuestionnaireEnabled:
  101. * type: boolean
  102. * example: true
  103. * isV5Compatible:
  104. * type: boolean
  105. * example: true
  106. * s3AccessKeyId:
  107. * type: string
  108. * s3Bucket:
  109. * type: string
  110. * s3CustomEndpoint:
  111. * type: string
  112. * s3ReferenceFileWithRelayMode:
  113. * type: boolean
  114. * s3Region:
  115. * type: string
  116. * siteUrl:
  117. * type: string
  118. * siteUrlUseOnlyEnvVars:
  119. * type: boolean
  120. * smtpHost:
  121. * type: string
  122. * smtpPassword:
  123. * type: string
  124. * smtpPort:
  125. * type: string
  126. * smtpUser:
  127. * type: string
  128. * useOnlyEnvVarForFileUploadType:
  129. * type: boolean
  130. * AppSettingPutParams:
  131. * description: AppSettingPutParams
  132. * type: object
  133. * properties:
  134. * title:
  135. * type: string
  136. * description: title of the site
  137. * example: 'GROWI'
  138. * confidential:
  139. * type: string
  140. * description: confidential show on page header
  141. * example: 'GROWI'
  142. * globalLang:
  143. * type: string
  144. * description: global language
  145. * example: 'ja_JP'
  146. * isEmailPublishedForNewUser:
  147. * type: boolean
  148. * description: is email published for new user, or not
  149. * example: true
  150. * fileUpload:
  151. * type: boolean
  152. * description: is file upload enabled, or not
  153. * example: true
  154. * SiteUrlSettingParams:
  155. * description: SiteUrlSettingParams
  156. * type: object
  157. * properties:
  158. * siteUrl:
  159. * type: string
  160. * description: Site URL. e.g. https://example.com, https://example.com:8080
  161. * envSiteUrl:
  162. * type: string
  163. * description: environment variable 'APP_SITE_URL'
  164. * SmtpSettingParams:
  165. * description: SmtpSettingParams
  166. * type: object
  167. * properties:
  168. * smtpHost:
  169. * type: string
  170. * description: host name of client's smtp server
  171. * example: 'smtp.example.com'
  172. * smtpPort:
  173. * type: string
  174. * description: port of client's smtp server
  175. * example: '587'
  176. * smtpUser:
  177. * type: string
  178. * description: user name of client's smtp server
  179. * example: 'USER'
  180. * smtpPassword:
  181. * type: string
  182. * description: password of client's smtp server
  183. * example: 'PASSWORD'
  184. * fromAddress:
  185. * type: string
  186. * description: e-mail address
  187. * example: 'info@example.com'
  188. * SmtpSettingResponseParams:
  189. * description: SmtpSettingResponseParams
  190. * type: object
  191. * properties:
  192. * isMailerSetup:
  193. * type: boolean
  194. * description: is mailer setup, or not
  195. * example: true
  196. * smtpHost:
  197. * type: string
  198. * description: host name of client's smtp server
  199. * example: 'smtp.example.com'
  200. * smtpPort:
  201. * type: string
  202. * description: port of client's smtp server
  203. * example: '587'
  204. * smtpUser:
  205. * type: string
  206. * description: user name of client's smtp server
  207. * example: 'USER'
  208. * smtpPassword:
  209. * type: string
  210. * description: password of client's smtp server
  211. * example: 'PASSWORD'
  212. * fromAddress:
  213. * type: string
  214. * description: e-mail address
  215. * example: 'info@example.com'
  216. * SesSettingParams:
  217. * description: SesSettingParams
  218. * type: object
  219. * properties:
  220. * from:
  221. * type: string
  222. * description: e-mail address used as from address of mail which sent from GROWI app
  223. * example: 'info@growi.org'
  224. * transmissionMethod:
  225. * type: string
  226. * description: transmission method
  227. * example: 'ses'
  228. * sesAccessKeyId:
  229. * type: string
  230. * description: accesskey id for authentification of AWS
  231. * sesSecretAccessKey:
  232. * type: string
  233. * description: secret key for authentification of AWS
  234. * SesSettingResponseParams:
  235. * description: SesSettingParams
  236. * type: object
  237. * properties:
  238. * isMailerSetup:
  239. * type: boolean
  240. * description: is mailer setup, or not
  241. * example: true
  242. * from:
  243. * type: string
  244. * description: e-mail address used as from address of mail which sent from GROWI app
  245. * example: 'info@growi.org'
  246. * transmissionMethod:
  247. * type: string
  248. * description: transmission method
  249. * example: 'ses'
  250. * sesAccessKeyId:
  251. * type: string
  252. * description: accesskey id for authentification of AWS
  253. * sesSecretAccessKey:
  254. * type: string
  255. * description: secret key for authentification of AWS
  256. * FileUploadSettingParams:
  257. * description: FileUploadTypeParams
  258. * type: object
  259. * properties:
  260. * fileUploadType:
  261. * type: string
  262. * description: fileUploadType
  263. * s3Region:
  264. * type: string
  265. * description: region of AWS S3
  266. * s3CustomEndpoint:
  267. * type: string
  268. * description: custom endpoint of AWS S3
  269. * s3Bucket:
  270. * type: string
  271. * description: AWS S3 bucket name
  272. * s3AccessKeyId:
  273. * type: string
  274. * description: accesskey id for authentification of AWS
  275. * s3SecretAccessKey:
  276. * type: string
  277. * description: secret key for authentification of AWS
  278. * s3ReferenceFileWithRelayMode:
  279. * type: boolean
  280. * description: is enable internal stream system for s3 file request
  281. * gcsApiKeyJsonPath:
  282. * type: string
  283. * description: apiKeyJsonPath of gcp
  284. * gcsBucket:
  285. * type: string
  286. * description: bucket name of gcs
  287. * gcsUploadNamespace:
  288. * type: string
  289. * description: name space of gcs
  290. * gcsReferenceFileWithRelayMode:
  291. * type: boolean
  292. * description: is enable internal stream system for gcs file request
  293. * azureTenantId:
  294. * type: string
  295. * description: tenant id of azure
  296. * azureClientId:
  297. * type: string
  298. * description: client id of azure
  299. * azureClientSecret:
  300. * type: string
  301. * description: client secret of azure
  302. * azureStorageAccountName:
  303. * type: string
  304. * description: storage account name of azure
  305. * azureStorageContainerName:
  306. * type: string
  307. * description: storage container name of azure
  308. * azureReferenceFileWithRelayMode:
  309. * type: boolean
  310. * description: is enable internal stream system for azure file request
  311. * QuestionnaireSettingParams:
  312. * description: QuestionnaireSettingParams
  313. * type: object
  314. * properties:
  315. * isQuestionnaireEnabled:
  316. * type: boolean
  317. * description: is questionnaire enabled, or not
  318. * example: true
  319. * isAppSiteUrlHashed:
  320. * type: boolean
  321. * description: is app site url hashed, or not
  322. */
  323. /** @param {import('~/server/crowi').default} crowi Crowi instance */
  324. module.exports = (crowi) => {
  325. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  326. const adminRequired = require('../../middlewares/admin-required')(crowi);
  327. const addActivity = generateAddActivityMiddleware(crowi);
  328. const activityEvent = crowi.event('activity');
  329. const validator = {
  330. appSetting: [
  331. body('title').trim(),
  332. body('confidential'),
  333. body('globalLang').isIn(i18n.locales),
  334. body('isEmailPublishedForNewUser').isBoolean(),
  335. body('fileUpload').isBoolean(),
  336. ],
  337. siteUrlSetting: [
  338. // https://regex101.com/r/5Xef8V/1
  339. body('siteUrl').trim().matches(/^(https?:\/\/)/).isURL({ require_tld: false }),
  340. ],
  341. mailSetting: [
  342. body('fromAddress').trim().if(value => value !== '').isEmail(),
  343. body('transmissionMethod').isIn(['smtp', 'ses']),
  344. ],
  345. smtpSetting: [
  346. body('smtpHost').trim(),
  347. body('smtpPort').trim().if(value => value !== '').isPort(),
  348. body('smtpUser').trim(),
  349. body('smtpPassword').trim(),
  350. ],
  351. sesSetting: [
  352. body('sesAccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
  353. body('sesSecretAccessKey').trim(),
  354. ],
  355. fileUploadSetting: [
  356. body('fileUploadType').isIn(['aws', 'gcs', 'local', 'gridfs', 'azure']),
  357. body('gcsApiKeyJsonPath').trim(),
  358. body('gcsBucket').trim(),
  359. body('gcsUploadNamespace').trim(),
  360. body('gcsReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
  361. body('s3Region')
  362. .trim()
  363. .if(value => value !== '')
  364. .custom(async(value) => {
  365. const { t } = await getTranslation();
  366. if (!/^[a-z]+-[a-z]+-\d+$/.test(value)) {
  367. throw new Error(t('validation.aws_region'));
  368. }
  369. return true;
  370. }),
  371. body('s3CustomEndpoint')
  372. .trim()
  373. .if(value => value !== '')
  374. .custom(async(value) => {
  375. const { t } = await getTranslation();
  376. if (!/^(https?:\/\/[^/]+|)$/.test(value)) {
  377. throw new Error(t('validation.aws_custom_endpoint'));
  378. }
  379. return true;
  380. }),
  381. body('s3Bucket').trim(),
  382. body('s3AccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
  383. body('s3SecretAccessKey').trim(),
  384. body('s3ReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
  385. body('azureTenantId').trim(),
  386. body('azureClientId').trim(),
  387. body('azureClientSecret').trim(),
  388. body('azureStorageAccountName').trim(),
  389. body('azureStorageStorageName').trim(),
  390. body('azureReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
  391. ],
  392. questionnaireSettings: [
  393. body('isQuestionnaireEnabled').isBoolean(),
  394. body('isAppSiteUrlHashed').isBoolean(),
  395. ],
  396. pageBulkExportSettings: [
  397. body('isBulkExportPagesEnabled').isBoolean(),
  398. body('bulkExportDownloadExpirationSeconds').isInt(),
  399. ],
  400. maintenanceMode: [
  401. body('flag').isBoolean(),
  402. ],
  403. };
  404. /**
  405. * @swagger
  406. *
  407. * /app-settings:
  408. * get:
  409. * tags: [AppSettings]
  410. * security:
  411. * - bearer: []
  412. * - accessTokenInQuery: []
  413. * summary: /app-settings
  414. * description: get app setting params
  415. * responses:
  416. * 200:
  417. * description: Resources are available
  418. * content:
  419. * application/json:
  420. * schema:
  421. * properties:
  422. * appSettingsParams:
  423. * type: object
  424. * $ref: '#/components/schemas/AppSettingParams'
  425. */
  426. router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
  427. const appSettingsParams = {
  428. title: configManager.getConfig('app:title'),
  429. confidential: configManager.getConfig('app:confidential'),
  430. globalLang: configManager.getConfig('app:globalLang'),
  431. isEmailPublishedForNewUser: configManager.getConfig('customize:isEmailPublishedForNewUser'),
  432. fileUpload: configManager.getConfig('app:fileUpload'),
  433. useOnlyEnvVarsForIsBulkExportPagesEnabled: configManager.getConfig('env:useOnlyEnvVars:app:isBulkExportPagesEnabled'),
  434. isV5Compatible: configManager.getConfig('app:isV5Compatible'),
  435. siteUrl: configManager.getConfig('app:siteUrl'),
  436. siteUrlUseOnlyEnvVars: configManager.getConfig('env:useOnlyEnvVars:app:siteUrl'),
  437. envSiteUrl: configManager.getConfig('app:siteUrl', ConfigSource.env),
  438. isMailerSetup: crowi.mailService.isMailerSetup,
  439. fromAddress: configManager.getConfig('mail:from'),
  440. transmissionMethod: configManager.getConfig('mail:transmissionMethod'),
  441. smtpHost: configManager.getConfig('mail:smtpHost'),
  442. smtpPort: configManager.getConfig('mail:smtpPort'),
  443. smtpUser: configManager.getConfig('mail:smtpUser'),
  444. smtpPassword: configManager.getConfig('mail:smtpPassword'),
  445. sesAccessKeyId: configManager.getConfig('mail:sesAccessKeyId'),
  446. sesSecretAccessKey: configManager.getConfig('mail:sesSecretAccessKey'),
  447. fileUploadType: configManager.getConfig('app:fileUploadType'),
  448. envFileUploadType: configManager.getConfig('app:fileUploadType', ConfigSource.env),
  449. useOnlyEnvVarForFileUploadType: configManager.getConfig('env:useOnlyEnvVars:app:fileUploadType'),
  450. s3Region: configManager.getConfig('aws:s3Region'),
  451. s3CustomEndpoint: configManager.getConfig('aws:s3CustomEndpoint'),
  452. s3Bucket: configManager.getConfig('aws:s3Bucket'),
  453. s3AccessKeyId: configManager.getConfig('aws:s3AccessKeyId'),
  454. s3ReferenceFileWithRelayMode: configManager.getConfig('aws:referenceFileWithRelayMode'),
  455. gcsUseOnlyEnvVars: configManager.getConfig('env:useOnlyEnvVars:gcs'),
  456. gcsApiKeyJsonPath: configManager.getConfig('gcs:apiKeyJsonPath'),
  457. gcsBucket: configManager.getConfig('gcs:bucket'),
  458. gcsUploadNamespace: configManager.getConfig('gcs:uploadNamespace'),
  459. gcsReferenceFileWithRelayMode: configManager.getConfig('gcs:referenceFileWithRelayMode'),
  460. envGcsApiKeyJsonPath: configManager.getConfig('gcs:apiKeyJsonPath', ConfigSource.env),
  461. envGcsBucket: configManager.getConfig('gcs:bucket', ConfigSource.env),
  462. envGcsUploadNamespace: configManager.getConfig('gcs:uploadNamespace', ConfigSource.env),
  463. azureUseOnlyEnvVars: configManager.getConfig('env:useOnlyEnvVars:azure'),
  464. azureTenantId: configManager.getConfig('azure:tenantId', ConfigSource.db),
  465. azureClientId: configManager.getConfig('azure:clientId', ConfigSource.db),
  466. azureClientSecret: configManager.getConfig('azure:clientSecret', ConfigSource.db),
  467. azureStorageAccountName: configManager.getConfig('azure:storageAccountName', ConfigSource.db),
  468. azureStorageContainerName: configManager.getConfig('azure:storageContainerName', ConfigSource.db),
  469. azureReferenceFileWithRelayMode: configManager.getConfig('azure:referenceFileWithRelayMode'),
  470. envAzureTenantId: configManager.getConfig('azure:tenantId', ConfigSource.env),
  471. envAzureClientId: configManager.getConfig('azure:clientId', ConfigSource.env),
  472. envAzureClientSecret: configManager.getConfig('azure:clientSecret', ConfigSource.env),
  473. envAzureStorageAccountName: configManager.getConfig('azure:storageAccountName', ConfigSource.env),
  474. envAzureStorageContainerName: configManager.getConfig('azure:storageContainerName', ConfigSource.env),
  475. isEnabledPlugins: configManager.getConfig('plugin:isEnabledPlugins'),
  476. isQuestionnaireEnabled: configManager.getConfig('questionnaire:isQuestionnaireEnabled'),
  477. isAppSiteUrlHashed: configManager.getConfig('questionnaire:isAppSiteUrlHashed'),
  478. isMaintenanceMode: configManager.getConfig('app:isMaintenanceMode'),
  479. isBulkExportPagesEnabled: configManager.getConfig('app:isBulkExportPagesEnabled'),
  480. envIsBulkExportPagesEnabled: configManager.getConfig('app:isBulkExportPagesEnabled'),
  481. bulkExportDownloadExpirationSeconds: configManager.getConfig('app:bulkExportDownloadExpirationSeconds'),
  482. // TODO: remove this property when bulk export can be relased for cloud (https://redmine.weseek.co.jp/issues/163220)
  483. isBulkExportDisabledForCloud: configManager.getConfig('app:growiCloudUri') != null,
  484. };
  485. return res.apiv3({ appSettingsParams });
  486. });
  487. /**
  488. * @swagger
  489. *
  490. * /app-settings/app-setting:
  491. * put:
  492. * tags: [AppSettings]
  493. * security:
  494. * - cookieAuth: []
  495. * summary: /app-settings/app-setting
  496. * description: Update app setting
  497. * requestBody:
  498. * required: true
  499. * content:
  500. * application/json:
  501. * schema:
  502. * $ref: '#/components/schemas/AppSettingPutParams'
  503. * responses:
  504. * 200:
  505. * description: Succeeded to update app setting
  506. * content:
  507. * application/json:
  508. * schema:
  509. * type: object
  510. * properties:
  511. * appSettingParams:
  512. * type: object
  513. * $ref: '#/components/schemas/AppSettingPutParams'
  514. */
  515. router.put('/app-setting', loginRequiredStrictly, adminRequired, addActivity, validator.appSetting, apiV3FormValidator, async(req, res) => {
  516. const requestAppSettingParams = {
  517. 'app:title': req.body.title,
  518. 'app:confidential': req.body.confidential,
  519. 'app:globalLang': req.body.globalLang,
  520. 'customize:isEmailPublishedForNewUser': req.body.isEmailPublishedForNewUser,
  521. 'app:fileUpload': req.body.fileUpload,
  522. };
  523. try {
  524. await configManager.updateConfigs(requestAppSettingParams);
  525. const appSettingParams = {
  526. title: configManager.getConfig('app:title'),
  527. confidential: configManager.getConfig('app:confidential'),
  528. globalLang: configManager.getConfig('app:globalLang'),
  529. isEmailPublishedForNewUser: configManager.getConfig('customize:isEmailPublishedForNewUser'),
  530. fileUpload: configManager.getConfig('app:fileUpload'),
  531. };
  532. const parameters = { action: SupportedAction.ACTION_ADMIN_APP_SETTINGS_UPDATE };
  533. activityEvent.emit('update', res.locals.activity._id, parameters);
  534. return res.apiv3({ appSettingParams });
  535. }
  536. catch (err) {
  537. const msg = 'Error occurred in updating app setting';
  538. logger.error('Error', err);
  539. return res.apiv3Err(new ErrorV3(msg, 'update-appSetting-failed'));
  540. }
  541. });
  542. /**
  543. * @swagger
  544. *
  545. * /app-settings/site-url-setting:
  546. * put:
  547. * tags: [AppSettings]
  548. * security:
  549. * - cookieAuth: []
  550. * summary: /app-settings/site-url-setting
  551. * description: Update site url setting
  552. * requestBody:
  553. * required: true
  554. * content:
  555. * application/json:
  556. * schema:
  557. * $ref: '#/components/schemas/SiteUrlSettingParams'
  558. * responses:
  559. * 200:
  560. * description: Succeeded to update site url setting
  561. * content:
  562. * application/json:
  563. * schema:
  564. * type: object
  565. * properties:
  566. * siteUrlSettingParams:
  567. * type: object
  568. * properties:
  569. * siteUrl:
  570. * type: string
  571. * description: Site URL. e.g. https://example.com, https://example.com:3000
  572. * example: 'http://localhost:3000'
  573. */
  574. router.put('/site-url-setting', loginRequiredStrictly, adminRequired, addActivity, validator.siteUrlSetting, apiV3FormValidator, async(req, res) => {
  575. const useOnlyEnvVars = configManager.getConfig('env:useOnlyEnvVars:app:siteUrl');
  576. if (useOnlyEnvVars) {
  577. const msg = 'Updating the Site URL is prohibited on this system.';
  578. return res.apiv3Err(new ErrorV3(msg, 'update-siteUrlSetting-prohibited'));
  579. }
  580. const requestSiteUrlSettingParams = {
  581. 'app:siteUrl': pathUtils.removeTrailingSlash(req.body.siteUrl),
  582. };
  583. try {
  584. await configManager.updateConfigs(requestSiteUrlSettingParams);
  585. const siteUrlSettingParams = {
  586. siteUrl: configManager.getConfig('app:siteUrl'),
  587. };
  588. const parameters = { action: SupportedAction.ACTION_ADMIN_SITE_URL_UPDATE };
  589. activityEvent.emit('update', res.locals.activity._id, parameters);
  590. return res.apiv3({ siteUrlSettingParams });
  591. }
  592. catch (err) {
  593. const msg = 'Error occurred in updating site url setting';
  594. logger.error('Error', err);
  595. return res.apiv3Err(new ErrorV3(msg, 'update-siteUrlSetting-failed'));
  596. }
  597. });
  598. /**
  599. * send mail (Promise wrapper)
  600. */
  601. async function sendMailPromiseWrapper(smtpClient, options) {
  602. return new Promise((resolve, reject) => {
  603. smtpClient.sendMail(options, (err, res) => {
  604. if (err) {
  605. reject(err);
  606. }
  607. else {
  608. resolve(res);
  609. }
  610. });
  611. });
  612. }
  613. /**
  614. * validate mail setting send test mail
  615. */
  616. async function sendTestEmail(destinationAddress) {
  617. const { mailService } = crowi;
  618. if (!mailService.isMailerSetup) {
  619. throw Error('mailService is not setup');
  620. }
  621. const fromAddress = configManager.getConfig('mail:from');
  622. if (fromAddress == null) {
  623. throw Error('fromAddress is not setup');
  624. }
  625. const smtpHost = configManager.getConfig('mail:smtpHost');
  626. const smtpPort = configManager.getConfig('mail:smtpPort');
  627. const smtpUser = configManager.getConfig('mail:smtpUser');
  628. const smtpPassword = configManager.getConfig('mail:smtpPassword');
  629. const option = {
  630. host: smtpHost,
  631. port: smtpPort,
  632. };
  633. if (smtpUser && smtpPassword) {
  634. option.auth = {
  635. user: smtpUser,
  636. pass: smtpPassword,
  637. };
  638. }
  639. if (option.port === 465) {
  640. option.secure = true;
  641. }
  642. const smtpClient = mailService.createSMTPClient(option);
  643. logger.debug('mailer setup for validate SMTP setting', smtpClient);
  644. const mailOptions = {
  645. from: fromAddress,
  646. to: destinationAddress,
  647. subject: 'Wiki管理設定のアップデートによるメール通知',
  648. text: 'このメールは、WikiのSMTP設定のアップデートにより送信されています。',
  649. };
  650. await sendMailPromiseWrapper(smtpClient, mailOptions);
  651. }
  652. const updateMailSettinConfig = async function(requestMailSettingParams) {
  653. const {
  654. mailService,
  655. } = crowi;
  656. // update config without publishing S2sMessage
  657. await configManager.updateConfigs(requestMailSettingParams, { skipPubsub: true });
  658. await mailService.initialize();
  659. mailService.publishUpdatedMessage();
  660. return {
  661. isMailerSetup: mailService.isMailerSetup,
  662. fromAddress: configManager.getConfig('mail:from'),
  663. smtpHost: configManager.getConfig('mail:smtpHost'),
  664. smtpPort: configManager.getConfig('mail:smtpPort'),
  665. smtpUser: configManager.getConfig('mail:smtpUser'),
  666. smtpPassword: configManager.getConfig('mail:smtpPassword'),
  667. sesAccessKeyId: configManager.getConfig('mail:sesAccessKeyId'),
  668. sesSecretAccessKey: configManager.getConfig('mail:sesSecretAccessKey'),
  669. };
  670. };
  671. /**
  672. * @swagger
  673. *
  674. * /app-settings/smtp-setting:
  675. * put:
  676. * tags: [AppSettings]
  677. * security:
  678. * - cookieAuth: []
  679. * summary: /app-settings/smtp-setting
  680. * description: Update smtp setting
  681. * requestBody:
  682. * required: true
  683. * content:
  684. * application/json:
  685. * schema:
  686. * $ref: '#/components/schemas/SmtpSettingParams'
  687. * responses:
  688. * 200:
  689. * description: Succeeded to update smtp setting
  690. * content:
  691. * application/json:
  692. * schema:
  693. * type: object
  694. * properties:
  695. * mailSettingParams:
  696. * type: object
  697. * $ref: '#/components/schemas/SmtpSettingResponseParams'
  698. */
  699. router.put('/smtp-setting', loginRequiredStrictly, adminRequired, addActivity, validator.smtpSetting, apiV3FormValidator, async(req, res) => {
  700. const requestMailSettingParams = {
  701. 'mail:from': req.body.fromAddress,
  702. 'mail:transmissionMethod': req.body.transmissionMethod,
  703. 'mail:smtpHost': req.body.smtpHost,
  704. 'mail:smtpPort': req.body.smtpPort,
  705. 'mail:smtpUser': req.body.smtpUser,
  706. 'mail:smtpPassword': req.body.smtpPassword,
  707. };
  708. try {
  709. const mailSettingParams = await updateMailSettinConfig(requestMailSettingParams);
  710. const parameters = { action: SupportedAction.ACTION_ADMIN_MAIL_SMTP_UPDATE };
  711. activityEvent.emit('update', res.locals.activity._id, parameters);
  712. return res.apiv3({ mailSettingParams });
  713. }
  714. catch (err) {
  715. const msg = 'Error occurred in updating smtp setting';
  716. logger.error('Error', err);
  717. return res.apiv3Err(new ErrorV3(msg, 'update-smtpSetting-failed'));
  718. }
  719. });
  720. /**
  721. * @swagger
  722. *
  723. * /app-settings/smtp-test:
  724. * post:
  725. * tags: [AppSettings]
  726. * security:
  727. * - cookieAuth: []
  728. * summary: /app-settings/smtp-setting
  729. * description: Send test mail for smtp
  730. * responses:
  731. * 200:
  732. * description: Succeeded to send test mail for smtp
  733. * content:
  734. * application/json:
  735. * schema:
  736. * type: object
  737. * description: Empty object
  738. */
  739. router.post('/smtp-test', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
  740. const { t } = await getTranslation({ lang: req.user.lang });
  741. try {
  742. await sendTestEmail(req.user.email);
  743. const parameters = { action: SupportedAction.ACTION_ADMIN_MAIL_TEST_SUBMIT };
  744. activityEvent.emit('update', res.locals.activity._id, parameters);
  745. return res.apiv3({});
  746. }
  747. catch (err) {
  748. const msg = t('validation.failed_to_send_a_test_email');
  749. logger.error('Error', err);
  750. logger.debug('Error validate mail setting: ', err);
  751. return res.apiv3Err(new ErrorV3(msg, 'send-email-with-smtp-failed'));
  752. }
  753. });
  754. /**
  755. * @swagger
  756. *
  757. * /app-settings/ses-setting:
  758. * put:
  759. * tags: [AppSettings]
  760. * security:
  761. * - cookieAuth: []
  762. * summary: /app-settings/ses-setting
  763. * description: Update ses setting
  764. * requestBody:
  765. * required: true
  766. * content:
  767. * application/json:
  768. * schema:
  769. * $ref: '#/components/schemas/SesSettingParams'
  770. * responses:
  771. * 200:
  772. * description: Succeeded to update ses setting
  773. * content:
  774. * application/json:
  775. * schema:
  776. * $ref: '#/components/schemas/SesSettingResponseParams'
  777. */
  778. router.put('/ses-setting', loginRequiredStrictly, adminRequired, addActivity, validator.sesSetting, apiV3FormValidator, async(req, res) => {
  779. const { mailService } = crowi;
  780. const requestSesSettingParams = {
  781. 'mail:from': req.body.fromAddress,
  782. 'mail:transmissionMethod': req.body.transmissionMethod,
  783. 'mail:sesAccessKeyId': req.body.sesAccessKeyId,
  784. 'mail:sesSecretAccessKey': req.body.sesSecretAccessKey,
  785. };
  786. let mailSettingParams;
  787. try {
  788. mailSettingParams = await updateMailSettinConfig(requestSesSettingParams);
  789. }
  790. catch (err) {
  791. const msg = 'Error occurred in updating ses setting';
  792. logger.error('Error', err);
  793. return res.apiv3Err(new ErrorV3(msg, 'update-ses-setting-failed'));
  794. }
  795. await mailService.initialize();
  796. mailService.publishUpdatedMessage();
  797. const parameters = { action: SupportedAction.ACTION_ADMIN_MAIL_SES_UPDATE };
  798. activityEvent.emit('update', res.locals.activity._id, parameters);
  799. return res.apiv3({ mailSettingParams });
  800. });
  801. /**
  802. * @swagger
  803. *
  804. * /app-settings/file-upload-settings:
  805. * put:
  806. * tags: [AppSettings]
  807. * security:
  808. * - cookieAuth: []
  809. * summary: /app-settings/file-upload-setting
  810. * description: Update fileUploadSetting
  811. * requestBody:
  812. * required: true
  813. * content:
  814. * application/json:
  815. * schema:
  816. * $ref: '#/components/schemas/FileUploadSettingParams'
  817. * responses:
  818. * 200:
  819. * description: Succeeded to update fileUploadSetting
  820. * content:
  821. * application/json:
  822. * schema:
  823. * type: object
  824. * properties:
  825. * responseParams:
  826. * type: object
  827. * $ref: '#/components/schemas/FileUploadSettingParams'
  828. */
  829. // eslint-disable-next-line max-len
  830. router.put('/file-upload-setting', loginRequiredStrictly, adminRequired, addActivity, validator.fileUploadSetting, apiV3FormValidator, async(req, res) => {
  831. const { fileUploadType } = req.body;
  832. const requestParams = {
  833. 'app:fileUploadType': fileUploadType,
  834. };
  835. if (fileUploadType === 'gcs') {
  836. requestParams['gcs:apiKeyJsonPath'] = req.body.gcsApiKeyJsonPath;
  837. requestParams['gcs:bucket'] = req.body.gcsBucket;
  838. requestParams['gcs:uploadNamespace'] = req.body.gcsUploadNamespace;
  839. requestParams['gcs:referenceFileWithRelayMode'] = req.body.gcsReferenceFileWithRelayMode;
  840. }
  841. if (fileUploadType === 'aws') {
  842. try {
  843. requestParams['aws:s3Region'] = toNonBlankString(req.body.s3Region);
  844. requestParams['aws:s3Bucket'] = toNonBlankString(req.body.s3Bucket);
  845. }
  846. catch (err) {
  847. logger.warn('AWS S3 region or bucket name is not updated because of invalid value', err);
  848. }
  849. requestParams['aws:s3CustomEndpoint'] = toNonBlankStringOrUndefined(req.body.s3CustomEndpoint);
  850. requestParams['aws:s3AccessKeyId'] = toNonBlankStringOrUndefined(req.body.s3AccessKeyId);
  851. requestParams['aws:s3SecretAccessKey'] = toNonBlankStringOrUndefined(req.body.s3SecretAccessKey);
  852. requestParams['aws:referenceFileWithRelayMode'] = req.body.s3ReferenceFileWithRelayMode;
  853. }
  854. if (fileUploadType === 'azure') {
  855. requestParams['azure:tenantId'] = req.body.azureTenantId;
  856. requestParams['azure:clientId'] = req.body.azureClientId;
  857. requestParams['azure:clientSecret'] = req.body.azureClientSecret;
  858. requestParams['azure:storageAccountName'] = req.body.azureStorageAccountName;
  859. requestParams['azure:storageContainerName'] = req.body.azureStorageContainerName;
  860. requestParams['azure:referenceFileWithRelayMode'] = req.body.azureReferenceFileWithRelayMode;
  861. }
  862. try {
  863. await configManager.updateConfigs(requestParams, { skipPubsub: true, removeIfUndefined: true });
  864. await crowi.setUpFileUpload(true);
  865. crowi.fileUploaderSwitchService.publishUpdatedMessage();
  866. const responseParams = {
  867. fileUploadType: configManager.getConfig('app:fileUploadType'),
  868. };
  869. if (fileUploadType === 'gcs') {
  870. responseParams.gcsApiKeyJsonPath = configManager.getConfig('gcs:apiKeyJsonPath');
  871. responseParams.gcsBucket = configManager.getConfig('gcs:bucket');
  872. responseParams.gcsUploadNamespace = configManager.getConfig('gcs:uploadNamespace');
  873. responseParams.gcsReferenceFileWithRelayMode = configManager.getConfig('gcs:referenceFileWithRelayMode ');
  874. }
  875. if (fileUploadType === 'aws') {
  876. responseParams.s3Region = configManager.getConfig('aws:s3Region');
  877. responseParams.s3CustomEndpoint = configManager.getConfig('aws:s3CustomEndpoint');
  878. responseParams.s3Bucket = configManager.getConfig('aws:s3Bucket');
  879. responseParams.s3AccessKeyId = configManager.getConfig('aws:s3AccessKeyId');
  880. responseParams.s3ReferenceFileWithRelayMode = configManager.getConfig('aws:referenceFileWithRelayMode');
  881. }
  882. if (fileUploadType === 'azure') {
  883. responseParams.azureTenantId = configManager.getConfig('azure:tenantId');
  884. responseParams.azureClientId = configManager.getConfig('azure:clientId');
  885. responseParams.azureClientSecret = configManager.getConfig('azure:clientSecret');
  886. responseParams.azureStorageAccountName = configManager.getConfig('azure:storageAccountName');
  887. responseParams.azureStorageContainerName = configManager.getConfig('azure:storageContainerName');
  888. responseParams.azureReferenceFileWithRelayMode = configManager.getConfig('azure:referenceFileWithRelayMode');
  889. }
  890. const parameters = { action: SupportedAction.ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE };
  891. activityEvent.emit('update', res.locals.activity._id, parameters);
  892. return res.apiv3({ responseParams });
  893. }
  894. catch (err) {
  895. const msg = 'Error occurred in updating fileUploadType';
  896. logger.error('Error', err);
  897. return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
  898. }
  899. });
  900. /**
  901. * @swagger
  902. *
  903. * /app-settings/questionnaire-settings:
  904. * put:
  905. * tags: [AppSettings]
  906. * security:
  907. * - cookieAuth: []
  908. * summary: /app-settings/questionnaire-settings
  909. * description: Update QuestionnaireSetting
  910. * requestBody:
  911. * required: true
  912. * content:
  913. * application/json:
  914. * schema:
  915. * $ref: '#/components/schemas/QuestionnaireSettingParams'
  916. * responses:
  917. * 200:
  918. * description: Succeeded to update QuestionnaireSetting
  919. * content:
  920. * application/json:
  921. * schema:
  922. * type: object
  923. * properties:
  924. * responseParams:
  925. * type: object
  926. * $ref: '#/components/schemas/QuestionnaireSettingParams'
  927. */
  928. // eslint-disable-next-line max-len
  929. router.put('/questionnaire-settings', loginRequiredStrictly, adminRequired, addActivity, validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
  930. const { isQuestionnaireEnabled, isAppSiteUrlHashed } = req.body;
  931. const requestParams = {
  932. 'questionnaire:isQuestionnaireEnabled': isQuestionnaireEnabled,
  933. 'questionnaire:isAppSiteUrlHashed': isAppSiteUrlHashed,
  934. };
  935. try {
  936. await configManager.updateConfigs(requestParams, { skipPubsub: true });
  937. const responseParams = {
  938. isQuestionnaireEnabled: configManager.getConfig('questionnaire:isQuestionnaireEnabled'),
  939. isAppSiteUrlHashed: configManager.getConfig('questionnaire:isAppSiteUrlHashed'),
  940. };
  941. const parameters = { action: SupportedAction.ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE };
  942. activityEvent.emit('update', res.locals.activity._id, parameters);
  943. return res.apiv3({ responseParams });
  944. }
  945. catch (err) {
  946. const msg = 'Error occurred in updating questionnaire settings';
  947. logger.error('Error', err);
  948. return res.apiv3Err(new ErrorV3(msg, 'update-questionnaire-settings-failed'));
  949. }
  950. });
  951. router.put('/page-bulk-export-settings', loginRequiredStrictly, adminRequired, addActivity, validator.pageBulkExportSettings, apiV3FormValidator,
  952. async(req, res) => {
  953. const requestParams = {
  954. 'app:isBulkExportPagesEnabled': req.body.isBulkExportPagesEnabled,
  955. 'app:bulkExportDownloadExpirationSeconds': req.body.bulkExportDownloadExpirationSeconds,
  956. };
  957. try {
  958. await configManager.updateConfigs(requestParams, { skipPubsub: true });
  959. const responseParams = {
  960. isBulkExportPagesEnabled: configManager.getConfig('app:isBulkExportPagesEnabled'),
  961. bulkExportDownloadExpirationSeconds: configManager.getConfig('app:bulkExportDownloadExpirationSeconds'),
  962. };
  963. const parameters = { action: SupportedAction.ACTION_ADMIN_APP_SETTINGS_UPDATE };
  964. activityEvent.emit('update', res.locals.activity._id, parameters);
  965. return res.apiv3({ responseParams });
  966. }
  967. catch (err) {
  968. const msg = 'Error occurred in updating page bulk export settings';
  969. logger.error('Error', err);
  970. return res.apiv3Err(new ErrorV3(msg, 'update-page-bulk-export-settings-failed'));
  971. }
  972. });
  973. /**
  974. * @swagger
  975. *
  976. * /app-settings/v5-schema-migration:
  977. * post:
  978. * tags: [AppSettings]
  979. * security:
  980. * - bearer: []
  981. * - accessTokenInQuery: []
  982. * summary: AccessToken supported.
  983. * description: Update V5SchemaMigration
  984. * responses:
  985. * 200:
  986. * description: Succeeded to get V5SchemaMigration
  987. * content:
  988. * application/json:
  989. * schema:
  990. * type: object
  991. * properties:
  992. * isV5Compatible:
  993. * type: boolean
  994. * description: is V5 compatible, or not
  995. * example: true
  996. */
  997. router.post('/v5-schema-migration', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
  998. const isMaintenanceMode = crowi.appService.isMaintenanceMode();
  999. if (!isMaintenanceMode) {
  1000. return res.apiv3Err(new ErrorV3('GROWI is not maintenance mode. To import data, please activate the maintenance mode first.', 'not_maintenance_mode'));
  1001. }
  1002. const isV5Compatible = configManager.getConfig('app:isV5Compatible');
  1003. try {
  1004. if (!isV5Compatible) {
  1005. // This method throws and emit socketIo event when error occurs
  1006. crowi.pageService.normalizeAllPublicPages();
  1007. }
  1008. }
  1009. catch (err) {
  1010. return res.apiv3Err(new ErrorV3(`Failed to migrate pages: ${err.message}`), 500);
  1011. }
  1012. return res.apiv3({ isV5Compatible });
  1013. });
  1014. /**
  1015. * @swagger
  1016. *
  1017. * /app-settings/maintenance-mode:
  1018. * post:
  1019. * tags: [AppSettings]
  1020. * security:
  1021. * - bearer: []
  1022. * - accessTokenInQuery: []
  1023. * summary: AccessToken supported.
  1024. * description: Update MaintenanceMode
  1025. * requestBody:
  1026. * content:
  1027. * application/json:
  1028. * schema:
  1029. * type: object
  1030. * properties:
  1031. * flag:
  1032. * type: boolean
  1033. * description: flag for maintenance mode
  1034. * responses:
  1035. * 200:
  1036. * description: Succeeded to update MaintenanceMode
  1037. * content:
  1038. * application/json:
  1039. * schema:
  1040. * type: object
  1041. * properties:
  1042. * flag:
  1043. * type: boolean
  1044. * description: true if maintenance mode is enabled
  1045. * example: true
  1046. */
  1047. // eslint-disable-next-line max-len
  1048. router.post('/maintenance-mode', accessTokenParser, loginRequiredStrictly, adminRequired, addActivity, validator.maintenanceMode, apiV3FormValidator, async(req, res) => {
  1049. const { flag } = req.body;
  1050. const parameters = {};
  1051. try {
  1052. if (flag) {
  1053. await crowi.appService.startMaintenanceMode();
  1054. Object.assign(parameters, { action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_ENABLED });
  1055. }
  1056. else {
  1057. await crowi.appService.endMaintenanceMode();
  1058. Object.assign(parameters, { action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_DISABLED });
  1059. }
  1060. }
  1061. catch (err) {
  1062. logger.error(err);
  1063. if (flag) {
  1064. res.apiv3Err(new ErrorV3('Failed to start maintenance mode', 'failed_to_start_maintenance_mode'), 500);
  1065. }
  1066. else {
  1067. res.apiv3Err(new ErrorV3('Failed to end maintenance mode', 'failed_to_end_maintenance_mode'), 500);
  1068. }
  1069. }
  1070. if ('action' in parameters) {
  1071. activityEvent.emit('update', res.locals.activity._id, parameters);
  1072. }
  1073. res.apiv3({ flag });
  1074. });
  1075. return router;
  1076. };