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

Merge pull request #350 from weseek/master

release v3.0.6
Yuki Takei 8 лет назад
Родитель
Сommit
76f26bf26e

+ 14 - 2
CHANGES.md

@@ -1,9 +1,21 @@
 CHANGES
 ========
 
-## 3.0.5-RC
+## 3.0.6-RC
 
-* 
+* Improvement: Simplify configuration for Slack Web API
+* Support: Use 'slack-node' instead of '@slack/client'
+* Support: Upgrade libs
+    * googleapis
+    * i18next
+    * i18next-express-middleware
+    * react-bootstrap-typeahead
+    * sass-loader
+    * uglifycss
+
+## 3.0.5
+
+* Fix: lsx plugins doesn't show page names
 
 ## 3.0.4
 

+ 1 - 1
lib/form/admin/securityGeneral.js

@@ -11,6 +11,6 @@ module.exports = form(
   field('settingForm[security:basicSecret]'),
   field('settingForm[security:restrictGuestMode]').required(),
   field('settingForm[security:registrationMode]').required(),
-  field('settingForm[security:registrationWhiteList]').custom(normalizeCRLF).custom(stringToArray)
+  field('settingForm[security:registrationWhiteList]').custom(normalizeCRLF).custom(stringToArray),
 );
 

+ 1 - 0
lib/form/admin/securityPassportLdap.js

@@ -16,6 +16,7 @@ module.exports = form(
   field('settingForm[security:passport-ldap:bindDNPassword]'),
   field('settingForm[security:passport-ldap:searchFilter]'),
   field('settingForm[security:passport-ldap:attrMapUsername]'),
+  field('settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
   field('settingForm[security:passport-ldap:groupSearchBase]'),
   field('settingForm[security:passport-ldap:groupSearchFilter]'),
   field('settingForm[security:passport-ldap:groupDnProperty]')

+ 1 - 2
lib/form/admin/slackSetting.js

@@ -4,7 +4,6 @@ var form = require('express-form')
   , field = form.field;
 
 module.exports = form(
-  field('slackSetting[slack:clientId]', 'clientId'),
-  field('slackSetting[slack:clientSecret]', 'clientSecret')
+  field('slackSetting[slack:token]', 'token'),
 );
 

+ 3 - 1
lib/locales/en-US/translation.json

@@ -275,7 +275,9 @@
     "for_instance":" For instance, if you use growi within a company, you can write ",
     "only_those":" Only those whose e-mail address including the company address can register.",
     "insert_single":"Please insert single e-mail address per line.",
-    "Authentication mechanism settings":"Authentication mechanism settings"
+    "Authentication mechanism settings":"Authentication mechanism settings",
+    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
+    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>."
   },
 
   "markdown_setting": {

+ 3 - 1
lib/locales/ja/translation.json

@@ -295,7 +295,9 @@
     "for_instance":"例えば、会社で使う場合 などと記載すると、",
     "only_those":"その会社のメールアドレスを持っている人のみ登録可能になります。",
     "insert_single":"1行に1メールアドレス入力してください。",
-    "Authentication mechanism settings":"認証機構設定"
+    "Authentication mechanism settings":"認証機構設定",
+    "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
+    "Treat username matching as identical_warn": "WARNING: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください"
   },
   "markdown_setting": {
     "markdown_rendering": "Markdownレンダリングの設定を変更できます。",

+ 12 - 22
lib/models/config.js

@@ -63,6 +63,7 @@ module.exports = function(crowi) {
       'security:passport-ldap:groupSearchBase' : undefined,
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,
+      'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser': false,
 
       'aws:bucket'          : 'growi',
       'aws:region'          : 'ap-northeast-1',
@@ -277,6 +278,12 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
   };
 
+  configSchema.statics.isSameUsernameTreatedAsIdenticalUser = function(config, providerType)
+  {
+    const key = `security:passport-${providerType}:isSameUsernameTreatedAsIdenticalUser`;
+    return getValueForCrowiNS(config, key);
+  };
+
   configSchema.statics.isUploadable = function(config)
   {
     var method = crowi.env.FILE_UPLOAD || 'aws';
@@ -443,20 +450,7 @@ module.exports = function(crowi) {
 
   configSchema.statics.hasSlackConfig = function(config)
   {
-    return Config.hasSlackWebClientConfig(config) || Config.hasSlackIwhUrl(config);
-  };
-
-  configSchema.statics.hasSlackWebClientConfig = function(config)
-  {
-    if (!config.notification) {
-      return false;
-    }
-    if (!config.notification['slack:clientId'] ||
-        !config.notification['slack:clientSecret']) {
-      return false;
-    }
-
-    return true;
+    return Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config);
   };
 
   /**
@@ -467,7 +461,7 @@ module.exports = function(crowi) {
     if (!config.notification) {
       return false;
     }
-    return config.notification['slack:incomingWebhookUrl'];
+    return (config.notification['slack:incomingWebhookUrl'] ? true : false);
   };
 
   configSchema.statics.isIncomingWebhookPrioritized = function(config)
@@ -475,20 +469,16 @@ module.exports = function(crowi) {
     if (!config.notification) {
       return false;
     }
-    return config.notification['slack:isIncomingWebhookPrioritized'];
+    return (config.notification['slack:isIncomingWebhookPrioritized'] ? true : false);
   };
 
   configSchema.statics.hasSlackToken = function(config)
   {
-    if (!this.hasSlackWebClientConfig(config)) {
-      return false;
-    }
-
-    if (!config.notification['slack:token']) {
+    if (!config.notification) {
       return false;
     }
 
-    return true;
+    return (config.notification['slack:token'] ? true : false);
   };
 
   configSchema.statics.getLocalconfig = function(config)

+ 32 - 23
lib/models/external-account.js

@@ -71,34 +71,42 @@ class ExternalAccount {
   static findOrRegister(providerType, accountId, usernameToBeRegistered) {
 
     return this.findOne({ providerType, accountId })
-      .then((account) => {
-        // found
+      .then(account => {
+        // ExternalAccount is found
         if (account != null) {
           debug(`ExternalAccount '${accountId}' is found `, account);
           return account;
         }
-        // not found
-        else {
-          debug(`ExternalAccount '${accountId}' is not found, it is going to be registered.`);
-
-          const User = ExternalAccount.crowi.model('User');
-
-          return User.count({username: usernameToBeRegistered})
-            .then((count) => {
-              // throw Exception when count is not zero
-              if (count > 0) {
-                throw new DuplicatedUsernameException(`username '${usernameToBeRegistered}' has already been existed`);
-              }
-
-              // create user with STATUS_ACTIVE
-              return User.createUser('', usernameToBeRegistered, undefined, undefined, undefined, User.STATUS_ACTIVE);
-            })
-            .then((user) => {
-              return this.create({ providerType: 'ldap', accountId, user: user._id });
-            });
-        }
+
+        const User = ExternalAccount.crowi.model('User');
+
+        return User.findOne({username: usernameToBeRegistered})
+          .then(user => {
+            // when the User that have the same `username` exists
+            if (user != null) {
+              throw new DuplicatedUsernameException(`User '${usernameToBeRegistered}' already exists`, user);
+            }
+
+            // create a new User with STATUS_ACTIVE
+            debug(`ExternalAccount '${accountId}' is not found, it is going to be registered.`);
+            return User.createUser('', usernameToBeRegistered, undefined, undefined, undefined, User.STATUS_ACTIVE);
+          })
+          .then(newUser => {
+            return this.associate(providerType, accountId, newUser._id);
+          });
+
       });
+  }
 
+  /**
+   * Create ExternalAccount document and associate to existing User
+   *
+   * @param {string} providerType
+   * @param {string} accountId
+   * @param {object} user
+   */
+  static associate(providerType, accountId, user) {
+    return this.create({ providerType, accountId, user: user._id });
   }
 
   /**
@@ -135,9 +143,10 @@ class ExternalAccount {
  * @class DuplicatedUsernameException
  */
 class DuplicatedUsernameException {
-  constructor(message) {
+  constructor(message, user) {
     this.name = this.constructor.name;
     this.message = message;
+    this.user = user;
   }
 }
 

+ 2 - 14
lib/routes/admin.js

@@ -95,8 +95,7 @@ module.exports = function(crowi, app) {
   // app.get('/admin/security'                  , admin.security.index);
   actions.security = {};
   actions.security.index = function(req, res) {
-    var settingForm;
-    settingForm = Config.setupCofigFormData('crowi', req.config);
+    const settingForm = Config.setupCofigFormData('crowi', req.config);
     return res.render('admin/security', { settingForm });
   };
 
@@ -159,19 +158,10 @@ module.exports = function(crowi, app) {
     var config = crowi.getConfig();
     var UpdatePost = crowi.model('UpdatePost');
     var slackSetting = Config.setupCofigFormData('notification', config);
-    var hasSlackWebClientConfig = Config.hasSlackWebClientConfig(config);
     var hasSlackIwhUrl = Config.hasSlackIwhUrl(config);
     var hasSlackToken = Config.hasSlackToken(config);
     var slack = crowi.slack;
-    var slackAuthUrl = '';
 
-    if (!Config.hasSlackWebClientConfig(req.config)) {
-      slackSetting['slack:clientId'] = '';
-      slackSetting['slack:clientSecret'] = '';
-    }
-    else {
-      slackAuthUrl = slack.getAuthorizeURL();
-    }
     if (!Config.hasSlackIwhUrl(req.config)) {
       slackSetting['slack:incomingWebhookUrl'] = '';
     }
@@ -186,10 +176,8 @@ module.exports = function(crowi, app) {
       return res.render('admin/notification', {
         settings,
         slackSetting,
-        hasSlackWebClientConfig,
         hasSlackIwhUrl,
         hasSlackToken,
-        slackAuthUrl
       });
     });
   };
@@ -862,7 +850,7 @@ module.exports = function(crowi, app) {
   };
 
   actions.api.securitySetting = function(req, res) {
-    var form = req.form.settingForm;
+    const form = req.form.settingForm;
 
     if (req.form.isValid) {
       debug('form content', form);

+ 13 - 1
lib/routes/login-passport.js

@@ -112,6 +112,18 @@ module.exports = function(crowi, app) {
 
       // find or register(create) user
       ExternalAccount.findOrRegister('ldap', ldapAccountId, usernameToBeRegistered)
+        .catch((err) => {
+          if (err.name === 'DuplicatedUsernameException') {
+            // get option
+            const isSameUsernameTreatedAsIdenticalUser = Config.isSameUsernameTreatedAsIdenticalUser(config, 'ldap');
+            if (isSameUsernameTreatedAsIdenticalUser) {
+              // associate to existing user
+              debug(`ExternalAccount '${ldapAccountId}' will be created and bound to the exisiting User account`);
+              return ExternalAccount.associate('ldap', ldapAccountId, err.user);
+            }
+          }
+          throw err;  // throw again
+        })
         .then((externalAccount) => {
           return externalAccount.getPopulatedUser();
         })
@@ -125,7 +137,7 @@ module.exports = function(crowi, app) {
           });
         })
         .catch((err) => {
-          if (err.name != null && err.name === 'DuplicatedUsernameException') {
+          if (err.name === 'DuplicatedUsernameException') {
             req.flash('isDuplicatedUsernameExceptionOccured', true);
             return next();
           }

+ 1 - 1
lib/routes/me.js

@@ -268,7 +268,7 @@ module.exports = function(crowi, app) {
         const ldapAccountId = passportService.getLdapAccountIdFromReq(req);
         const user = req.user;
 
-        ExternalAccount.create({ providerType: 'ldap', accountId: ldapAccountId, user: user._id })
+        ExternalAccount.associate('ldap', ldapAccountId, user)
           .then(() => {
             return redirectWithFlash('successMessage', 'Successfully added.');
           })

+ 4 - 2
lib/routes/page.js

@@ -620,8 +620,10 @@ module.exports = function(crowi, app) {
 
           if (crowi.slack) {
             notify.slack.channel.split(',').map(function(chan) {
-              var message = crowi.slack.prepareSlackMessage(pageData, req.user, chan, updateOrCreate, previousRevision);
-              crowi.slack.post(message.channel, message.text, message).then(function(){}).catch(function(){});
+              crowi.slack.post(pageData, req.user, chan, updateOrCreate, previousRevision)
+                .catch((err) => {
+                  debug(err);
+                });
             });
           }
         }

+ 1 - 1
lib/service/passport.js

@@ -170,7 +170,7 @@ class PassportService {
     const match = serverUrl.match(/(ldaps?:\/\/[^\/]+)\/(.*)?/);
     if (match == null || match.length < 1) {
       debug('LdapStrategy: serverUrl is invalid');
-      return;
+      return (req, callback) => { callback({ message: 'serverUrl is invalid'}) };
     }
     const url = match[1];
     const searchBase = match[2] || '';

+ 63 - 124
lib/util/slack.js

@@ -8,128 +8,27 @@ module.exports = function(crowi) {
   const SLACK_URL = 'https://slack.com';
 
   const debug = require('debug')('crowi:util:slack'),
+    config = crowi.getConfig(),
     Config = crowi.model('Config'),
-    SlackWebClient = require('@slack/client').WebClient,
-    SlackIncomingWebhook = require('@slack/client').IncomingWebhook,
+    Slack = require('slack-node'),
     slack = {};
 
-  slack.client = undefined;
-  slack.incomingWebhook = undefined;
-
-  slack.getClient = function() {
-    // alreay created
-    if (slack.client) {
-      return slack.client;
-    }
-
-    const config = crowi.getConfig();
-
-    let client;
-    if (Config.hasSlackToken(config)) {
-      client = new SlackWebClient(config.notification['slack:token']);
-      slack.client = client;
-    }
-
-    return slack.client;
-  };
-
-  // this is called to generate redirect_uri
-  slack.getSlackAuthCallbackUrl = function()
-  {
-    var config = crowi.getConfig();
-    // Web アクセスがきてないと app:url がセットされないので crowi.setupSlack 時にはできない
-    // cli, bot 系作るときに問題なりそう
-    return (config.crowi['app:url'] || '') + '/admin/notification/slackAuth';
+  const postWithIwh = function(messageObj, callback) {
+    const client = new Slack();
+    client.setWebhook(config.notification['slack:incomingWebhookUrl']);
+    client.webhook(messageObj, callback);
   }
 
-  // this is called to get the url for oauth screen
-  slack.getAuthorizeURL = function () {
-    const config = crowi.getConfig();
-    if (Config.hasSlackWebClientConfig(config)) {
-      const slackClientId = config.notification['slack:clientId'];
-      const redirectUri = slack.getSlackAuthCallbackUrl();
-
-      return `${SLACK_URL}/oauth/authorize?client_id=${slackClientId}&redirect_uri=${redirectUri}&scope=chat:write:bot`;
-    } else {
-
-      return '';
+  const postWithWebApi = function(messageObj, callback) {
+    const client = new Slack(config.notification['slack:token']);
+    // stringify attachments
+    if (messageObj.attachments != null) {
+      messageObj.attachments = JSON.stringify(messageObj.attachments);
     }
+    client.api('chat.postMessage', messageObj, callback);
   }
 
-  // this is called to get access token with code (oauth process)
-  slack.getOauthAccessToken = function(code) {
-
-    const client = new SlackWebClient();
-
-    const config = crowi.getConfig();
-    const clientId = config.notification['slack:clientId'];
-    const clientSecret = config.notification['slack:clientSecret'];
-    const redirectUri = slack.getSlackAuthCallbackUrl();
-
-    return client.oauth.access(clientId, clientSecret, code, {redirect_uri: redirectUri});
-  }
-
-  slack.getIncomingWebhook = function() {
-    // alreay created
-    if (slack.incomingWebhook) {
-      return slack.incomingWebhook;
-    }
-
-    const config = crowi.getConfig();
-
-    let incomingWebhook;
-    if (Config.hasSlackIwhUrl(config)) {
-      incomingWebhook = new SlackIncomingWebhook(config.notification['slack:incomingWebhookUrl']);
-      slack.incomingWebhook = incomingWebhook;
-    }
-
-    return slack.incomingWebhook;
-  };
-
-  slack.post = function (channel, message, opts) {
-    const config = crowi.getConfig();
-
-    return new Promise(function(resolve, reject) {
-
-      // define callback function
-      const callback = function(err, res) {
-        if (err) {
-          debug('Post error', err, res);
-          debug('Sent data to slack is:', message);
-          return reject(err);
-        }
-        resolve(res);
-      };
-
-      // when incoming Webhooks is prioritized
-      if (Config.isIncomingWebhookPrioritized(config)) {
-        if (Config.hasSlackIwhUrl(config)) {
-          debug(`posting message with IncomingWebhook`);
-          slack.getIncomingWebhook().send(opts, callback);
-        }
-        else if (Config.hasSlackToken(config)) {
-          debug(`posting message with WebClient`);
-          slack.getClient().chat.postMessage(channel, message, opts, callback);
-        }
-      }
-      // else
-      else {
-        if (Config.hasSlackToken(config)) {
-          debug(`posting message with WebClient`);
-          slack.getClient().chat.postMessage(channel, message, opts, callback);
-        }
-        else if (Config.hasSlackIwhUrl(config)) {
-          debug(`posting message with IncomingWebhook`);
-          slack.getIncomingWebhook().send(opts, callback);
-        }
-      }
-
-      resolve();
-    });
-  };
-
-  slack.convertMarkdownToMrkdwn = function(body) {
-    var config = crowi.getConfig();
+  const convertMarkdownToMrkdwn = function(body) {
     var url = '';
     if (config.crowi && config.crowi['app:url']) {
       url = config.crowi['app:url'];
@@ -145,16 +44,16 @@ module.exports = function(crowi) {
     return body;
   };
 
-  slack.prepareAttachmentTextForCreate = function(page, user) {
+  const prepareAttachmentTextForCreate = function(page, user) {
     var body = page.revision.body;
     if (body.length > 2000) {
       body = body.substr(0, 2000) + '...';
     }
 
-    return this.convertMarkdownToMrkdwn(body);
+    return convertMarkdownToMrkdwn(body);
   };
 
-  slack.prepareAttachmentTextForUpdate = function(page, user, previousRevision) {
+  const prepareAttachmentTextForUpdate = function(page, user, previousRevision) {
     var diff = require('diff');
     var diffText = ''
 
@@ -179,15 +78,14 @@ module.exports = function(crowi) {
     return diffText;
   };
 
-  slack.prepareSlackMessage = function(page, user, channel, updateType, previousRevision) {
-    var config = crowi.getConfig();
+  const prepareSlackMessage = function(page, user, channel, updateType, previousRevision) {
     var url = config.crowi['app:url'] || '';
     var body = page.revision.body;
 
     if (updateType == 'create') {
-      body = this.prepareAttachmentTextForCreate(page, user);
+      body = prepareAttachmentTextForCreate(page, user);
     } else {
-      body = this.prepareAttachmentTextForUpdate(page, user, previousRevision);
+      body = prepareAttachmentTextForUpdate(page, user, previousRevision);
     }
 
     var attachment = {
@@ -207,16 +105,15 @@ module.exports = function(crowi) {
     var message = {
       channel: '#' + channel,
       username: Config.appTitle(config),
-      text: this.getSlackMessageText(page.path, user, updateType),
+      text: getSlackMessageText(page.path, user, updateType),
       attachments: [attachment],
     };
 
     return message;
   };
 
-  slack.getSlackMessageText = function(path, user, updateType) {
+  const getSlackMessageText = function(path, user, updateType) {
     let text;
-    const config = crowi.getConfig();
     const url = config.crowi['app:url'] || '';
 
     const pageUrl = `<${url}${path}|${path}>`;
@@ -229,5 +126,47 @@ module.exports = function(crowi) {
     return text;
   };
 
+  // slack.post = function (channel, message, opts) {
+  slack.post = (page, user, channel, updateType, previousRevision) => {
+    const messageObj = prepareSlackMessage(page, user, channel, updateType, previousRevision);
+
+    return new Promise((resolve, reject) => {
+      // define callback function for Promise
+      const callback = function(err, res) {
+        if (err) {
+          debug('Post error', err, res);
+          debug('Sent data to slack is:', messageObj);
+          return reject(err);
+        }
+        resolve(res);
+      };
+
+      // when incoming Webhooks is prioritized
+      if (Config.isIncomingWebhookPrioritized(config)) {
+        if (Config.hasSlackIwhUrl(config)) {
+          debug(`posting message with IncomingWebhook`);
+          postWithIwh(messageObj, callback);
+        }
+        else if (Config.hasSlackToken(config)) {
+          debug(`posting message with Web API`);
+          postWithWebApi(messageObj, callback);
+        }
+      }
+      // else
+      else {
+        if (Config.hasSlackToken(config)) {
+          debug(`posting message with Web API`);
+          postWithWebApi(messageObj, callback);
+        }
+        else if (Config.hasSlackIwhUrl(config)) {
+          debug(`posting message with IncomingWebhook`);
+          postWithIwh(messageObj, callback);
+        }
+      }
+
+      resolve();
+    });
+  };
+
   return slack;
 };

+ 9 - 63
lib/views/admin/notification.html

@@ -75,7 +75,7 @@
 
               <div class="form-group">
                 <div class="col-xs-offset-3 col-xs-6">
-                  <button type="submit" class="btn btn-primary">Submit</button>
+                  <button type="submit" class="btn btn-primary">Save</button>
                 </div>
               </div>
             </fieldset>
@@ -117,55 +117,27 @@
                 <i class="icon-fw icon-exclamation text-danger"></i><span class="text-danger">NOT RECOMMENDED</span>
                 <br><br>
                 This is the way that compatible with Crowi,<br>
-                but not recommended in GROWI because it is too complex.
+                but not recommended in GROWI because it is <strong>too complex</strong>.
                 <br><br>
                 Please use <a href="#slack-incoming-webhooks" data-toggle="tab" onclick="activateTab('slack-incoming-webhooks')">Slack incomming webhooks Configuration</a> instead.
               </p>
 
               <div class="form-group">
-                <label for="slackSetting[slack:clientId]" class="col-xs-3 control-label">clientId</label>
+                <label for="slackSetting[slack:token]" class="col-xs-3 control-label">OAuth Access Token</label>
                 <div class="col-xs-6">
-                  <input class="form-control" type="text" name="slackSetting[slack:clientId]" value="{{ slackSetting['slack:clientId'] }}">
-                </div>
-              </div>
-
-              <div class="form-group">
-                <label for="slackSetting[slack:clientSecret]" class="col-xs-3 control-label">clientSecret</label>
-                <div class="col-xs-6">
-                  <input class="form-control" type="text" name="slackSetting[slack:clientSecret]" value="{{ slackSetting['slack:clientSecret'] }}">
+                  <input class="form-control" type="text" name="slackSetting[slack:token]" value="{{ slackSetting['slack:token'] || '' }}">
                 </div>
               </div>
 
               <div class="form-group">
                 <div class="col-xs-offset-3 col-xs-6">
-                  <button type="submit" class="btn btn-primary">Submit</button>
+                  <button type="submit" class="btn btn-primary">Save</button>
                 </div>
               </div>
             </fieldset>
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
           </form>
 
-          {% if hasSlackWebClientConfig %}
-          <div class="text-center">
-            {% if hasSlackToken %}
-            <p>Crowi and Slack is already <strong>connected</strong>. You can re-connect to refresh and overwirte the token with your Slack account.</p>
-            <a class="btn btn-warning btn-rounded" href="/admin/notification/slackSetting/disconnect">
-              <i class="icon-power"></i> Disconnect from Slack
-            </a>
-            <a class="btn btn-success btn-outline btn-rounded" href="{{ slackAuthUrl }}" target="_blank">
-              <i class="icon-login"></i> Reconnect to Slack
-            </a>
-            {% else %}
-            <p>Slack clientId and clientSecret is configured. Now, you can connect with Slack.</p>
-            <a class="btn btn-primary btn-outline2 btn-rounded" href="{{ slackAuthUrl }}" target="_blank">
-              <i class="icon-login"></i> Connect to Slack
-            </a>
-            {% endif %}
-          </div>
-
-          {% endif %}
-
-          {# {% if not hasSlackWebClientConfig %} #}
           <hr>
           <h3>
             <i class="icon-question" aria-hidden="true"></i>
@@ -186,20 +158,6 @@
                 <li><strong>Save</strong> it.</li>
               </ol>
             </li>
-            <li>
-              Get App Credentials
-              <ol>
-                <li>Go To "Basic Information" page and make a note "Client ID" and "Client Secret".</li>
-              </ol>
-            </li>
-            <li>
-              Set Redirect URLs
-              <ol>
-                <li>Go to "OAuth &amp; Permissions" page.</li>
-                <li>Add <code><script>document.write(location.origin);</script>/admin/notification/slackAuth</code> .</li>
-                <li>Don't forget to <strong>save</strong>.</li>
-              </ol>
-            </li>
             <li>
               Set Permission Scopes to the App
               <ol>
@@ -218,29 +176,17 @@
               Install the app
               <ol>
                 <li>Go to "Install App to Your Workspace" page and install.</li>
+                <li>Go to "OAuth &amp; Permissions" page and copy <code>OAuth Access Token</code>.</li>
               </ol>
             </li>
             <li>
-              (At Workspace) Approve the app
-              <ol>
-                <li>Go to the management Apps page for the workspace you installed the app and approve "growi".</li>
-              </ol>
-            </li>
-            <li>
-              (At Workspace) Invite the bot to your workspace
+              (At this page) Set OAuth Access Token
               <ol>
-                <li>Invite the user you created in <code>4. Add a bot user</code> to the channel you notify to.</li>
+                <li>Input "OAuth Access Token".</li>
+                <li>Don't forget to <strong>save</strong>.</li>
               </ol>
             </li>
-            <li>
-              (At GROWI admin page) Input "clientId" and "clientSecret" and submit on this page.
-            </li>
-            <li>
-              (At GROWI admin page) Click "Connect to Slack" button to start OAuth process.
-            </li>
           </ol>
-          {# {% endif %} #}
-
 
         </div><!-- /#slack-app -->
 

+ 9 - 0
lib/views/admin/widget/passport/ldap.html

@@ -122,9 +122,18 @@
               Specification of mappings when creating new users
             </small>
           </p>
+
+          <div class="checkbox checkbox-info">
+            <input type="checkbox" id="cbSameUsernameTreatedAsIdenticalUser" name="settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]" value="1"
+                {% if settingForm['security:passport-ldap:isSameUsernameTreatedAsIdenticalUser'] %}checked{% endif %} />
+            <label for="cbSameUsernameTreatedAsIdenticalUser">
+              {{ t("security_setting.Treat username matching as identical") }}
+            </label>
+          </div>
         </div>
       </div>
 
+
       <h4>Group Search Filter (Optional)</h4>
 
       <div class="form-group">

+ 2 - 2
lib/views/me/external-accounts.html

@@ -90,7 +90,7 @@
             <td class="text-center">
               <button class="btn btn-default btn-sm btn-danger"
                   data-toggle="modal" data-target="#diassociate-external-account" data-provider-type="{{ account.providerType }}" data-account-id="{{ account.accountId }}">
-                <i class="fa fa-unlink"></i>
+                <i class="ti-unlink"></i>
                 {{ t('Diassociate') }}
               </button>
             </td>
@@ -216,7 +216,7 @@
               {{ t('Cancel') }}
             </button>
             <button type="submit" class="btn btn-sm btn-danger">
-              <i class="fa fa-unlink"></i>
+              <i class="ti-unlink"></i>
               {{ t('Diassociate') }}
             </button>
           </form>

+ 1 - 1
lib/views/widget/page_alerts.html

@@ -45,7 +45,7 @@
         <input type="hidden" name="path" value="{{ page.path }}">
         <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
         <button type="submit" class="btn btn-default btn-sm pull-right">
-          <i class="fa fa-unlink" aria-hidden="true"></i>
+          <i class="ti-unlink" aria-hidden="true"></i>
           Unlink
         </button>
       </form>

+ 8 - 8
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.0.5-RC",
+  "version": "3.0.6-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -47,7 +47,6 @@
     "webpack": "webpack"
   },
   "dependencies": {
-    "@slack/client": "^3.14.0",
     "assets-webpack-plugin": "~3.5.1",
     "async": "^2.3.0",
     "autoprefixer": "^8.2.0",
@@ -87,11 +86,11 @@
     "express-webpack-assets": "^0.1.0",
     "extract-text-webpack-plugin": "^3.0.2",
     "file-loader": "^1.1.0",
-    "googleapis": "^27.0.0",
+    "googleapis": "^28.1.0",
     "graceful-fs": "^4.1.11",
     "growi-pluginkit": "^1.1.0",
-    "i18next": "^10.0.1",
-    "i18next-express-middleware": "^1.0.5",
+    "i18next": "^11.1.1",
+    "i18next-express-middleware": "^1.1.1",
     "i18next-node-fs-backend": "^1.0.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "jquery-slimscroll": "^1.3.8",
@@ -128,7 +127,7 @@
     "postcss-loader": "^2.1.3",
     "react": "^16.2.0",
     "react-bootstrap": "^0.32.1",
-    "react-bootstrap-typeahead": "^2.6.0",
+    "react-bootstrap-typeahead": "^3.0.3",
     "react-clipboard.js": "^1.1.3",
     "react-codemirror2": "^4.2.1",
     "react-dom": "^16.2.0",
@@ -136,14 +135,15 @@
     "redis": "^2.7.1",
     "reveal.js": "^3.5.0",
     "rimraf": "^2.6.1",
-    "sass-loader": "^6.0.3",
+    "sass-loader": "^7.0.1",
+    "slack-node": "^0.1.8",
     "socket.io": "^2.0.3",
     "socket.io-client": "^2.0.3",
     "style-loader": "^0.20.1",
     "swig-templates": "^2.0.2",
     "throttle-debounce": "^1.0.1",
     "toastr": "^2.1.2",
-    "uglifycss": "^0.0.28",
+    "uglifycss": "^0.0.29",
     "url-join": "^4.0.0",
     "uslug": "^1.0.4",
     "webpack": "3.11.0",

+ 2 - 4
test/util/slack.test.js

@@ -10,9 +10,7 @@ describe('Slack Util', function () {
   var crowi = new (require(ROOT_DIR + '/lib/crowi'))(ROOT_DIR, process.env);
   var slack = require(crowi.libDir + '/util/slack')(crowi);
 
-  it('convert markdown', function() {
-    var markdown = '# ほげほげ\n\n* aaa\n* bbb\n* ccc\n\n## ほげほげほげ\n\n[Yahoo! Japan](http://www.yahoo.co.jp/) is here\n**Bold** and *Italic*';
-    var markdownConverted = '\n*ほげほげ*\n\n• aaa\n• bbb\n• ccc\n\n\n*ほげほげほげ*\n\n<http://www.yahoo.co.jp/|Yahoo! Japan> is here\n**Bold** and *Italic*';
-    expect(slack.convertMarkdownToMrkdwn(markdown)).to.be.equal(markdownConverted);
+  it('post method exists', function() {
+    expect(slack).to.respondTo('post');
   });
 });

+ 117 - 173
yarn.lock

@@ -2,23 +2,6 @@
 # yarn lockfile v1
 
 
-"@slack/client@^3.14.0":
-  version "3.15.0"
-  resolved "https://registry.yarnpkg.com/@slack/client/-/client-3.15.0.tgz#796ee2b1182cd37fadbaeb37752121b2028a1704"
-  dependencies:
-    async "^1.5.0"
-    bluebird "^3.3.3"
-    eventemitter3 "^1.1.1"
-    https-proxy-agent "^1.0.0"
-    inherits "^2.0.1"
-    lodash "^4.13.1"
-    pkginfo "^0.4.0"
-    request ">=2.0.0 <2.77.0"
-    retry "^0.9.0"
-    url-join "0.0.1"
-    winston "^2.1.1"
-    ws "^1.0.1"
-
 "@types/body-parser@*":
   version "1.16.8"
   resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.8.tgz#687ec34140624a3bec2b1a8ea9268478ae8f3be3"
@@ -128,13 +111,6 @@ after@0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
 
-agent-base@2:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.1.1.tgz#d6de10d5af6132d5bd692427d46fc538539094c7"
-  dependencies:
-    extend "~3.0.0"
-    semver "~5.0.1"
-
 agentkeepalive@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-2.2.0.tgz#c5d1bd4b129008f1163f236f86e5faea2026e2ef"
@@ -370,7 +346,7 @@ async-limiter@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
 
-async@1.5.2, async@^1.5.0:
+async@1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
@@ -384,7 +360,7 @@ async@^0.9.0:
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
 
-async@^2.1.2, async@^2.1.5, async@^2.3.0, async@^2.4.1:
+async@^2.1.2, async@^2.3.0, async@^2.4.1:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
   dependencies:
@@ -394,10 +370,6 @@ async@~0.2.6:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
 
-async@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9"
-
 asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -1123,10 +1095,6 @@ bluebird@3.5.0:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
 
-bluebird@^3.3.3:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
-
 bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
   version "4.11.8"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
@@ -1604,14 +1572,14 @@ cliui@^3.0.3, cliui@^3.2.0:
     strip-ansi "^3.0.1"
     wrap-ansi "^2.0.0"
 
-clone-deep@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8"
+clone-deep@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713"
   dependencies:
     for-own "^1.0.0"
-    is-plain-object "^2.0.1"
-    kind-of "^3.2.2"
-    shallow-clone "^0.1.2"
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.0"
+    shallow-clone "^1.0.0"
 
 clone@^1.0.2:
   version "1.0.3"
@@ -1671,7 +1639,7 @@ colormin@^1.0.5:
     css-color-names "0.0.4"
     has "^1.0.1"
 
-colors@1.0.3, colors@1.0.x:
+colors@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
@@ -2030,10 +1998,6 @@ currently-unhandled@^0.4.1:
   dependencies:
     array-find-index "^1.0.1"
 
-cycle@1.0.x:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
-
 d@1:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@@ -2065,12 +2029,6 @@ debounce@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408"
 
-debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.2.0, debug@^2.6.8, debug@~2.6.4, debug@~2.6.6, debug@~2.6.9:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
-  dependencies:
-    ms "2.0.0"
-
 debug@2.6.4:
   version "2.6.4"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.4.tgz#7586a9b3c39741c0282ae33445c4e8ac74734fe0"
@@ -2083,6 +2041,12 @@ debug@2.6.8:
   dependencies:
     ms "2.0.0"
 
+debug@2.6.9, debug@^2.0.0, debug@^2.2.0, debug@^2.6.8, debug@~2.6.4, debug@~2.6.6, debug@~2.6.9:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  dependencies:
+    ms "2.0.0"
+
 debug@3.1.0, debug@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
@@ -2643,7 +2607,7 @@ event-stream@~3.3.0:
     stream-combiner "~0.0.4"
     through "~2.3.1"
 
-eventemitter3@1.x.x, eventemitter3@^1.1.1:
+eventemitter3@1.x.x:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
 
@@ -2753,7 +2717,7 @@ express@^4.15.2, express@^4.16.1:
     utils-merge "1.0.1"
     vary "~1.1.2"
 
-extend@3, extend@^3.0.1, extend@~3.0.0, extend@~3.0.1:
+extend@^3.0.0, extend@^3.0.1, extend@~3.0.0, extend@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
 
@@ -2792,10 +2756,6 @@ extsprintf@^1.2.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
 
-eyes@0.1.x:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
-
 fast-deep-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
@@ -3207,9 +3167,9 @@ google-p12-pem@^1.0.0:
     node-forge "^0.7.1"
     pify "^3.0.0"
 
-googleapis@^27.0.0:
-  version "27.0.0"
-  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-27.0.0.tgz#c210633b43e7047b65d33da40c489b6d8f9c02b8"
+googleapis@^28.1.0:
+  version "28.1.0"
+  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-28.1.0.tgz#f78ce5751581387274f8eb22eee947a13c7c4285"
   dependencies:
     google-auth-library "^1.3.1"
     pify "^3.0.0"
@@ -3446,17 +3406,9 @@ https-browserify@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
 
-https-proxy-agent@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6"
-  dependencies:
-    agent-base "2"
-    debug "2"
-    extend "3"
-
-i18next-express-middleware@^1.0.5:
-  version "1.0.9"
-  resolved "https://registry.yarnpkg.com/i18next-express-middleware/-/i18next-express-middleware-1.0.9.tgz#5f198d2166794f0dbaaca2c9b613c0795485dfbe"
+i18next-express-middleware@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/i18next-express-middleware/-/i18next-express-middleware-1.1.1.tgz#9204f28c8800ac3bff87fbee01945367956f349c"
   dependencies:
     cookies "0.7.1"
 
@@ -3471,9 +3423,9 @@ i18next-sprintf-postprocessor@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/i18next-sprintf-postprocessor/-/i18next-sprintf-postprocessor-0.2.2.tgz#2e409f1043579382698b6a2da70cdaa551d67ea4"
 
-i18next@^10.0.1:
-  version "10.2.2"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-10.2.2.tgz#1f2dc55ca2e8d7e071f7aff9f78654ef7f003c0e"
+i18next@^11.1.1:
+  version "11.1.1"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.1.1.tgz#df3a683542d7756a8aa8d6b884b61141239c394a"
 
 iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
   version "0.4.19"
@@ -3593,7 +3545,7 @@ is-binary-path@^1.0.0:
   dependencies:
     binary-extensions "^1.0.0"
 
-is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.1:
+is-buffer@^1.1.5, is-buffer@~1.1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
 
@@ -3706,7 +3658,7 @@ is-plain-obj@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
 
-is-plain-object@^2.0.1:
+is-plain-object@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
   dependencies:
@@ -3801,7 +3753,7 @@ isomorphic-fetch@^2.1.1:
     node-fetch "^1.0.1"
     whatwg-fetch ">=0.10.0"
 
-isstream@0.1.x, isstream@~0.1.2:
+isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
 
@@ -3978,13 +3930,7 @@ keygrip@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91"
 
-kind-of@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5"
-  dependencies:
-    is-buffer "^1.0.2"
-
-kind-of@^3.0.2, kind-of@^3.2.2:
+kind-of@^3.0.2:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
   dependencies:
@@ -3996,9 +3942,13 @@ kind-of@^4.0.0:
   dependencies:
     is-buffer "^1.1.5"
 
-lazy-cache@^0.2.3:
-  version "0.2.7"
-  resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
+kind-of@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+
+kind-of@^6.0.0:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
 
 lazy-cache@^1.0.3:
   version "1.0.4"
@@ -4310,11 +4260,11 @@ lodash@^3.10.1:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
-lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@~4.17.4:
+lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@~4.17.4:
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
-lodash@^4.2.0, lodash@^4.3.0:
+lodash@^4.15.0, lodash@^4.2.0, lodash@^4.3.0:
   version "4.17.5"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
 
@@ -4777,6 +4727,10 @@ negotiator@0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
 
+neo-async@^2.5.0:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee"
+
 nise@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/nise/-/nise-1.2.0.tgz#079d6cadbbcb12ba30e38f1c999f36ad4d6baa53"
@@ -4913,10 +4867,6 @@ node-sass@^4.5.0:
     stdout-stream "^1.4.0"
     "true-case-path" "^1.0.2"
 
-node-uuid@~1.4.7:
-  version "1.4.8"
-  resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907"
-
 nodemailer-ses-transport@~1.5.0:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/nodemailer-ses-transport/-/nodemailer-ses-transport-1.5.1.tgz#dc0598c1bf53e8652e632e8f31692ce022d7dea9"
@@ -5139,10 +5089,6 @@ optionator@^0.8.2:
     type-check "~0.3.2"
     wordwrap "~1.0.0"
 
-options@>=0.0.5:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
-
 os-browserify@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
@@ -5433,10 +5379,6 @@ pkg-dir@^2.0.0:
   dependencies:
     find-up "^2.1.0"
 
-pkginfo@^0.4.0:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
-
 plantuml-encoder@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/plantuml-encoder/-/plantuml-encoder-1.2.5.tgz#6b8e5b9e1a1dbd88b3fd9fb46f734eec7d44b548"
@@ -5448,6 +5390,10 @@ pluralize@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
 
+popper.js@^1.14.1:
+  version "1.14.3"
+  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
+
 portscanner@2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.1.1.tgz#eabb409e4de24950f5a2a516d35ae769343fbb96"
@@ -5793,6 +5739,14 @@ prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8,
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
+prop-types@^15.6.1:
+  version "15.6.1"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
+  dependencies:
+    fbjs "^0.8.16"
+    loose-envify "^1.3.1"
+    object-assign "^4.1.1"
+
 proxy-addr@~2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
@@ -5933,9 +5887,9 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
-react-bootstrap-typeahead@^2.6.0:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/react-bootstrap-typeahead/-/react-bootstrap-typeahead-2.6.0.tgz#d18fd808df2b2f339bd137ff36d7f4b0e6c2cb26"
+react-bootstrap-typeahead@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/react-bootstrap-typeahead/-/react-bootstrap-typeahead-3.0.3.tgz#63998f9576f0e5cd2498d7addc55e65b77b6cd6b"
   dependencies:
     classnames "^2.2.0"
     escape-string-regexp "^1.0.5"
@@ -5945,6 +5899,7 @@ react-bootstrap-typeahead@^2.6.0:
     prop-types-extra "^1.0.1"
     react-onclickoutside "^6.1.1"
     react-overlays "^0.8.1"
+    react-popper "^0.9.0"
     warning "^3.0.0"
 
 react-bootstrap@^0.32.1:
@@ -6006,6 +5961,13 @@ react-overlays@^0.8.0, react-overlays@^0.8.1:
     react-transition-group "^2.2.0"
     warning "^3.0.0"
 
+react-popper@^0.9.0:
+  version "0.9.5"
+  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.9.5.tgz#02a24ef3eec33af9e54e8358ab70eb0e331edd05"
+  dependencies:
+    popper.js "^1.14.1"
+    prop-types "^15.6.1"
+
 react-prop-types@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/react-prop-types/-/react-prop-types-0.4.0.tgz#f99b0bfb4006929c9af2051e7c1414a5c75b93d0"
@@ -6267,30 +6229,32 @@ request@2.81.0:
     tunnel-agent "^0.6.0"
     uuid "^3.0.0"
 
-"request@>=2.0.0 <2.77.0":
-  version "2.76.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.76.0.tgz#be44505afef70360a0436955106be3945d95560e"
+request@^2.74.0:
+  version "2.85.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
   dependencies:
-    aws-sign2 "~0.6.0"
-    aws4 "^1.2.1"
-    caseless "~0.11.0"
+    aws-sign2 "~0.7.0"
+    aws4 "^1.6.0"
+    caseless "~0.12.0"
     combined-stream "~1.0.5"
-    extend "~3.0.0"
+    extend "~3.0.1"
     forever-agent "~0.6.1"
-    form-data "~2.1.1"
-    har-validator "~2.0.6"
-    hawk "~3.1.3"
-    http-signature "~1.1.0"
+    form-data "~2.3.1"
+    har-validator "~5.0.3"
+    hawk "~6.0.2"
+    http-signature "~1.2.0"
     is-typedarray "~1.0.0"
     isstream "~0.1.2"
     json-stringify-safe "~5.0.1"
-    mime-types "~2.1.7"
-    node-uuid "~1.4.7"
-    oauth-sign "~0.8.1"
-    qs "~6.3.0"
-    stringstream "~0.0.4"
-    tough-cookie "~2.3.0"
-    tunnel-agent "~0.4.1"
+    mime-types "~2.1.17"
+    oauth-sign "~0.8.2"
+    performance-now "^2.1.0"
+    qs "~6.5.1"
+    safe-buffer "^5.1.1"
+    stringstream "~0.0.5"
+    tough-cookie "~2.3.3"
+    tunnel-agent "^0.6.0"
+    uuid "^3.1.0"
 
 request@~2.79.0:
   version "2.79.0"
@@ -6317,6 +6281,15 @@ request@~2.79.0:
     tunnel-agent "~0.4.1"
     uuid "^3.0.0"
 
+requestretry@^1.2.2:
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/requestretry/-/requestretry-1.13.0.tgz#213ec1006eeb750e8b8ce54176283d15a8d55d94"
+  dependencies:
+    extend "^3.0.0"
+    lodash "^4.15.0"
+    request "^2.74.0"
+    when "^3.7.7"
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -6387,10 +6360,6 @@ retry-axios@^0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.3.2.tgz#5757c80f585b4cc4c4986aa2ffd47a60c6d35e13"
 
-retry@^0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/retry/-/retry-0.9.0.tgz#6f697e50a0e4ddc8c8f7fb547a9b60dead43678d"
-
 reveal.js@^3.5.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/reveal.js/-/reveal.js-3.6.0.tgz#ce0e64f30cbebd6e5ce885c2f384085c5e5821e8"
@@ -6469,14 +6438,14 @@ sass-graph@^2.2.4:
     scss-tokenizer "^0.2.3"
     yargs "^7.0.0"
 
-sass-loader@^6.0.3:
-  version "6.0.6"
-  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9"
+sass-loader@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.0.1.tgz#fd937259ccba3a9cfe0d5f8a98746d48adfcc261"
   dependencies:
-    async "^2.1.5"
-    clone-deep "^0.3.0"
+    clone-deep "^2.0.1"
     loader-utils "^1.0.1"
     lodash.tail "^4.1.1"
+    neo-async "^2.5.0"
     pify "^3.0.0"
 
 sax@1.2.1:
@@ -6522,10 +6491,6 @@ select@^1.1.2:
   version "5.4.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
 
-semver@~5.0.1:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a"
-
 semver@~5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -6631,13 +6596,12 @@ sha.js@^2.4.0, sha.js@^2.4.8:
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
 
-shallow-clone@^0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060"
+shallow-clone@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571"
   dependencies:
     is-extendable "^0.1.1"
-    kind-of "^2.0.1"
-    lazy-cache "^0.2.3"
+    kind-of "^5.0.0"
     mixin-object "^2.0.1"
 
 shebang-command@^1.2.0:
@@ -6683,6 +6647,12 @@ sinon@^4.0.0:
     supports-color "^4.4.0"
     type-detect "^4.0.5"
 
+slack-node@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/slack-node/-/slack-node-0.1.8.tgz#cda98de8681485b301dc6742ddc3897117fad349"
+  dependencies:
+    requestretry "^1.2.2"
+
 slash@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@@ -6832,10 +6802,6 @@ sshpk@^1.7.0:
     jsbn "~0.1.0"
     tweetnacl "~0.14.0"
 
-stack-trace@0.0.x:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
-
 "statuses@>= 1.3.1 < 2":
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
@@ -7238,9 +7204,9 @@ uglify-to-browserify@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
 
-uglifycss@^0.0.28:
-  version "0.0.28"
-  resolved "https://registry.yarnpkg.com/uglifycss/-/uglifycss-0.0.28.tgz#b865ae5f66e4fcec6ea1d979e150dc5bf37ceb1c"
+uglifycss@^0.0.29:
+  version "0.0.29"
+  resolved "https://registry.yarnpkg.com/uglifycss/-/uglifycss-0.0.29.tgz#abe49531155d146e75dd2fdf933d371bc1180054"
 
 uglifyjs-webpack-plugin@^0.4.6:
   version "0.4.6"
@@ -7266,10 +7232,6 @@ uid-safe@~2.1.5:
   dependencies:
     random-bytes "~1.0.0"
 
-ultron@1.0.x:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
-
 ultron@~1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
@@ -7306,10 +7268,6 @@ unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
 
-url-join@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8"
-
 url-join@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a"
@@ -7505,6 +7463,10 @@ whatwg-fetch@>=0.10.0, whatwg-fetch@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
 
+when@^3.7.7:
+  version "3.7.8"
+  resolved "https://registry.yarnpkg.com/when/-/when-3.7.8.tgz#c7130b6a7ea04693e842cdc9e7a1f2aa39a39f82"
+
 whet.extend@~0.9.9:
   version "0.9.9"
   resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
@@ -7541,17 +7503,6 @@ window-size@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
 
-winston@^2.1.1:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.0.tgz#808050b93d52661ed9fb6c26b3f0c826708b0aee"
-  dependencies:
-    async "~1.0.0"
-    colors "1.0.x"
-    cycle "1.0.x"
-    eyes "0.1.x"
-    isstream "0.1.x"
-    stack-trace "0.0.x"
-
 wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
@@ -7581,13 +7532,6 @@ write@^0.2.1:
   dependencies:
     mkdirp "^0.5.1"
 
-ws@^1.0.1:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.5.tgz#cbd9e6e75e09fc5d2c90015f21f0c40875e0dd51"
-  dependencies:
-    options ">=0.0.5"
-    ultron "1.0.x"
-
 ws@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/ws/-/ws-4.0.0.tgz#bfe1da4c08eeb9780b986e0e4d10eccd7345999f"