diff --git a/frontend/components/SlackAuthLink/SlackAuthLink.js b/frontend/components/SlackAuthLink/SlackAuthLink.js
index c07735a9..3e146148 100644
--- a/frontend/components/SlackAuthLink/SlackAuthLink.js
+++ b/frontend/components/SlackAuthLink/SlackAuthLink.js
@@ -8,6 +8,7 @@ class SlackAuthLink extends React.Component {
static propTypes = {
scopes: React.PropTypes.arrayOf(React.PropTypes.string),
user: React.PropTypes.object.isRequired,
+ redirectUri: React.PropTypes.string,
}
static defaultProps = {
@@ -24,7 +25,7 @@ class SlackAuthLink extends React.Component {
const params = {
client_id: SLACK_KEY,
scope: this.props.scopes.join(' '),
- redirect_uri: SLACK_REDIRECT_URI,
+ redirect_uri: this.props.redirectUri || SLACK_REDIRECT_URI,
state: this.props.user.getOauthState(),
};
diff --git a/frontend/index.js b/frontend/index.js
index 14778eb0..6d496192 100644
--- a/frontend/index.js
+++ b/frontend/index.js
@@ -71,6 +71,7 @@ render((
+
diff --git a/frontend/scenes/Dashboard/Dashboard.js b/frontend/scenes/Dashboard/Dashboard.js
index a68c3677..04ebdd38 100644
--- a/frontend/scenes/Dashboard/Dashboard.js
+++ b/frontend/scenes/Dashboard/Dashboard.js
@@ -8,9 +8,7 @@ import Layout from 'components/Layout';
import AtlasPreview from 'components/AtlasPreview';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
-// import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu';
-
-// import styles from './Dashboard.scss';
+import SlackAuthLink from 'components/SlackAuthLink';
@observer(['user'])
class Dashboard extends React.Component {
@@ -23,16 +21,6 @@ class Dashboard extends React.Component {
}
render() {
- // const actions = (
- //
- // } >
- //
- //
- //
- // );
-
return (
@@ -44,6 +32,7 @@ class Dashboard extends React.Component {
return ();
}) }
+
diff --git a/frontend/scenes/SlackAuth/SlackAuth.js b/frontend/scenes/SlackAuth/SlackAuth.js
index f4153380..44a3eddb 100644
--- a/frontend/scenes/SlackAuth/SlackAuth.js
+++ b/frontend/scenes/SlackAuth/SlackAuth.js
@@ -1,16 +1,31 @@
import React from 'react';
import { observer } from 'mobx-react';
+import { browserHistory } from 'react-router';
+import { client } from 'utils/ApiClient';
@observer(['user'])
class SlackAuth extends React.Component {
static propTypes = {
user: React.PropTypes.object.isRequired,
location: React.PropTypes.object.isRequired,
+ route: React.PropTypes.object.isRequired,
}
- componentDidMount = () => {
+ componentDidMount = async () => {
const { code, state } = this.props.location.query;
- this.props.user.authWithSlack(code, state);
+
+ if (this.props.route.apiPath) {
+ try {
+ await client.post(this.props.route.apiPath, { code });
+ browserHistory.replace('/dashboard');
+ } catch (e) {
+ browserHistory.push('/auth-error');
+ return;
+ }
+ } else {
+ // Regular Slack authentication
+ this.props.user.authWithSlack(code, state);
+ }
}
render() {
diff --git a/server/api/auth.js b/server/api/auth.js
index 2a1fcce8..351331ad 100644
--- a/server/api/auth.js
+++ b/server/api/auth.js
@@ -15,7 +15,7 @@ router.post('auth.slack', async (ctx) => {
const body = {
client_id: process.env.SLACK_KEY,
client_secret: process.env.SLACK_SECRET,
- redirect_uri: process.env.SLACK_REDIRECT_URI,
+ redirect_uri: `${process.env.URL}/auth/slack/`,
code,
};
@@ -78,4 +78,29 @@ router.post('auth.slack', async (ctx) => {
} };
});
+router.post('auth.slackCommands', async (ctx) => {
+ const { code } = ctx.body;
+ ctx.assertPresent(code, 'code is required');
+
+ const body = {
+ client_id: process.env.SLACK_KEY,
+ client_secret: process.env.SLACK_SECRET,
+ redirect_uri: `${process.env.URL}/auth/slack/commands`,
+ code,
+ };
+
+ let data;
+ try {
+ const response = await fetch(`https://slack.com/api/oauth.access?${querystring.stringify(body)}`);
+ data = await response.json();
+ } catch (e) {
+ throw httpErrors.BadRequest();
+ }
+
+ if (!data.ok) throw httpErrors.BadRequest(data.error);
+
+ ctx.body = { success: true };
+});
+
+
export default router;
diff --git a/server/api/documents.js b/server/api/documents.js
index 6c9f3f03..f528bfe0 100644
--- a/server/api/documents.js
+++ b/server/api/documents.js
@@ -78,25 +78,7 @@ router.post('documents.search', auth(), async (ctx) => {
const user = await ctx.state.user;
- const sql = `
- SELECT * FROM documents
- WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
- "teamId" = '${user.teamId}'::uuid AND
- "deletedAt" IS NULL
- ORDER BY ts_rank(documents."searchVector", plainto_tsquery('english', :query))
- DESC;
- `;
-
- const documents = await sequelize
- .query(
- sql,
- {
- replacements: {
- query,
- },
- model: Document,
- }
- );
+ const documents = await Document.searchForUser(user, query);
const data = [];
await Promise.all(documents.map(async (document) => {
diff --git a/server/api/hooks.js b/server/api/hooks.js
new file mode 100644
index 00000000..c4950382
--- /dev/null
+++ b/server/api/hooks.js
@@ -0,0 +1,49 @@
+import Router from 'koa-router';
+import httpErrors from 'http-errors';
+import { Document, User } from '../models';
+
+const router = new Router();
+
+router.post('hooks.slack', async (ctx) => {
+ const {
+ token,
+ user_id,
+ text,
+ } = ctx.body;
+ ctx.assertPresent(token, 'token is required');
+ ctx.assertPresent(user_id, 'user_id is required');
+ ctx.assertPresent(text, 'text is required');
+
+ if (token !== process.env.SLACK_VERIFICATION_TOKEN) throw httpErrors.BadRequest('Invalid token');
+
+ const user = await User.find({
+ where: {
+ slackId: user_id,
+ },
+ });
+
+ if (!user) throw httpErrors.BadRequest('Invalid user');
+
+ const documents = await Document.searchForUser(user, text, {
+ limit: 5,
+ });
+
+ const results = [];
+ let number = 1;
+ for (const document of documents) {
+ results.push(`${number}. <${process.env.URL}${document.getUrl()}|${document.title}>`);
+ number += 1;
+ }
+
+ ctx.body = {
+ text: 'Search results:',
+ attachments: [
+ {
+ text: results.join('\n'),
+ color: '#3AA3E3',
+ },
+ ],
+ };
+});
+
+export default router;
diff --git a/server/models/Document.js b/server/models/Document.js
index 6e2052f2..5b79d964 100644
--- a/server/models/Document.js
+++ b/server/models/Document.js
@@ -99,4 +99,35 @@ const Document = sequelize.define('document', {
Document.belongsTo(User);
+Document.searchForUser = async (user, query, options = {}) => {
+ const limit = options.limit || 15;
+ const offset = options.offset || 0;
+
+ const sql = `
+ SELECT * FROM documents
+ WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
+ "teamId" = '${user.teamId}'::uuid AND
+ "deletedAt" IS NULL
+ ORDER BY ts_rank(documents."searchVector", plainto_tsquery('english', :query))
+ LIMIT :limit
+ OFFSET :offset
+ DESC;
+ `;
+
+ const documents = await sequelize
+ .query(
+ sql,
+ {
+ replacements: {
+ query,
+ limit,
+ offset,
+ },
+ model: Document,
+ }
+ );
+
+ return documents;
+}
+
export default Document;
diff --git a/server/models/User.js b/server/models/User.js
index 6eab9086..a7a0ba42 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -24,7 +24,7 @@ const User = sequelize.define('user', {
},
async getTeam() {
return this.team;
- }
+ },
},
indexes: [
{
diff --git a/webpack.config.js b/webpack.config.js
index 38d4b4cc..7f3765bc 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -10,6 +10,7 @@ var definePlugin = new webpack.DefinePlugin({
__PRERELEASE__: JSON.stringify(JSON.parse(process.env.BUILD_PRERELEASE || 'false')),
SLACK_REDIRECT_URI: JSON.stringify(process.env.SLACK_REDIRECT_URI),
SLACK_KEY: JSON.stringify(process.env.SLACK_KEY),
+ URL: JSON.stringify(process.env.URL),
});
module.exports = {