slack-integration-settings.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  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. const proxyServerUri = settings.proxyServerUri;
  148. if (proxyServerUri != null) {
  149. try {
  150. const slackAppIntegrations = await SlackAppIntegration.find();
  151. settings.slackAppIntegrations = slackAppIntegrations;
  152. if (slackAppIntegrations.length > 0) {
  153. const tokenGtoPs = slackAppIntegrations.map(slackAppIntegration => slackAppIntegration.tokenGtoP);
  154. connectionStatuses = (await getConnectionStatusesFromProxy(tokenGtoPs)).connectionStatuses;
  155. }
  156. }
  157. catch (error) {
  158. const msg = 'Error occured in getting connection statuses';
  159. logger.error('Error', error);
  160. return res.apiv3Err(new ErrorV3(msg, 'get-connection-failed'), 500);
  161. }
  162. }
  163. }
  164. return res.apiv3({ currentBotType, settings, connectionStatuses });
  165. });
  166. /**
  167. * @swagger
  168. *
  169. * /slack-integration-settings/:
  170. * put:
  171. * tags: [SlackIntegration]
  172. * operationId: putSlackIntegration
  173. * summary: put /slack-integration
  174. * description: Put SlackIntegration setting.
  175. * requestBody:
  176. * required: true
  177. * content:
  178. * application/json:
  179. * schema:
  180. * $ref: '#/components/schemas/SlackIntegration'
  181. * responses:
  182. * 200:
  183. * description: Succeeded to put Slack Integration setting.
  184. */
  185. router.put('/', accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.SlackIntegration, apiV3FormValidator, async(req, res) => {
  186. const { currentBotType } = req.body;
  187. const requestParams = {
  188. 'slackbot:currentBotType': currentBotType,
  189. };
  190. try {
  191. await updateSlackBotSettings(requestParams);
  192. crowi.slackBotService.publishUpdatedMessage();
  193. const slackIntegrationSettingsParams = {
  194. currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
  195. };
  196. return res.apiv3({ slackIntegrationSettingsParams });
  197. }
  198. catch (error) {
  199. const msg = 'Error occured in updating Slack bot setting';
  200. logger.error('Error', error);
  201. return res.apiv3Err(new ErrorV3(msg, 'update-SlackIntegrationSetting-failed'), 500);
  202. }
  203. });
  204. /**
  205. * @swagger
  206. *
  207. * /slack-integration-settings/bot-type/:
  208. * put:
  209. * tags: [botType]
  210. * operationId: putBotType
  211. * summary: /slack-integration/bot-type
  212. * description: Put botType setting.
  213. * requestBody:
  214. * required: true
  215. * content:
  216. * application/json:
  217. * schema:
  218. * $ref: '#/components/schemas/BotType'
  219. * responses:
  220. * 200:
  221. * description: Succeeded to put botType setting.
  222. */
  223. router.put('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.BotType, apiV3FormValidator, async(req, res) => {
  224. const { currentBotType } = req.body;
  225. await resetAllBotSettings();
  226. const requestParams = { 'slackbot:currentBotType': currentBotType };
  227. try {
  228. await updateSlackBotSettings(requestParams);
  229. crowi.slackBotService.publishUpdatedMessage();
  230. // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
  231. const slackBotTypeParam = { slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType') };
  232. return res.apiv3({ slackBotTypeParam });
  233. }
  234. catch (error) {
  235. const msg = 'Error occured in updating Custom bot setting';
  236. logger.error('Error', error);
  237. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  238. }
  239. });
  240. /**
  241. * @swagger
  242. *
  243. * /slack-integration/bot-type/:
  244. * delete:
  245. * tags: [botType]
  246. * operationId: deleteBotType
  247. * summary: /slack-integration/bot-type
  248. * description: Delete botType setting.
  249. * requestBody:
  250. * content:
  251. * application/json:
  252. * schema:
  253. * $ref: '#/components/schemas/BotType'
  254. * responses:
  255. * 200:
  256. * description: Succeeded to delete botType setting.
  257. */
  258. router.delete('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, csrf, apiV3FormValidator, async(req, res) => {
  259. await resetAllBotSettings();
  260. const params = { 'slackbot:currentBotType': null };
  261. try {
  262. await updateSlackBotSettings(params);
  263. crowi.slackBotService.publishUpdatedMessage();
  264. // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
  265. const slackBotTypeParam = { slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType') };
  266. return res.apiv3({ slackBotTypeParam });
  267. }
  268. catch (error) {
  269. const msg = 'Error occured in updating Custom bot setting';
  270. logger.error('Error', error);
  271. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  272. }
  273. });
  274. /**
  275. * @swagger
  276. *
  277. * /slack-integration-settings/without-proxy/update-settings/:
  278. * put:
  279. * tags: [UpdateWithoutProxySettings]
  280. * operationId: putWithoutProxySettings
  281. * summary: update customBotWithoutProxy settings
  282. * description: Update customBotWithoutProxy setting.
  283. * responses:
  284. * 200:
  285. * description: Succeeded to put CustomBotWithoutProxy setting.
  286. */
  287. router.put('/without-proxy/update-settings', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
  288. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  289. if (currentBotType !== 'customBotWithoutProxy') {
  290. const msg = 'Not CustomBotWithoutProxy';
  291. return res.apiv3Err(new ErrorV3(msg, 'not-customBotWithoutProxy'), 400);
  292. }
  293. const { slackSigningSecret, slackBotToken } = req.body;
  294. const requestParams = {
  295. 'slackbot:signingSecret': slackSigningSecret,
  296. 'slackbot:token': slackBotToken,
  297. };
  298. try {
  299. await updateSlackBotSettings(requestParams);
  300. crowi.slackBotService.publishUpdatedMessage();
  301. const customBotWithoutProxySettingParams = {
  302. slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
  303. slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
  304. };
  305. return res.apiv3({ customBotWithoutProxySettingParams });
  306. }
  307. catch (error) {
  308. const msg = 'Error occured in updating Custom bot setting';
  309. logger.error('Error', error);
  310. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  311. }
  312. });
  313. /**
  314. * @swagger
  315. *
  316. * /slack-integration-settings/slack-app-integrations:
  317. * put:
  318. * tags: [SlackIntegration]
  319. * operationId: putSlackAppIntegrations
  320. * summary: /slack-integration
  321. * description: Generate SlackAppIntegrations
  322. * responses:
  323. * 200:
  324. * description: Succeeded to create slack app integration
  325. */
  326. router.put('/slack-app-integrations', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
  327. let checkTokens;
  328. let tokenGtoP;
  329. let tokenPtoG;
  330. let generateTokens;
  331. do {
  332. generateTokens = SlackAppIntegration.generateAccessToken();
  333. tokenGtoP = generateTokens[0];
  334. tokenPtoG = generateTokens[1];
  335. // eslint-disable-next-line no-await-in-loop
  336. checkTokens = await SlackAppIntegration.findOne({ $or: [{ tokenGtoP }, { tokenPtoG }] });
  337. } while (checkTokens != null);
  338. try {
  339. const slackAppTokens = await SlackAppIntegration.create({ tokenGtoP, tokenPtoG });
  340. return res.apiv3(slackAppTokens, 200);
  341. }
  342. catch (error) {
  343. const msg = 'Error occured in updating access token for slack app tokens';
  344. logger.error('Error', error);
  345. return res.apiv3Err(new ErrorV3(msg, 'update-slackAppTokens-failed'), 500);
  346. }
  347. });
  348. /**
  349. * @swagger
  350. *
  351. * /slack-integration-settings/slack-app-integration:
  352. * delete:
  353. * tags: [SlackIntegration]
  354. * operationId: deleteAccessTokens
  355. * summary: delete accessTokens
  356. * description: Delete accessTokens
  357. * responses:
  358. * 200:
  359. * description: Succeeded to delete access tokens for slack
  360. */
  361. router.delete('/slack-app-integration', validator.deleteIntegration, apiV3FormValidator, async(req, res) => {
  362. const SlackAppIntegration = mongoose.model('SlackAppIntegration');
  363. const { integrationIdToDelete } = req.query;
  364. try {
  365. const response = await SlackAppIntegration.findOneAndDelete({ _id: integrationIdToDelete });
  366. return res.apiv3({ response });
  367. }
  368. catch (error) {
  369. const msg = 'Error occured in deleting access token for slack app tokens';
  370. logger.error('Error', error);
  371. return res.apiv3Err(new ErrorV3(msg, 'update-slackAppTokens-failed'), 500);
  372. }
  373. });
  374. router.put('/proxy-uri', loginRequiredStrictly, adminRequired, csrf, validator.proxyUri, apiV3FormValidator, async(req, res) => {
  375. const { proxyUri } = req.body;
  376. const requestParams = { 'slackbot:proxyServerUri': proxyUri };
  377. try {
  378. await updateSlackBotSettings(requestParams);
  379. crowi.slackBotService.publishUpdatedMessage();
  380. return res.apiv3({});
  381. }
  382. catch (error) {
  383. const msg = 'Error occured in updating Custom bot setting';
  384. logger.error('Error', error);
  385. return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
  386. }
  387. });
  388. /**
  389. * @swagger
  390. *
  391. * /slack-integration-settings/with-proxy/relation-test:
  392. * post:
  393. * tags: [botType]
  394. * operationId: postRelationTest
  395. * summary: /slack-integration/bot-type
  396. * description: Delete botType setting.
  397. * requestBody:
  398. * content:
  399. * application/json:
  400. * schema:
  401. * properties:
  402. * slackAppIntegrationId:
  403. * type: string
  404. * responses:
  405. * 200:
  406. * description: Succeeded to delete botType setting.
  407. */
  408. router.post('/with-proxy/relation-test', loginRequiredStrictly, adminRequired, csrf, validator.RelationTest, apiV3FormValidator, async(req, res) => {
  409. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  410. if (currentBotType === 'customBotWithoutProxy') {
  411. const msg = 'Not Proxy Type';
  412. return res.apiv3Err(new ErrorV3(msg, 'not-proxy-type'), 400);
  413. }
  414. const { slackAppIntegrationId } = req.body;
  415. let slackBotToken;
  416. try {
  417. const slackAppIntegration = await SlackAppIntegration.findOne({ _id: slackAppIntegrationId });
  418. if (slackAppIntegration == null) {
  419. const msg = 'Could not find SlackAppIntegration by id';
  420. return res.apiv3Err(new ErrorV3(msg, 'find-slackAppIntegration-failed'), 400);
  421. }
  422. const result = await postRelationTest(slackAppIntegration.tokenGtoP);
  423. slackBotToken = result.slackBotToken;
  424. if (slackBotToken == null) {
  425. const msg = 'Could not find slackBotToken by relation';
  426. return res.apiv3Err(new ErrorV3(msg, 'find-slackBotToken-failed'), 400);
  427. }
  428. }
  429. catch (error) {
  430. logger.error('Error', error);
  431. return res.apiv3Err(new ErrorV3(`Error occured while testing. Cause: ${error.message}`, 'test-failed', error.stack));
  432. }
  433. const { channel } = req.body;
  434. const appSiteURL = crowi.configManager.getConfig('crowi', 'app:siteUrl');
  435. try {
  436. await sendSuccessMessage(slackBotToken, channel, appSiteURL);
  437. }
  438. catch (error) {
  439. return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
  440. }
  441. });
  442. /**
  443. * @swagger
  444. *
  445. * /slack-integration-settings/without-proxy/test:
  446. * post:
  447. * tags: [botType]
  448. * operationId: postTest
  449. * summary: test the connection
  450. * description: Test the connection with slack work space.
  451. * requestBody:
  452. * content:
  453. * application/json:
  454. * schema:
  455. * properties:
  456. * testChannel:
  457. * type: string
  458. * responses:
  459. * 200:
  460. * description: Succeeded to connect to slack work space.
  461. */
  462. router.post('/without-proxy/test', loginRequiredStrictly, adminRequired, csrf, validator.SlackChannel, apiV3FormValidator, async(req, res) => {
  463. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  464. if (currentBotType !== 'customBotWithoutProxy') {
  465. const msg = 'Select Without Proxy Type';
  466. return res.apiv3Err(new ErrorV3(msg, 'select-not-proxy-type'), 400);
  467. }
  468. const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
  469. try {
  470. await testToSlack(slackBotToken);
  471. }
  472. catch (error) {
  473. logger.error('Error', error);
  474. return res.apiv3Err(new ErrorV3(`Error occured while testing. Cause: ${error.message}`, 'test-failed', error.stack));
  475. }
  476. const { channel } = req.body;
  477. const appSiteURL = crowi.configManager.getConfig('crowi', 'app:siteUrl');
  478. try {
  479. await sendSuccessMessage(slackBotToken, channel, appSiteURL);
  480. }
  481. catch (error) {
  482. return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
  483. }
  484. return res.apiv3();
  485. });
  486. return router;
  487. };