slack-integration-settings.js 18 KB

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