Просмотр исходного кода

Merge branch 'feat/growi-bot' into feat/5610-insert-components

Steven Fukase 5 лет назад
Родитель
Сommit
8b05281c92

+ 7 - 0
.github/ISSUE_TEMPLATE.md → .github/ISSUE_TEMPLATE/bug-report.md

@@ -1,3 +1,10 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: 'Bug:' 
+labels: bug
+---
+
 Environment
 ------------
 

+ 5 - 0
.github/ISSUE_TEMPLATE/config.yml

@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+  - name: Question or Suggestions
+    url: https://growi-slackin.weseek.co.jp/
+    about: If you have questions or suggestions, you can join our Slack team and talk about anything, anytime.

+ 25 - 0
.github/ISSUE_TEMPLATE/user-request.md

@@ -0,0 +1,25 @@
+---
+name: User request
+about: Suggest an idea for this project
+title: 'Request:'
+labels: user requests
+---
+
+
+## Informations
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. 
+
+e.g. I'm having trouble getting immediate access to information
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to realization.
+
+e.g. It's good if there is a space where everyone can access information in common.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+- [ ] Custom Page In Sidebar
+  - [ ] Can be edited in `/Sidebar` path
+  - [ ] Can be described with md like a page

+ 1 - 1
.github/workflows/release.yml

@@ -125,7 +125,7 @@ jobs:
     - name: Slack Notification
       uses: weseek/ghaction-release-slack-notification@master
       with:
-        channel: '#general'
+        channel: '#release'
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         created_tag: 'v${{ needs.github-release.outputs.RELEASE_VERSION }}${{ env.SUFFIX }}'
 

+ 5 - 0
packages/slackbot-proxy/src/controllers/slack.ts

@@ -3,9 +3,11 @@ import {
 } from '@tsed/common';
 
 import { Installation } from '~/entities/installation';
+import { Relation } from '~/entities/relation';
 import { Order } from '~/entities/order';
 
 import { InstallationRepository } from '~/repositories/installation';
+import { RelationRepository } from '~/repositories/relation';
 import { OrderRepository } from '~/repositories/order';
 import { InstallerService } from '~/services/InstallerService';
 import { ReceiveService } from '~/services/RecieveService';
@@ -20,6 +22,9 @@ export class SlackCtrl {
   @Inject()
   installationRepository: InstallationRepository;
 
+  @Inject()
+  relationRepository: RelationRepository;
+
   @Inject()
   orderRepository: OrderRepository;
 

+ 0 - 1
packages/slackbot-proxy/src/entities/installation.ts

@@ -6,7 +6,6 @@ import {
 } from 'typeorm';
 
 import { Installation as SlackInstallation } from '@slack/oauth';
-import { Order } from './order';
 
 @Entity()
 export class Installation {

+ 33 - 0
packages/slackbot-proxy/src/entities/relation.ts

@@ -0,0 +1,33 @@
+import {
+  Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, ManyToOne,Index
+} from 'typeorm';
+import { Installation } from './installation';
+
+@Entity()
+@Index(["installation", "growiUri"], { unique: true })
+export class Relation {
+
+  @PrimaryGeneratedColumn()
+  readonly id: number;
+
+  @CreateDateColumn()
+  readonly createdAt: Date;
+
+  @UpdateDateColumn()
+  readonly updatedAt: Date;
+
+  @ManyToOne(() => Installation)
+  readonly installation: number;
+
+  @Column()
+  @Index({ unique: true })
+  tokenGtoP: string;
+
+  @Column()
+  @Index()
+  tokenPtoG: string;
+
+  @Column()
+  growiUri: string;
+
+}

+ 10 - 0
packages/slackbot-proxy/src/repositories/relation.ts

@@ -0,0 +1,10 @@
+import {
+  Repository, EntityRepository,
+} from 'typeorm';
+
+import { Relation } from '~/entities/relation';
+
+@EntityRepository(Relation)
+export class RelationRepository extends Repository<Relation> {
+
+}

+ 1 - 1
resource/locales/ja_JP/admin/admin.json

@@ -294,7 +294,7 @@
       "install_bot_to_slack": "Bot を Slackにインストールする",
       "register_secret_and_token": "Signing Secret と Bot Token を登録する",
       "test_connection": "連携状況のテストをする",
-      "how_to_create_a_bot": "Bot の作り方"
+      "how_to_create_a_bot": "作成方法はこちら"
     }
   },
   "user_management": {

+ 43 - 22
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -10,27 +10,35 @@ import introductionToChannelImage from '../../../../../../public/images/slack-in
 
 const CustomBotWithoutSettingsAccordion = () => {
   const { t } = useTranslation('admin');
-  const [currentlyOpenAccordionIndex, setCurrentlyOpenAccordionIndex] = useState(null);
+  const [openAccordionIndexes, setOpenAccordionIndexes] = useState(new Set());
+
   const onToggleAccordionHandler = (i) => {
-    if (currentlyOpenAccordionIndex === i) {
-      setCurrentlyOpenAccordionIndex(null);
-      return;
+    const accordionIndexes = new Set(openAccordionIndexes);
+    if (accordionIndexes.has(i)) {
+      accordionIndexes.delete(i);
+    }
+    else {
+      accordionIndexes.add(i);
     }
-    setCurrentlyOpenAccordionIndex(i);
+    setOpenAccordionIndexes(accordionIndexes);
   };
 
   return (
-    <div className="card border-0 rounded-lg shadow overflow-hidden" id="customBotWithoutProxySettingsAccordion">
+    <div className="card border-0 rounded-lg shadow overflow-hidden">
 
       <div className="card border-0 rounded-lg mb-0">
-        <div className="card-header clickable py-3 d-flex justify-content-between" onClick={() => onToggleAccordionHandler(0)}>
-          <p className="mb-0 text-primary">{`① ${t('slack_integration.without_proxy.create_bot')}`}</p>
-          {currentlyOpenAccordionIndex === 0
+        <div
+          className="card-header font-weight-normal py-3 d-flex justify-content-between"
+          role="button"
+          onClick={() => onToggleAccordionHandler(0)}
+        >
+          <p className="mb-0 text-primary"><span className="mr-2">①</span>{t('slack_integration.without_proxy.create_bot')}</p>
+          {openAccordionIndexes.has(0)
             ? <i className="fa fa-chevron-up" />
             : <i className="fa fa-chevron-down" />
           }
         </div>
-        <Collapse isOpen={currentlyOpenAccordionIndex === 0}>
+        <Collapse isOpen={openAccordionIndexes.has(0)}>
           <div className="card-body">
 
             <div className="row my-5">
@@ -41,6 +49,7 @@ const CustomBotWithoutSettingsAccordion = () => {
                     <i className="fa fa-external-link ml-2" aria-hidden="true" />
                   </button>
                 </div>
+                {/* TODO: Insert DOCS link */}
                 <a href="#">
                   <p className="text-center mt-1">
                     <small>
@@ -56,9 +65,13 @@ const CustomBotWithoutSettingsAccordion = () => {
       </div>
 
       <div className="card border-0 rounded-lg mb-0">
-        <div className="card-header clickable py-3 d-flex justify-content-between" onClick={() => onToggleAccordionHandler(1)}>
-          <p className="mb-0 text-primary">{`② ${t('slack_integration.without_proxy.install_bot_to_slack')}`}</p>
-          {currentlyOpenAccordionIndex === 1
+        <div
+          className="card-header font-weight-normal py-3 d-flex justify-content-between"
+          role="button"
+          onClick={() => onToggleAccordionHandler(1)}
+        >
+          <p className="mb-0 text-primary"><span className="mr-2">②</span>{t('slack_integration.without_proxy.install_bot_to_slack')}</p>
+          {openAccordionIndexes.has(1)
             ? <i className="fa fa-chevron-up" />
             : <i className="fa fa-chevron-down" />
           }
@@ -83,14 +96,18 @@ const CustomBotWithoutSettingsAccordion = () => {
       </div>
 
       <div className="card border-0 rounded-lg mb-0">
-        <div className="card-header clickable py-3 d-flex justify-content-between" onClick={() => onToggleAccordionHandler(2)}>
-          <p className="mb-0 text-primary">{`③ ${t('slack_integration.without_proxy.register_secret_and_token')}`}</p>
-          {currentlyOpenAccordionIndex === 2
+        <div
+          className="card-header font-weight-normal py-3 d-flex justify-content-between"
+          role="button"
+          onClick={() => onToggleAccordionHandler(2)}
+        >
+          <p className="mb-0 text-primary"><span className="mr-2">③</span>{t('slack_integration.without_proxy.register_secret_and_token')}</p>
+          {openAccordionIndexes.has(2)
             ? <i className="fa fa-chevron-up" />
             : <i className="fa fa-chevron-down" />
           }
         </div>
-        <Collapse isOpen={currentlyOpenAccordionIndex === 2}>
+        <Collapse isOpen={openAccordionIndexes.has(2)}>
           <div className="card-body">
             BODY 3
           </div>
@@ -98,19 +115,23 @@ const CustomBotWithoutSettingsAccordion = () => {
       </div>
 
       <div className="card border-0 rounded-lg mb-0">
-        <div className="card-header clickable py-3 d-flex justify-content-between" onClick={() => onToggleAccordionHandler(3)}>
-          <p className="mb-0 text-primary">{`④ ${t('slack_integration.without_proxy.test_connection')}`}</p>
-          {currentlyOpenAccordionIndex === 3
+        <div
+          className="card-header font-weight-normal py-3 d-flex justify-content-between"
+          role="button"
+          onClick={() => onToggleAccordionHandler(3)}
+        >
+          <p className="mb-0 text-primary"><span className="mr-2">④</span>{t('slack_integration.without_proxy.test_connection')}</p>
+          {openAccordionIndexes.has(3)
             ? <i className="fa fa-chevron-up" />
             : <i className="fa fa-chevron-down" />
           }
         </div>
-        <Collapse isOpen={currentlyOpenAccordionIndex === 3}>
+        <Collapse isOpen={openAccordionIndexes.has(3)}>
           <div className="card-body">
             <p className="text-dark">以下のテストボタンを押して、Slack連携が完了しているかの確認をしましょう</p>
             <button type="button" className="btn btn-info">Test</button>
             <p className="text-danger">エラーが発生しました。下記のログを確認してください。</p>
-            
+
           </div>
         </Collapse>
       </div>

+ 0 - 4
src/client/styles/scss/_admin.scss

@@ -105,10 +105,6 @@
     }
   }
 
-  #customBotWithoutProxySettingsAccordion .card-header {
-    font-weight: 300;
-  }
-
   //// TODO: migrate to Bootstrap 4
   //// omit all .btn-toggle and use Switches
   //// https://getbootstrap.com/docs/4.2/components/forms/#switches

+ 39 - 3
src/server/routes/apiv3/slack-bot.js

@@ -1,6 +1,9 @@
 
 const express = require('express');
 
+const crypto = require('crypto');
+const qs = require('qs');
+
 const loggerFactory = require('@alias/logger');
 
 const logger = loggerFactory('growi:routes:apiv3:slack-bot');
@@ -10,7 +13,6 @@ const router = express.Router();
 module.exports = (crowi) => {
   this.app = crowi.express;
 
-
   // Check if the access token is correct
   function verificationAccessToken(req, res, next) {
     const slackBotAccessToken = req.body.slack_bot_access_token || null;
@@ -32,7 +34,41 @@ module.exports = (crowi) => {
     return next();
   }
 
-  router.post('/', verificationRequestUrl, verificationAccessToken, async(req, res) => {
+  /**
+   * Verify if the request came from slack
+   * See: https://api.slack.com/authentication/verifying-requests-from-slack
+   */
+  // TODO GW-5628 move this to slack package
+  function verifyingIsSlackRequest(req, res, next) {
+    // Temporary
+    req.signingSecret = crowi.configManager.getConfig('crowi', 'slackbot:signingSecret');
+
+    // take out slackSignature and timestamp from header
+    const slackSignature = req.headers['x-slack-signature'];
+    const timestamp = req.headers['x-slack-request-timestamp'];
+
+    // protect against replay attacks
+    const time = Math.floor(new Date().getTime() / 1000);
+    if (Math.abs(time - timestamp) > 300) {
+      return res.send('Verification failed.');
+    }
+
+    // generate growi signature
+    const sigBaseString = `v0:${timestamp}:${qs.stringify(req.body, { format: 'RFC1738' })}`;
+    const hasher = crypto.createHmac('sha256', req.signingSecret);
+    hasher.update(sigBaseString, 'utf8');
+    const hashedSigningSecret = hasher.digest('hex');
+    const growiSignature = `v0=${hashedSigningSecret}`;
+
+    // compare growiSignature and slackSignature
+    if (crypto.timingSafeEqual(Buffer.from(growiSignature, 'utf8'), Buffer.from(slackSignature, 'utf8'))) {
+      return next();
+    }
+
+    return res.send('Verification failed');
+  }
+
+  router.post('/', verificationRequestUrl, verifyingIsSlackRequest, verificationAccessToken, async(req, res) => {
 
     // Send response immediately to avoid opelation_timeout error
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
@@ -94,7 +130,7 @@ module.exports = (crowi) => {
     }
   };
 
-  router.post('/interactive', verificationRequestUrl, async(req, res) => {
+  router.post('/interactive', verificationRequestUrl, verifyingIsSlackRequest, async(req, res) => {
 
     // Send response immediately to avoid opelation_timeout error
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events