slack-integration-settings.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. const mongoose = require('mongoose');
  2. const express = require('express');
  3. const { body } = require('express-validator');
  4. const axios = require('axios');
  5. const urljoin = require('url-join');
  6. const loggerFactory = require('@alias/logger');
  7. const { getConnectionStatuses } = require('@growi/slack');
  8. const ErrorV3 = require('../../models/vo/error-apiv3');
  9. const logger = loggerFactory('growi:routes:apiv3:notification-setting');
  10. const router = express.Router();
  11. /**
  12. * @swagger
  13. * tags:
  14. * name: SlackIntegrationSettings
  15. */
  16. /**
  17. * @swagger
  18. *
  19. * components:
  20. * schemas:
  21. * BotType:
  22. * description: BotType
  23. * properties:
  24. * currentBotType:
  25. * type: string
  26. * SlackIntegration:
  27. * description: SlackIntegration
  28. * type: object
  29. * properties:
  30. * currentBotType:
  31. * type: string
  32. */
  33. module.exports = (crowi) => {
  34. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  35. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  36. const adminRequired = require('../../middlewares/admin-required')(crowi);
  37. const csrf = require('../../middlewares/csrf')(crowi);
  38. const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
  39. const SlackAppIntegration = mongoose.model('SlackAppIntegration');
  40. const validator = {
  41. BotType: [
  42. body('currentBotType').isString(),
  43. ],
  44. SlackIntegration: [
  45. body('currentBotType')
  46. .isIn(['officialBot', 'customBotWithoutProxy', 'customBotWithProxy']),
  47. ],
  48. NotificationTestToSlackWorkSpace: [
  49. body('channel').trim().not().isEmpty()
  50. .isString(),
  51. ],
  52. AccessTokens: [
  53. body('tokenGtoP').trim().not().isEmpty()
  54. .isString()
  55. .isLength({ min: 1 }),
  56. body('tokenPtoG').trim().not().isEmpty()
  57. .isString()
  58. .isLength({ min: 1 }),
  59. ],
  60. RelationTest: [
  61. body('slackappintegrationsId').isMongoId(),
  62. ],
  63. };
  64. async function resetAllBotSettings() {
  65. const params = {
  66. 'slackbot:currentBotType': null,
  67. 'slackbot:signingSecret': null,
  68. 'slackbot:token': null,
  69. };
  70. const { configManager } = crowi;
  71. // update config without publishing S2sMessage
  72. return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
  73. }
  74. async function updateSlackBotSettings(params) {
  75. const { configManager } = crowi;
  76. // update config without publishing S2sMessage
  77. return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
  78. }
  79. async function getConnectionStatusesFromProxy(tokens) {
  80. const csv = tokens.join(',');
  81. const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:serverUri');
  82. const result = await axios.get(urljoin(proxyUri, '/g2s/connection-status'), {
  83. headers: {
  84. 'x-growi-gtop-tokens': csv,
  85. },
  86. });
  87. return result.data;
  88. }
  89. async function postRelationTest(token) {
  90. const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:serverUri');
  91. const result = await axios.get(urljoin(proxyUri, '/g2s/relation-test'), {
  92. headers: {
  93. 'x-growi-gtop-tokens': token,
  94. },
  95. });
  96. return result.data;
  97. }
  98. /**
  99. * @swagger
  100. *
  101. * /slack-integration-settings/:
  102. * get:
  103. * tags: [SlackBotSettingParams]
  104. * operationId: getSlackBotSettingParams
  105. * summary: get /slack-integration
  106. * description: Get current settings and connection statuses.
  107. * responses:
  108. * 200:
  109. * description: Succeeded to get info.
  110. */
  111. router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
  112. const { configManager } = crowi;
  113. const currentBotType = configManager.getConfig('crowi', 'slackbot:currentBotType');
  114. // retrieve settings
  115. const settings = {};
  116. if (currentBotType === 'customBotWithoutProxy') {
  117. settings.slackSigningSecretEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:signingSecret');
  118. settings.slackBotTokenEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:token');
  119. settings.slackSigningSecret = configManager.getConfig('crowi', 'slackbot:signingSecret');
  120. settings.slackBotToken = configManager.getConfig('crowi', 'slackbot:token');
  121. }
  122. else {
  123. settings.proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:serverUri');
  124. settings.proxyUriEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:serverUri');
  125. }
  126. // retrieve connection statuses
  127. let connectionStatuses;
  128. if (currentBotType == null) {
  129. // TODO imple null action
  130. }
  131. else if (currentBotType === 'customBotWithoutProxy') {
  132. const token = settings.slackBotToken;
  133. // check the token is not null
  134. if (token != null) {
  135. try {
  136. connectionStatuses = await getConnectionStatuses([token]);
  137. }
  138. catch (error) {
  139. const msg = 'Error occured in getting connection statuses';
  140. logger.error('Error', error);
  141. return res.apiv3Err(new ErrorV3(msg, 'get-connection-failed'), 500);
  142. }
  143. }
  144. }
  145. else {
  146. const proxyUri = settings.proxyUri;
  147. if (proxyUri != null) {
  148. try {
  149. const slackAppIntegrations = await SlackAppIntegration.find();
  150. settings.slackAppIntegrations = slackAppIntegrations;
  151. if (slackAppIntegrations.length > 0) {
  152. const tokenGtoPs = slackAppIntegrations.map(slackAppIntegration => slackAppIntegration.tokenGtoP);
  153. connectionStatuses = (await getConnectionStatusesFromProxy(tokenGtoPs)).connectionStatuses;
  154. }
  155. }
  156. catch (error) {
  157. const msg = 'Error occured in getting connection statuses';
  158. logger.error('Error', error);
  159. return res.apiv3Err(new ErrorV3(msg, 'get-connection-failed'), 500);
  160. }
  161. }
  162. }
  163. return res.apiv3({ currentBotType, settings, connectionStatuses });
  164. });
  165. /**
  166. * @swagger
  167. *
  168. * /slack-integration-settings/:
  169. * put:
  170. * tags: [SlackIntegration]
  171. * operationId: putSlackIntegration
  172. * summary: put /slack-integration
  173. * description: Put SlackIntegration setting.
  174. * requestBody:
  175. * required: true
  176. * content:
  177. * application/json:
  178. * schema:
  179. * $ref: '#/components/schemas/SlackIntegration'
  180. * responses:
  181. * 200:
  182. * description: Succeeded to put Slack Integration setting.
  183. */
  184. router.put('/', accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.SlackIntegration, apiV3FormValidator, async(req, res) => {
  185. const { currentBotType } = req.body;
  186. const requestParams = {
  187. 'slackbot:currentBotType': currentBotType,
  188. };
  189. try {
  190. await updateSlackBotSettings(requestParams);
  191. crowi.slackBotService.publishUpdatedMessage();
  192. const slackIntegrationSettingsParams = {
  193. currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
  194. };
  195. return res.apiv3({ slackIntegrationSettingsParams });
  196. }
  197. catch (error) {
  198. const msg = 'Error occured in updating Slack bot setting';
  199. logger.error('Error', error);
  200. return res.apiv3Err(new ErrorV3(msg, 'update-SlackIntegrationSetting-failed'), 500);
  201. }
  202. });
  203. /**
  204. * @swagger
  205. *
  206. * /slack-integration-settings/bot-type/:
  207. * put:
  208. * tags: [botType]
  209. * operationId: putBotType
  210. * summary: /slack-integration/bot-type
  211. * description: Put botType setting.
  212. * requestBody:
  213. * required: true
  214. * content:
  215. * application/json:
  216. * schema:
  217. * $ref: '#/components/schemas/BotType'
  218. * responses:
  219. * 200:
  220. * description: Succeeded to put botType setting.
  221. */
  222. router.put('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.BotType, apiV3FormValidator, async(req, res) => {
  223. const { currentBotType } = req.body;
  224. await resetAllBotSettings();
  225. const requestParams = { 'slackbot:currentBotType': currentBotType };
  226. try {
  227. await updateSlackBotSettings(requestParams);
  228. crowi.slackBotService.publishUpdatedMessage();
  229. // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
  230. const slackBotTypeParam = { slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType') };
  231. return res.apiv3({ slackBotTypeParam });
  232. }
  233. catch (error) {
  234. const msg = 'Error occured in updating Custom bot setting';
  235. logger.error('Error', error);
  236. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  237. }
  238. });
  239. /**
  240. * @swagger
  241. *
  242. * /slack-integration/bot-type/:
  243. * delete:
  244. * tags: [botType]
  245. * operationId: deleteBotType
  246. * summary: /slack-integration/bot-type
  247. * description: Delete botType setting.
  248. * requestBody:
  249. * content:
  250. * application/json:
  251. * schema:
  252. * $ref: '#/components/schemas/BotType'
  253. * responses:
  254. * 200:
  255. * description: Succeeded to delete botType setting.
  256. */
  257. router.delete('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, csrf, apiV3FormValidator, async(req, res) => {
  258. await resetAllBotSettings();
  259. const params = { 'slackbot:currentBotType': null };
  260. try {
  261. await updateSlackBotSettings(params);
  262. crowi.slackBotService.publishUpdatedMessage();
  263. // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
  264. const slackBotTypeParam = { slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType') };
  265. return res.apiv3({ slackBotTypeParam });
  266. }
  267. catch (error) {
  268. const msg = 'Error occured in updating Custom bot setting';
  269. logger.error('Error', error);
  270. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  271. }
  272. });
  273. /**
  274. * @swagger
  275. *
  276. * /slack-integration-settings/without-proxy/update-settings/:
  277. * put:
  278. * tags: [UpdateWithoutProxySettings]
  279. * operationId: putWithoutProxySettings
  280. * summary: update customBotWithoutProxy settings
  281. * description: Update customBotWithoutProxy setting.
  282. * responses:
  283. * 200:
  284. * description: Succeeded to put CustomBotWithoutProxy setting.
  285. */
  286. router.put('/without-proxy/update-settings', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
  287. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  288. if (currentBotType !== 'customBotWithoutProxy') {
  289. const msg = 'Not CustomBotWithoutProxy';
  290. return res.apiv3Err(new ErrorV3(msg, 'not-customBotWithoutProxy'), 400);
  291. }
  292. const { slackSigningSecret, slackBotToken } = req.body;
  293. const requestParams = {
  294. 'slackbot:signingSecret': slackSigningSecret,
  295. 'slackbot:token': slackBotToken,
  296. };
  297. try {
  298. await updateSlackBotSettings(requestParams);
  299. crowi.slackBotService.publishUpdatedMessage();
  300. const customBotWithoutProxySettingParams = {
  301. slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
  302. slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
  303. };
  304. return res.apiv3({ customBotWithoutProxySettingParams });
  305. }
  306. catch (error) {
  307. const msg = 'Error occured in updating Custom bot setting';
  308. logger.error('Error', error);
  309. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  310. }
  311. });
  312. /**
  313. * @swagger
  314. *
  315. * /slack-integration-settings/access-tokens:
  316. * put:
  317. * tags: [SlackIntegration]
  318. * operationId: putAccessTokens
  319. * summary: /slack-integration
  320. * description: Generate accessTokens
  321. * responses:
  322. * 200:
  323. * description: Succeeded to update access tokens for slack
  324. */
  325. router.put('/access-tokens', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
  326. let checkTokens;
  327. let tokenGtoP;
  328. let tokenPtoG;
  329. let generateTokens;
  330. do {
  331. generateTokens = SlackAppIntegration.generateAccessToken();
  332. tokenGtoP = generateTokens[0];
  333. tokenPtoG = generateTokens[1];
  334. // eslint-disable-next-line no-await-in-loop
  335. checkTokens = await SlackAppIntegration.findOne({ $or: [{ tokenGtoP }, { tokenPtoG }] });
  336. } while (checkTokens != null);
  337. try {
  338. const slackAppTokens = await SlackAppIntegration.create({ tokenGtoP, tokenPtoG });
  339. return res.apiv3(slackAppTokens, 200);
  340. }
  341. catch (error) {
  342. const msg = 'Error occured in updating access token for slack app tokens';
  343. logger.error('Error', error);
  344. return res.apiv3Err(new ErrorV3(msg, 'update-slackAppTokens-failed'), 500);
  345. }
  346. });
  347. /**
  348. * @swagger
  349. *
  350. * /slack-integration-settings/slack-app-integration:
  351. * delete:
  352. * tags: [SlackIntegration]
  353. * operationId: deleteAccessTokens
  354. * summary: delete accessTokens
  355. * description: Delete accessTokens
  356. * responses:
  357. * 200:
  358. * description: Succeeded to delete access tokens for slack
  359. */
  360. router.delete('/slack-app-integration', validator.AccessTokens, apiV3FormValidator, async(req, res) => {
  361. const SlackAppIntegration = mongoose.model('SlackAppIntegration');
  362. const { tokenGtoP, tokenPtoG } = req.body;
  363. try {
  364. await SlackAppIntegration.findOneAndDelete({ tokenGtoP, tokenPtoG });
  365. }
  366. catch (error) {
  367. const msg = 'Error occured in deleting access token for slack app tokens';
  368. logger.error('Error', error);
  369. return res.apiv3Err(new ErrorV3(msg, 'update-slackAppTokens-failed'), 500);
  370. }
  371. });
  372. router.put('/proxy-uri', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
  373. const { proxyUri } = req.body;
  374. const requestParams = { 'slackbot:serverUri': proxyUri };
  375. try {
  376. await updateSlackBotSettings(requestParams);
  377. crowi.slackBotService.publishUpdatedMessage();
  378. return res.apiv3({});
  379. }
  380. catch (error) {
  381. const msg = 'Error occured in updating Custom bot setting';
  382. logger.error('Error', error);
  383. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  384. }
  385. });
  386. /**
  387. * @swagger
  388. *
  389. * /slack-integration-settings/with-proxy/relation-test:
  390. * post:
  391. * tags: [botType]
  392. * operationId: postRelationTest
  393. * summary: /slack-integration/bot-type
  394. * description: Delete botType setting.
  395. * requestBody:
  396. * content:
  397. * application/json:
  398. * schema:
  399. * properties:
  400. * slackappintegrationsId:
  401. * type: string
  402. * responses:
  403. * 200:
  404. * description: Succeeded to delete botType setting.
  405. */
  406. router.post('/with-proxy/relation-test', loginRequiredStrictly, adminRequired, csrf, validator.RelationTest, apiV3FormValidator, async(req, res) => {
  407. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  408. if (currentBotType === 'customBotWithoutProxy') {
  409. const msg = 'Not Proxy Type';
  410. return res.apiv3Err(new ErrorV3(msg, 'not-proxy-type'), 400);
  411. }
  412. const { slackappintegrationsId } = req.body;
  413. try {
  414. const slackAppIntegration = await SlackAppIntegration.findOne({ _id: slackappintegrationsId });
  415. if (slackAppIntegration == null) {
  416. const msg = 'Could not find SlackAppIntegration by id';
  417. return res.apiv3Err(new ErrorV3(msg, 'find-slackAppIntegration-failed'), 400);
  418. }
  419. const response = await postRelationTest(slackAppIntegration.tokenGtoP);
  420. return res.apiv3({ response });
  421. }
  422. catch (error) {
  423. const msg = 'Error occured in updating Custom bot setting';
  424. logger.error('Error', error);
  425. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  426. }
  427. });
  428. return router;
  429. };