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 = ( - // - // } > - // - // Add collection - // - // - // - // ); - 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 = {