slack-integration-settings.js 19 KB

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