slack-integration-settings.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. const mongoose = require('mongoose');
  2. const express = require('express');
  3. const { body, query } = require('express-validator');
  4. const axios = require('axios');
  5. const urljoin = require('url-join');
  6. const loggerFactory = require('@alias/logger');
  7. const { getConnectionStatuses, testToSlack, sendSuccessMessage } = require('@growi/slack');
  8. const ErrorV3 = require('../../models/vo/error-apiv3');
  9. const logger = loggerFactory('growi:routes:apiv3:slack-integration-settings');
  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. proxyUri: [
  49. body('proxyUri').if(value => value !== '').trim().matches(/^(https?:\/\/)/)
  50. .isURL({ require_tld: false }),
  51. ],
  52. RelationTest: [
  53. body('slackAppIntegrationId').isMongoId(),
  54. body('channel').trim().isString(),
  55. ],
  56. deleteIntegration: [
  57. query('integrationIdToDelete').isMongoId(),
  58. ],
  59. SlackChannel: [
  60. body('channel').trim().not().isEmpty()
  61. .isString(),
  62. ],
  63. };
  64. async function resetAllBotSettings() {
  65. const params = {
  66. 'slackbot:currentBotType': null,
  67. 'slackbot:signingSecret': null,
  68. 'slackbot:token': null,
  69. 'slackbot:proxyServerUri': null,
  70. };
  71. const { configManager } = crowi;
  72. // update config without publishing S2sMessage
  73. return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
  74. }
  75. async function updateSlackBotSettings(params) {
  76. const { configManager } = crowi;
  77. // update config without publishing S2sMessage
  78. return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
  79. }
  80. async function getConnectionStatusesFromProxy(tokens) {
  81. const csv = tokens.join(',');
  82. const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
  83. const result = await axios.get(urljoin(proxyUri, '/g2s/connection-status'), {
  84. headers: {
  85. 'x-growi-gtop-tokens': csv,
  86. },
  87. });
  88. return result.data;
  89. }
  90. async function postRelationTest(token) {
  91. const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
  92. const result = await axios.get(urljoin(proxyUri, '/g2s/relation-test'), {
  93. headers: {
  94. 'x-growi-gtop-tokens': token,
  95. },
  96. });
  97. return result.data;
  98. }
  99. /**
  100. * @swagger
  101. *
  102. * /slack-integration-settings/:
  103. * get:
  104. * tags: [SlackBotSettingParams]
  105. * operationId: getSlackBotSettingParams
  106. * summary: get /slack-integration
  107. * description: Get current settings and connection statuses.
  108. * responses:
  109. * 200:
  110. * description: Succeeded to get info.
  111. */
  112. router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
  113. const { configManager } = crowi;
  114. const currentBotType = configManager.getConfig('crowi', 'slackbot:currentBotType');
  115. // retrieve settings
  116. const settings = {};
  117. if (currentBotType === 'customBotWithoutProxy') {
  118. settings.slackSigningSecretEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:signingSecret');
  119. settings.slackBotTokenEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:token');
  120. settings.slackSigningSecret = configManager.getConfig('crowi', 'slackbot:signingSecret');
  121. settings.slackBotToken = configManager.getConfig('crowi', 'slackbot:token');
  122. }
  123. else {
  124. settings.proxyServerUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
  125. settings.proxyUriEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:proxyServerUri');
  126. }
  127. // retrieve connection statuses
  128. let connectionStatuses;
  129. if (currentBotType == null) {
  130. // TODO imple null action
  131. }
  132. else if (currentBotType === 'customBotWithoutProxy') {
  133. const token = settings.slackBotToken;
  134. // check the token is not null
  135. if (token != null) {
  136. try {
  137. connectionStatuses = await getConnectionStatuses([token]);
  138. }
  139. catch (error) {
  140. const msg = 'Error occured in getting connection statuses';
  141. logger.error('Error', error);
  142. return res.apiv3Err(new ErrorV3(msg, 'get-connection-failed'), 500);
  143. }
  144. }
  145. }
  146. else {
  147. try {
  148. const slackAppIntegrations = await SlackAppIntegration.find();
  149. settings.slackAppIntegrations = slackAppIntegrations;
  150. }
  151. catch (error) {
  152. const msg = 'Error occured in getting connection statuses';
  153. logger.error('Error', error);
  154. return res.apiv3Err(new ErrorV3(msg, 'get-connection-failed'), 500);
  155. }
  156. const proxyServerUri = settings.proxyServerUri;
  157. if (proxyServerUri != null) {
  158. try {
  159. if (settings.slackAppIntegrations.length > 0) {
  160. const tokenGtoPs = settings.slackAppIntegrations.map(slackAppIntegration => slackAppIntegration.tokenGtoP);
  161. connectionStatuses = (await getConnectionStatusesFromProxy(tokenGtoPs)).connectionStatuses;
  162. }
  163. }
  164. catch (error) {
  165. const msg = 'Incorrect Proxy URL';
  166. logger.error('Error', error);
  167. return res.apiv3Err(new ErrorV3(msg, 'test-connection-failed'), 400);
  168. }
  169. }
  170. }
  171. return res.apiv3({ currentBotType, settings, connectionStatuses });
  172. });
  173. /**
  174. * @swagger
  175. *
  176. * /slack-integration-settings/:
  177. * put:
  178. * tags: [SlackIntegration]
  179. * operationId: putSlackIntegration
  180. * summary: put /slack-integration
  181. * description: Put SlackIntegration setting.
  182. * requestBody:
  183. * required: true
  184. * content:
  185. * application/json:
  186. * schema:
  187. * $ref: '#/components/schemas/SlackIntegration'
  188. * responses:
  189. * 200:
  190. * description: Succeeded to put Slack Integration setting.
  191. */
  192. router.put('/', accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.SlackIntegration, apiV3FormValidator, async(req, res) => {
  193. const { currentBotType } = req.body;
  194. const requestParams = {
  195. 'slackbot:currentBotType': currentBotType,
  196. };
  197. try {
  198. await updateSlackBotSettings(requestParams);
  199. crowi.slackBotService.publishUpdatedMessage();
  200. const slackIntegrationSettingsParams = {
  201. currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
  202. };
  203. return res.apiv3({ slackIntegrationSettingsParams });
  204. }
  205. catch (error) {
  206. const msg = 'Error occured in updating Slack bot setting';
  207. logger.error('Error', error);
  208. return res.apiv3Err(new ErrorV3(msg, 'update-SlackIntegrationSetting-failed'), 500);
  209. }
  210. });
  211. /**
  212. * @swagger
  213. *
  214. * /slack-integration-settings/bot-type/:
  215. * put:
  216. * tags: [botType]
  217. * operationId: putBotType
  218. * summary: /slack-integration/bot-type
  219. * description: Put botType setting.
  220. * requestBody:
  221. * required: true
  222. * content:
  223. * application/json:
  224. * schema:
  225. * $ref: '#/components/schemas/BotType'
  226. * responses:
  227. * 200:
  228. * description: Succeeded to put botType setting.
  229. */
  230. router.put('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.BotType, apiV3FormValidator, async(req, res) => {
  231. const { currentBotType } = req.body;
  232. await resetAllBotSettings();
  233. const requestParams = { 'slackbot:currentBotType': currentBotType };
  234. try {
  235. await updateSlackBotSettings(requestParams);
  236. crowi.slackBotService.publishUpdatedMessage();
  237. // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
  238. const slackBotTypeParam = { slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType') };
  239. return res.apiv3({ slackBotTypeParam });
  240. }
  241. catch (error) {
  242. const msg = 'Error occured in updating Custom bot setting';
  243. logger.error('Error', error);
  244. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  245. }
  246. });
  247. /**
  248. * @swagger
  249. *
  250. * /slack-integration/bot-type/:
  251. * delete:
  252. * tags: [botType]
  253. * operationId: deleteBotType
  254. * summary: /slack-integration/bot-type
  255. * description: Delete botType setting.
  256. * requestBody:
  257. * content:
  258. * application/json:
  259. * schema:
  260. * $ref: '#/components/schemas/BotType'
  261. * responses:
  262. * 200:
  263. * description: Succeeded to delete botType setting.
  264. */
  265. router.delete('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, csrf, apiV3FormValidator, async(req, res) => {
  266. await resetAllBotSettings();
  267. const params = { 'slackbot:currentBotType': null };
  268. try {
  269. await updateSlackBotSettings(params);
  270. crowi.slackBotService.publishUpdatedMessage();
  271. // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
  272. const slackBotTypeParam = { slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType') };
  273. return res.apiv3({ slackBotTypeParam });
  274. }
  275. catch (error) {
  276. const msg = 'Error occured in updating Custom bot setting';
  277. logger.error('Error', error);
  278. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  279. }
  280. });
  281. /**
  282. * @swagger
  283. *
  284. * /slack-integration-settings/without-proxy/update-settings/:
  285. * put:
  286. * tags: [UpdateWithoutProxySettings]
  287. * operationId: putWithoutProxySettings
  288. * summary: update customBotWithoutProxy settings
  289. * description: Update customBotWithoutProxy setting.
  290. * responses:
  291. * 200:
  292. * description: Succeeded to put CustomBotWithoutProxy setting.
  293. */
  294. router.put('/without-proxy/update-settings', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
  295. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  296. if (currentBotType !== 'customBotWithoutProxy') {
  297. const msg = 'Not CustomBotWithoutProxy';
  298. return res.apiv3Err(new ErrorV3(msg, 'not-customBotWithoutProxy'), 400);
  299. }
  300. const { slackSigningSecret, slackBotToken } = req.body;
  301. const requestParams = {
  302. 'slackbot:signingSecret': slackSigningSecret,
  303. 'slackbot:token': slackBotToken,
  304. };
  305. try {
  306. await updateSlackBotSettings(requestParams);
  307. crowi.slackBotService.publishUpdatedMessage();
  308. const customBotWithoutProxySettingParams = {
  309. slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
  310. slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
  311. };
  312. return res.apiv3({ customBotWithoutProxySettingParams });
  313. }
  314. catch (error) {
  315. const msg = 'Error occured in updating Custom bot setting';
  316. logger.error('Error', error);
  317. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  318. }
  319. });
  320. /**
  321. * @swagger
  322. *
  323. * /slack-integration-settings/slack-app-integrations:
  324. * put:
  325. * tags: [SlackIntegration]
  326. * operationId: putSlackAppIntegrations
  327. * summary: /slack-integration
  328. * description: Generate SlackAppIntegrations
  329. * responses:
  330. * 200:
  331. * description: Succeeded to create slack app integration
  332. */
  333. router.put('/slack-app-integrations', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
  334. // TODO: refactering generateAccessTokens by GW-6100
  335. let checkTokens;
  336. let tokenGtoP;
  337. let tokenPtoG;
  338. let generateTokens;
  339. do {
  340. generateTokens = SlackAppIntegration.generateAccessToken();
  341. tokenGtoP = generateTokens[0];
  342. tokenPtoG = generateTokens[1];
  343. // eslint-disable-next-line no-await-in-loop
  344. checkTokens = await SlackAppIntegration.findOne({ $or: [{ tokenGtoP }, { tokenPtoG }] });
  345. } while (checkTokens != null);
  346. try {
  347. const slackAppTokens = await SlackAppIntegration.create({ tokenGtoP, tokenPtoG });
  348. return res.apiv3(slackAppTokens, 200);
  349. }
  350. catch (error) {
  351. const msg = 'Error occured in updating access token for slack app tokens';
  352. logger.error('Error', error);
  353. return res.apiv3Err(new ErrorV3(msg, 'update-slackAppTokens-failed'), 500);
  354. }
  355. });
  356. // TODO: add swagger by GW-6161
  357. // TODO: refactering generateAccessTokens by GW-6100
  358. router.put('/regenerate-tokens', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
  359. const { slackAppIntegrationId } = req.body;
  360. try {
  361. const generateTokens = SlackAppIntegration.generateAccessToken();
  362. const newTokenGtoP = generateTokens[0];
  363. const newTokenPtoG = generateTokens[1];
  364. await SlackAppIntegration.findOneAndUpdate({ _id: slackAppIntegrationId }, { tokenGtoP: newTokenGtoP, tokenPtoG: newTokenPtoG });
  365. return res.apiv3({});
  366. }
  367. catch (error) {
  368. const msg = 'Error occured in updating access token for slack app tokens';
  369. logger.error('Error', error);
  370. return res.apiv3Err(new ErrorV3(msg, 'update-slackAppTokens-failed'), 500);
  371. }
  372. });
  373. /**
  374. * @swagger
  375. *
  376. * /slack-integration-settings/slack-app-integration:
  377. * delete:
  378. * tags: [SlackIntegration]
  379. * operationId: deleteAccessTokens
  380. * summary: delete accessTokens
  381. * description: Delete accessTokens
  382. * responses:
  383. * 200:
  384. * description: Succeeded to delete access tokens for slack
  385. */
  386. router.delete('/slack-app-integration', validator.deleteIntegration, apiV3FormValidator, async(req, res) => {
  387. const SlackAppIntegration = mongoose.model('SlackAppIntegration');
  388. const { integrationIdToDelete } = req.query;
  389. try {
  390. const response = await SlackAppIntegration.findOneAndDelete({ _id: integrationIdToDelete });
  391. return res.apiv3({ response });
  392. }
  393. catch (error) {
  394. const msg = 'Error occured in deleting access token for slack app tokens';
  395. logger.error('Error', error);
  396. return res.apiv3Err(new ErrorV3(msg, 'update-slackAppTokens-failed'), 500);
  397. }
  398. });
  399. router.put('/proxy-uri', loginRequiredStrictly, adminRequired, csrf, validator.proxyUri, apiV3FormValidator, async(req, res) => {
  400. const { proxyUri } = req.body;
  401. const requestParams = { 'slackbot:proxyServerUri': proxyUri };
  402. try {
  403. await updateSlackBotSettings(requestParams);
  404. crowi.slackBotService.publishUpdatedMessage();
  405. return res.apiv3({});
  406. }
  407. catch (error) {
  408. const msg = 'Error occured in updating Custom bot setting';
  409. logger.error('Error', error);
  410. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  411. }
  412. });
  413. /**
  414. * @swagger
  415. *
  416. * /slack-integration-settings/with-proxy/relation-test:
  417. * post:
  418. * tags: [botType]
  419. * operationId: postRelationTest
  420. * summary: /slack-integration/bot-type
  421. * description: Delete botType setting.
  422. * requestBody:
  423. * content:
  424. * application/json:
  425. * schema:
  426. * properties:
  427. * slackAppIntegrationId:
  428. * type: string
  429. * responses:
  430. * 200:
  431. * description: Succeeded to delete botType setting.
  432. */
  433. router.post('/with-proxy/relation-test', loginRequiredStrictly, adminRequired, csrf, validator.RelationTest, apiV3FormValidator, async(req, res) => {
  434. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  435. if (currentBotType === 'customBotWithoutProxy') {
  436. const msg = 'Not Proxy Type';
  437. return res.apiv3Err(new ErrorV3(msg, 'not-proxy-type'), 400);
  438. }
  439. const { slackAppIntegrationId } = req.body;
  440. let slackBotToken;
  441. try {
  442. const slackAppIntegration = await SlackAppIntegration.findOne({ _id: slackAppIntegrationId });
  443. if (slackAppIntegration == null) {
  444. const msg = 'Could not find SlackAppIntegration by id';
  445. return res.apiv3Err(new ErrorV3(msg, 'find-slackAppIntegration-failed'), 400);
  446. }
  447. const result = await postRelationTest(slackAppIntegration.tokenGtoP);
  448. slackBotToken = result.slackBotToken;
  449. if (slackBotToken == null) {
  450. const msg = 'Could not find slackBotToken by relation';
  451. return res.apiv3Err(new ErrorV3(msg, 'find-slackBotToken-failed'), 400);
  452. }
  453. }
  454. catch (error) {
  455. logger.error('Error', error);
  456. return res.apiv3Err(new ErrorV3(`Error occured while testing. Cause: ${error.message}`, 'test-failed', error.stack));
  457. }
  458. const { channel } = req.body;
  459. const appSiteURL = crowi.configManager.getConfig('crowi', 'app:siteUrl');
  460. try {
  461. await sendSuccessMessage(slackBotToken, channel, appSiteURL);
  462. }
  463. catch (error) {
  464. return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
  465. }
  466. });
  467. /**
  468. * @swagger
  469. *
  470. * /slack-integration-settings/without-proxy/test:
  471. * post:
  472. * tags: [botType]
  473. * operationId: postTest
  474. * summary: test the connection
  475. * description: Test the connection with slack work space.
  476. * requestBody:
  477. * content:
  478. * application/json:
  479. * schema:
  480. * properties:
  481. * testChannel:
  482. * type: string
  483. * responses:
  484. * 200:
  485. * description: Succeeded to connect to slack work space.
  486. */
  487. router.post('/without-proxy/test', loginRequiredStrictly, adminRequired, csrf, validator.SlackChannel, apiV3FormValidator, async(req, res) => {
  488. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  489. if (currentBotType !== 'customBotWithoutProxy') {
  490. const msg = 'Select Without Proxy Type';
  491. return res.apiv3Err(new ErrorV3(msg, 'select-not-proxy-type'), 400);
  492. }
  493. const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
  494. try {
  495. await testToSlack(slackBotToken);
  496. }
  497. catch (error) {
  498. logger.error('Error', error);
  499. return res.apiv3Err(new ErrorV3(`Error occured while testing. Cause: ${error.message}`, 'test-failed', error.stack));
  500. }
  501. const { channel } = req.body;
  502. const appSiteURL = crowi.configManager.getConfig('crowi', 'app:siteUrl');
  503. try {
  504. await sendSuccessMessage(slackBotToken, channel, appSiteURL);
  505. }
  506. catch (error) {
  507. return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
  508. }
  509. return res.apiv3();
  510. });
  511. return router;
  512. };