From 5bc3cea6fe4a909735753692d0c4cd960e8ab516 Mon Sep 17 00:00:00 2001 From: enahum Date: Wed, 3 Aug 2016 12:19:27 -0500 Subject: PLT-3484 OAuth2 Service Provider (#3632) * PLT-3484 OAuth2 Service Provider * PM text review for OAuth 2.0 Service Provider * PLT-3484 OAuth2 Service Provider UI tweaks (#3668) * Tweaks to help text * Pushing OAuth improvements (#3680) * Re-arrange System Console for OAuth 2.0 Provider --- webapp/actions/global_actions.jsx | 7 - webapp/actions/oauth_actions.jsx | 60 +++ webapp/client/client.jsx | 30 ++ webapp/components/admin_console/admin_settings.jsx | 17 +- webapp/components/admin_console/admin_sidebar.jsx | 6 +- .../admin_console/custom_integrations_settings.jsx | 180 +++++++++ .../components/admin_console/webhook_settings.jsx | 161 -------- webapp/components/authorize.jsx | 75 ++-- .../backstage/components/backstage_sidebar.jsx | 32 +- .../integrations/components/add_oauth_app.jsx | 435 +++++++++++++++++++++ .../components/installed_oauth_app.jsx | 219 +++++++++++ .../components/installed_oauth_apps.jsx | 108 +++++ .../integrations/components/integrations.jsx | 30 +- webapp/components/login/login_controller.jsx | 18 +- webapp/components/navbar_dropdown.jsx | 26 +- webapp/components/needs_team.jsx | 2 - webapp/components/register_app_modal.jsx | 411 ------------------- webapp/components/signup_user_complete.jsx | 14 +- webapp/components/user_settings/user_settings.jsx | 12 - .../user_settings/user_settings_developer.jsx | 138 ------- .../user_settings/user_settings_modal.jsx | 8 - webapp/i18n/en.json | 67 ++-- webapp/images/oauth_icon.png | Bin 0 -> 25529 bytes webapp/images/webhook_icon.jpg | Bin 68190 -> 20565 bytes webapp/root.html | 9 +- webapp/routes/route_admin_console.jsx | 8 +- webapp/routes/route_integrations.jsx | 16 + webapp/routes/route_root.jsx | 6 + webapp/sass/components/_oauth.scss | 11 +- webapp/sass/responsive/_mobile.scss | 17 + webapp/sass/routes/_backstage.scss | 20 +- webapp/stores/integration_store.jsx | 43 ++ webapp/stores/modal_store.jsx | 1 - webapp/utils/constants.jsx | 4 +- 34 files changed, 1331 insertions(+), 860 deletions(-) create mode 100644 webapp/actions/oauth_actions.jsx create mode 100644 webapp/components/admin_console/custom_integrations_settings.jsx delete mode 100644 webapp/components/admin_console/webhook_settings.jsx create mode 100644 webapp/components/integrations/components/add_oauth_app.jsx create mode 100644 webapp/components/integrations/components/installed_oauth_app.jsx create mode 100644 webapp/components/integrations/components/installed_oauth_apps.jsx delete mode 100644 webapp/components/register_app_modal.jsx delete mode 100644 webapp/components/user_settings/user_settings_developer.jsx create mode 100644 webapp/images/oauth_icon.png (limited to 'webapp') diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx index ba92255ce..829424c1f 100644 --- a/webapp/actions/global_actions.jsx +++ b/webapp/actions/global_actions.jsx @@ -308,13 +308,6 @@ export function showLeaveTeamModal() { }); } -export function showRegisterAppModal() { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_REGISTER_APP_MODAL, - value: true - }); -} - export function emitSuggestionPretextChanged(suggestionId, pretext) { AppDispatcher.handleViewAction({ type: ActionTypes.SUGGESTION_PRETEXT_CHANGED, diff --git a/webapp/actions/oauth_actions.jsx b/webapp/actions/oauth_actions.jsx new file mode 100644 index 000000000..d2e5b0c98 --- /dev/null +++ b/webapp/actions/oauth_actions.jsx @@ -0,0 +1,60 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Client from 'client/web_client.jsx'; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import Constants from 'utils/constants.jsx'; + +const ActionTypes = Constants.ActionTypes; + +export function listOAuthApps(userId, onSuccess, onError) { + Client.listOAuthApps( + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_OAUTHAPPS, + userId, + oauthApps: data + }); + + if (onSuccess) { + onSuccess(data); + } + }, + onError + ); +} + +export function deleteOAuthApp(id, userId, onSuccess, onError) { + Client.deleteOAuthApp( + id, + () => { + AppDispatcher.handleServerAction({ + type: ActionTypes.REMOVED_OAUTHAPP, + userId, + id + }); + + if (onSuccess) { + onSuccess(); + } + }, + onError + ); +} + +export function registerOAuthApp(app, onSuccess, onError) { + Client.registerOAuthApp( + app, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_OAUTHAPP, + oauthApp: data + }); + + if (onSuccess) { + onSuccess(); + } + }, + onError + ); +} \ No newline at end of file diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index cf015bc84..b200b2379 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -1498,6 +1498,36 @@ export default class Client { end(this.handleResponse.bind(this, 'allowOAuth2', success, error)); } + listOAuthApps(success, error) { + request. + get(`${this.getOAuthRoute()}/list`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(). + end(this.handleResponse.bind(this, 'getOAuthApps', success, error)); + } + + deleteOAuthApp(id, success, error) { + request. + post(`${this.getOAuthRoute()}/delete`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({id}). + end(this.handleResponse.bind(this, 'deleteOAuthApp', success, error)); + } + + getOAuthAppInfo(id, success, error) { + request. + get(`${this.getOAuthRoute()}/app/${id}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(). + end(this.handleResponse.bind(this, 'getOAuthAppInfo', success, error)); + } + // Routes for Hooks addIncomingHook(hook, success, error) { diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx index d670d599d..8601722eb 100644 --- a/webapp/components/admin_console/admin_settings.jsx +++ b/webapp/components/admin_console/admin_settings.jsx @@ -8,7 +8,6 @@ import Client from 'client/web_client.jsx'; import FormError from 'components/form_error.jsx'; import SaveButton from 'components/admin_console/save_button.jsx'; -import Constants from 'utils/constants.jsx'; export default class AdminSettings extends React.Component { static get propTypes() { @@ -22,7 +21,6 @@ export default class AdminSettings extends React.Component { this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); this.state = Object.assign(this.getStateFromConfig(props.config), { saveNeeded: false, @@ -38,20 +36,6 @@ export default class AdminSettings extends React.Component { }); } - componentDidMount() { - document.addEventListener('keydown', this.onKeyDown); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.onKeyDown); - } - - onKeyDown(e) { - if (e.keyCode === Constants.KeyCodes.ENTER) { - this.handleSubmit(e); - } - } - handleSubmit(e) { e.preventDefault(); @@ -118,6 +102,7 @@ export default class AdminSettings extends React.Component {
{this.renderSettings()}
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx index d812b83fd..6634d4ac6 100644 --- a/webapp/components/admin_console/admin_sidebar.jsx +++ b/webapp/components/admin_console/admin_sidebar.jsx @@ -521,11 +521,11 @@ export default class AdminSidebar extends React.Component { } > } /> diff --git a/webapp/components/admin_console/custom_integrations_settings.jsx b/webapp/components/admin_console/custom_integrations_settings.jsx new file mode 100644 index 000000000..cfa1a30ae --- /dev/null +++ b/webapp/components/admin_console/custom_integrations_settings.jsx @@ -0,0 +1,180 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import AdminSettings from './admin_settings.jsx'; +import BooleanSetting from './boolean_setting.jsx'; +import {FormattedHTMLMessage, FormattedMessage} from 'react-intl'; +import SettingsGroup from './settings_group.jsx'; + +export default class WebhookSettings extends AdminSettings { + constructor(props) { + super(props); + + this.getConfigFromState = this.getConfigFromState.bind(this); + + this.renderSettings = this.renderSettings.bind(this); + } + + getConfigFromState(config) { + config.ServiceSettings.EnableIncomingWebhooks = this.state.enableIncomingWebhooks; + config.ServiceSettings.EnableOutgoingWebhooks = this.state.enableOutgoingWebhooks; + config.ServiceSettings.EnableCommands = this.state.enableCommands; + config.ServiceSettings.EnableOnlyAdminIntegrations = this.state.enableOnlyAdminIntegrations; + config.ServiceSettings.EnablePostUsernameOverride = this.state.enablePostUsernameOverride; + config.ServiceSettings.EnablePostIconOverride = this.state.enablePostIconOverride; + config.ServiceSettings.EnableOAuthServiceProvider = this.state.enableOAuthServiceProvider; + + return config; + } + + getStateFromConfig(config) { + return { + enableIncomingWebhooks: config.ServiceSettings.EnableIncomingWebhooks, + enableOutgoingWebhooks: config.ServiceSettings.EnableOutgoingWebhooks, + enableCommands: config.ServiceSettings.EnableCommands, + enableOnlyAdminIntegrations: config.ServiceSettings.EnableOnlyAdminIntegrations, + enablePostUsernameOverride: config.ServiceSettings.EnablePostUsernameOverride, + enablePostIconOverride: config.ServiceSettings.EnablePostIconOverride, + enableOAuthServiceProvider: config.ServiceSettings.EnableOAuthServiceProvider + }; + } + + renderTitle() { + return ( +

+ +

+ ); + } + + renderSettings() { + return ( + + + } + helpText={ + + } + value={this.state.enableIncomingWebhooks} + onChange={this.handleChange} + /> + + } + helpText={ + + } + value={this.state.enableOutgoingWebhooks} + onChange={this.handleChange} + /> + + } + helpText={ + + } + value={this.state.enableCommands} + onChange={this.handleChange} + /> + + } + helpText={ + + } + value={this.state.enableOAuthServiceProvider} + onChange={this.handleChange} + /> + + } + helpText={ + + } + value={this.state.enableOnlyAdminIntegrations} + onChange={this.handleChange} + /> + + } + helpText={ + + } + value={this.state.enablePostUsernameOverride} + onChange={this.handleChange} + /> + + } + helpText={ + + } + value={this.state.enablePostIconOverride} + onChange={this.handleChange} + /> + + ); + } +} diff --git a/webapp/components/admin_console/webhook_settings.jsx b/webapp/components/admin_console/webhook_settings.jsx deleted file mode 100644 index ba2443442..000000000 --- a/webapp/components/admin_console/webhook_settings.jsx +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import AdminSettings from './admin_settings.jsx'; -import BooleanSetting from './boolean_setting.jsx'; -import {FormattedHTMLMessage, FormattedMessage} from 'react-intl'; -import SettingsGroup from './settings_group.jsx'; - -export default class WebhookSettings extends AdminSettings { - constructor(props) { - super(props); - - this.getConfigFromState = this.getConfigFromState.bind(this); - - this.renderSettings = this.renderSettings.bind(this); - } - - getConfigFromState(config) { - config.ServiceSettings.EnableIncomingWebhooks = this.state.enableIncomingWebhooks; - config.ServiceSettings.EnableOutgoingWebhooks = this.state.enableOutgoingWebhooks; - config.ServiceSettings.EnableCommands = this.state.enableCommands; - config.ServiceSettings.EnableOnlyAdminIntegrations = this.state.enableOnlyAdminIntegrations; - config.ServiceSettings.EnablePostUsernameOverride = this.state.enablePostUsernameOverride; - config.ServiceSettings.EnablePostIconOverride = this.state.enablePostIconOverride; - - return config; - } - - getStateFromConfig(config) { - return { - enableIncomingWebhooks: config.ServiceSettings.EnableIncomingWebhooks, - enableOutgoingWebhooks: config.ServiceSettings.EnableOutgoingWebhooks, - enableCommands: config.ServiceSettings.EnableCommands, - enableOnlyAdminIntegrations: config.ServiceSettings.EnableOnlyAdminIntegrations, - enablePostUsernameOverride: config.ServiceSettings.EnablePostUsernameOverride, - enablePostIconOverride: config.ServiceSettings.EnablePostIconOverride - }; - } - - renderTitle() { - return ( -

- -

- ); - } - - renderSettings() { - return ( - - - } - helpText={ - - } - value={this.state.enableIncomingWebhooks} - onChange={this.handleChange} - /> - - } - helpText={ - - } - value={this.state.enableOutgoingWebhooks} - onChange={this.handleChange} - /> - - } - helpText={ - - } - value={this.state.enableCommands} - onChange={this.handleChange} - /> - - } - helpText={ - - } - value={this.state.enableOnlyAdminIntegrations} - onChange={this.handleChange} - /> - - } - helpText={ - - } - value={this.state.enablePostUsernameOverride} - onChange={this.handleChange} - /> - - } - helpText={ - - } - value={this.state.enablePostIconOverride} - onChange={this.handleChange} - /> - - ); - } -} diff --git a/webapp/components/authorize.jsx b/webapp/components/authorize.jsx index 49ca0f36b..354b51ede 100644 --- a/webapp/components/authorize.jsx +++ b/webapp/components/authorize.jsx @@ -10,6 +10,13 @@ import React from 'react'; import icon50 from 'images/icon50x50.png'; export default class Authorize extends React.Component { + static get propTypes() { + return { + location: React.PropTypes.object.isRequired, + params: React.PropTypes.object.isRequired + }; + } + constructor(props) { super(props); @@ -18,17 +25,31 @@ export default class Authorize extends React.Component { this.state = {}; } + + componentWillMount() { + Client.getOAuthAppInfo( + this.props.location.query.client_id, + (app) => { + this.setState({app}); + } + ); + } + + componentDidMount() { + // if we get to this point remove the antiClickjack blocker + const blocker = document.getElementById('antiClickjack'); + if (blocker) { + blocker.parentNode.removeChild(blocker); + } + } + handleAllow() { - const responseType = this.props.responseType; - const clientId = this.props.clientId; - const redirectUri = this.props.redirectUri; - const state = this.props.state; - const scope = this.props.scope; + const params = this.props.location.query; - Client.allowOAuth2(responseType, clientId, redirectUri, state, scope, + Client.allowOAuth2(params.response_type, params.client_id, params.redirect_uri, params.state, params.scope, (data) => { if (data.redirect) { - window.location.replace(data.redirect); + window.location.href = data.redirect; } }, () => { @@ -36,28 +57,42 @@ export default class Authorize extends React.Component { } ); } + handleDeny() { - window.location.replace(this.props.redirectUri + '?error=access_denied'); + window.location.replace(this.props.location.query.redirect_uri + '?error=access_denied'); } + render() { + const app = this.state.app; + if (!app) { + return null; + } + + let icon; + if (app.icon_url) { + icon = app.icon_url; + } else { + icon = icon50; + } + return (
-
@@ -67,7 +102,7 @@ export default class Authorize extends React.Component { id='authorize.app' defaultMessage='The app {appName} would like the ability to access and modify your basic information.' values={{ - appName: this.props.appName + appName: app.name }} />

@@ -76,14 +111,14 @@ export default class Authorize extends React.Component { id='authorize.access' defaultMessage='Allow {appName} access?' values={{ - appName: this.props.appName + appName: app.name }} />