summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
authorenahum <nahumhbl@gmail.com>2016-08-03 12:19:27 -0500
committerHarrison Healey <harrisonmhealey@gmail.com>2016-08-03 13:19:27 -0400
commit5bc3cea6fe4a909735753692d0c4cd960e8ab516 (patch)
tree85715d9fcbc146a9672d84c9a1ea1e96b6e71231 /webapp/components
parentea027c8de44d44b6ac4e66ab802e675d315b0be5 (diff)
downloadchat-5bc3cea6fe4a909735753692d0c4cd960e8ab516.tar.gz
chat-5bc3cea6fe4a909735753692d0c4cd960e8ab516.tar.bz2
chat-5bc3cea6fe4a909735753692d0c4cd960e8ab516.zip
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
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/admin_console/admin_settings.jsx17
-rw-r--r--webapp/components/admin_console/admin_sidebar.jsx6
-rw-r--r--webapp/components/admin_console/custom_integrations_settings.jsx (renamed from webapp/components/admin_console/webhook_settings.jsx)25
-rw-r--r--webapp/components/authorize.jsx75
-rw-r--r--webapp/components/backstage/components/backstage_sidebar.jsx32
-rw-r--r--webapp/components/integrations/components/add_oauth_app.jsx435
-rw-r--r--webapp/components/integrations/components/installed_oauth_app.jsx219
-rw-r--r--webapp/components/integrations/components/installed_oauth_apps.jsx108
-rw-r--r--webapp/components/integrations/components/integrations.jsx30
-rw-r--r--webapp/components/login/login_controller.jsx18
-rw-r--r--webapp/components/navbar_dropdown.jsx26
-rw-r--r--webapp/components/needs_team.jsx2
-rw-r--r--webapp/components/register_app_modal.jsx411
-rw-r--r--webapp/components/signup_user_complete.jsx14
-rw-r--r--webapp/components/user_settings/user_settings.jsx12
-rw-r--r--webapp/components/user_settings/user_settings_developer.jsx138
-rw-r--r--webapp/components/user_settings/user_settings_modal.jsx8
17 files changed, 928 insertions, 648 deletions
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 {
<form
className='form-horizontal'
role='form'
+ onSubmit={this.handleSubmit}
>
{this.renderSettings()}
<div className='form-group'>
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 {
}
>
<AdminSidebarSection
- name='webhooks'
+ name='custom'
title={
<FormattedMessage
- id='admin.sidebar.webhooks'
- defaultMessage='Webhooks and Commands'
+ id='admin.sidebar.customIntegrations'
+ defaultMessage='Custom Integrations'
/>
}
/>
diff --git a/webapp/components/admin_console/webhook_settings.jsx b/webapp/components/admin_console/custom_integrations_settings.jsx
index ba2443442..cfa1a30ae 100644
--- a/webapp/components/admin_console/webhook_settings.jsx
+++ b/webapp/components/admin_console/custom_integrations_settings.jsx
@@ -24,6 +24,7 @@ export default class WebhookSettings extends AdminSettings {
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;
}
@@ -35,7 +36,8 @@ export default class WebhookSettings extends AdminSettings {
enableCommands: config.ServiceSettings.EnableCommands,
enableOnlyAdminIntegrations: config.ServiceSettings.EnableOnlyAdminIntegrations,
enablePostUsernameOverride: config.ServiceSettings.EnablePostUsernameOverride,
- enablePostIconOverride: config.ServiceSettings.EnablePostIconOverride
+ enablePostIconOverride: config.ServiceSettings.EnablePostIconOverride,
+ enableOAuthServiceProvider: config.ServiceSettings.EnableOAuthServiceProvider
};
}
@@ -43,8 +45,8 @@ export default class WebhookSettings extends AdminSettings {
return (
<h3>
<FormattedMessage
- id='admin.integrations.webhook'
- defaultMessage='Webhooks and Commands'
+ id='admin.integrations.custom'
+ defaultMessage='Custom Integrations'
/>
</h3>
);
@@ -105,6 +107,23 @@ export default class WebhookSettings extends AdminSettings {
onChange={this.handleChange}
/>
<BooleanSetting
+ id='enableOAuthServiceProvider'
+ label={
+ <FormattedMessage
+ id='admin.oauth.providerTitle'
+ defaultMessage='Enable OAuth 2.0 Service Provider: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.oauth.providerDescription'
+ defaultMessage='When true, Mattermost can act as an OAuth 2.0 service provider allowing external applications to authorize API requests to Mattermost.'
+ />
+ }
+ value={this.state.enableOAuthServiceProvider}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
id='enableOnlyAdminIntegrations'
label={
<FormattedMessage
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 (
<div className='container-fluid'>
<div className='prompt'>
<div className='prompt__heading'>
<div className='prompt__app-icon'>
<img
- src={icon50}
+ src={icon}
width='50'
height='50'
alt=''
/>
</div>
<div className='text'>
- <FormattedMessage
+ <FormattedHTMLMessage
id='authorize.title'
- defaultMessage='An application would like to connect to your {teamName} account'
+ defaultMessage='<strong>{appName}</strong> would like to connect to your <strong>Mattermost</strong> user account'
values={{
- teamName: this.props.teamName
+ appName: app.name
}}
/>
</div>
@@ -67,7 +102,7 @@ export default class Authorize extends React.Component {
id='authorize.app'
defaultMessage='The app <strong>{appName}</strong> would like the ability to access and modify your basic information.'
values={{
- appName: this.props.appName
+ appName: app.name
}}
/>
</p>
@@ -76,14 +111,14 @@ export default class Authorize extends React.Component {
id='authorize.access'
defaultMessage='Allow <strong>{appName}</strong> access?'
values={{
- appName: this.props.appName
+ appName: app.name
}}
/>
</h2>
<div className='prompt__buttons'>
<button
type='submit'
- className='btn authorize-btn'
+ className='btn btn-link authorize-btn'
onClick={this.handleDeny}
>
<FormattedMessage
@@ -107,13 +142,3 @@ export default class Authorize extends React.Component {
);
}
}
-
-Authorize.propTypes = {
- appName: React.PropTypes.string,
- teamName: React.PropTypes.string,
- responseType: React.PropTypes.string,
- clientId: React.PropTypes.string,
- redirectUri: React.PropTypes.string,
- state: React.PropTypes.string,
- scope: React.PropTypes.string
-};
diff --git a/webapp/components/backstage/components/backstage_sidebar.jsx b/webapp/components/backstage/components/backstage_sidebar.jsx
index 3434b315a..554e3043e 100644
--- a/webapp/components/backstage/components/backstage_sidebar.jsx
+++ b/webapp/components/backstage/components/backstage_sidebar.jsx
@@ -39,20 +39,22 @@ export default class BackstageSidebar extends React.Component {
}
renderIntegrations() {
- if (window.mm_config.EnableIncomingWebhooks !== 'true' &&
- window.mm_config.EnableOutgoingWebhooks !== 'true' &&
- window.mm_config.EnableCommands !== 'true') {
+ const config = window.mm_config;
+ if (config.EnableIncomingWebhooks !== 'true' &&
+ config.EnableOutgoingWebhooks !== 'true' &&
+ config.EnableCommands !== 'true' &&
+ config.EnableOAuthServiceProvider !== 'true') {
return null;
}
- if (window.mm_config.EnableOnlyAdminIntegrations !== 'false' &&
+ if (config.EnableOnlyAdminIntegrations !== 'false' &&
!Utils.isSystemAdmin(this.props.user.roles) &&
!TeamStore.isTeamAdmin(this.props.user.id, this.props.team.id)) {
return null;
}
let incomingWebhooks = null;
- if (window.mm_config.EnableIncomingWebhooks === 'true') {
+ if (config.EnableIncomingWebhooks === 'true') {
incomingWebhooks = (
<BackstageSection
name='incoming_webhooks'
@@ -67,7 +69,7 @@ export default class BackstageSidebar extends React.Component {
}
let outgoingWebhooks = null;
- if (window.mm_config.EnableOutgoingWebhooks === 'true') {
+ if (config.EnableOutgoingWebhooks === 'true') {
outgoingWebhooks = (
<BackstageSection
name='outgoing_webhooks'
@@ -82,7 +84,7 @@ export default class BackstageSidebar extends React.Component {
}
let commands = null;
- if (window.mm_config.EnableCommands === 'true') {
+ if (config.EnableCommands === 'true') {
commands = (
<BackstageSection
name='commands'
@@ -96,6 +98,21 @@ export default class BackstageSidebar extends React.Component {
);
}
+ let oauthApps = null;
+ if (config.EnableOAuthServiceProvider === 'true') {
+ oauthApps = (
+ <BackstageSection
+ name='oauth2-apps'
+ title={
+ <FormattedMessage
+ id='backstage_sidebar.integrations.oauthApps'
+ defaultMessage='OAuth 2.0 Applications'
+ />
+ }
+ />
+ );
+ }
+
return (
<BackstageCategory
name='integrations'
@@ -111,6 +128,7 @@ export default class BackstageSidebar extends React.Component {
{incomingWebhooks}
{outgoingWebhooks}
{commands}
+ {oauthApps}
</BackstageCategory>
);
}
diff --git a/webapp/components/integrations/components/add_oauth_app.jsx b/webapp/components/integrations/components/add_oauth_app.jsx
new file mode 100644
index 000000000..7e56aea8f
--- /dev/null
+++ b/webapp/components/integrations/components/add_oauth_app.jsx
@@ -0,0 +1,435 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as OAuthActions from 'actions/oauth_actions.jsx';
+
+import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
+import {FormattedMessage} from 'react-intl';
+import FormError from 'components/form_error.jsx';
+import {browserHistory, Link} from 'react-router/es6';
+import SpinnerButton from 'components/spinner_button.jsx';
+
+export default class AddOAuthApp extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.propTypes.object.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.updateName = this.updateName.bind(this);
+ this.updateTrusted = this.updateTrusted.bind(this);
+ this.updateDescription = this.updateDescription.bind(this);
+ this.updateHomepage = this.updateHomepage.bind(this);
+ this.updateIconUrl = this.updateIconUrl.bind(this);
+ this.updateCallbackUrls = this.updateCallbackUrls.bind(this);
+
+ this.imageLoaded = this.imageLoaded.bind(this);
+ this.image = new Image();
+ this.image.onload = this.imageLoaded;
+
+ this.state = {
+ name: '',
+ description: '',
+ homepage: '',
+ icon_url: '',
+ callbackUrls: '',
+ is_trusted: false,
+ has_icon: false,
+ saving: false,
+ serverError: '',
+ clientError: null
+ };
+ }
+
+ imageLoaded() {
+ this.setState({
+ has_icon: true,
+ icon_url: this.refs.icon_url.value
+ });
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ if (this.state.saving) {
+ return;
+ }
+
+ this.setState({
+ saving: true,
+ serverError: '',
+ clientError: ''
+ });
+
+ if (!this.state.name) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_oauth_app.nameRequired'
+ defaultMessage='Name for the OAuth 2.0 application is required.'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (!this.state.description) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_oauth_app.descriptionRequired'
+ defaultMessage='Description for the OAuth 2.0 application is required.'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (!this.state.homepage) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_oauth_app.homepageRequired'
+ defaultMessage='Homepage for the OAuth 2.0 application is required.'
+ />
+ )
+ });
+
+ return;
+ }
+
+ const callbackUrls = [];
+ for (let callbackUrl of this.state.callbackUrls.split('\n')) {
+ callbackUrl = callbackUrl.trim();
+
+ if (callbackUrl.length > 0) {
+ callbackUrls.push(callbackUrl);
+ }
+ }
+
+ if (callbackUrls.length === 0) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_oauth_app.callbackUrlsRequired'
+ defaultMessage='One or more callback URLs are required.'
+ />
+ )
+ });
+
+ return;
+ }
+
+ const app = {
+ name: this.state.name,
+ callback_urls: callbackUrls,
+ homepage: this.state.homepage,
+ description: this.state.description,
+ is_trusted: this.state.is_trusted,
+ icon_url: this.state.icon_url
+ };
+
+ OAuthActions.registerOAuthApp(
+ app,
+ () => {
+ browserHistory.push('/' + this.props.team.name + '/integrations/oauth2-apps');
+ },
+ (err) => {
+ this.setState({
+ saving: false,
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ updateName(e) {
+ this.setState({
+ name: e.target.value
+ });
+ }
+
+ updateTrusted(e) {
+ this.setState({
+ is_trusted: e.target.value === 'true'
+ });
+ }
+
+ updateDescription(e) {
+ this.setState({
+ description: e.target.value
+ });
+ }
+
+ updateHomepage(e) {
+ this.setState({
+ homepage: e.target.value
+ });
+ }
+
+ updateIconUrl(e) {
+ this.setState({
+ has_icon: false,
+ icon_url: ''
+ });
+ this.image.src = e.target.value;
+ }
+
+ updateCallbackUrls(e) {
+ this.setState({
+ callbackUrls: e.target.value
+ });
+ }
+
+ render() {
+ let icon;
+ if (this.state.has_icon) {
+ icon = (
+ <div className='integration__icon'>
+ <img src={this.state.icon_url}/>
+ </div>
+ );
+ }
+
+ return (
+ <div className='backstage-content'>
+ <BackstageHeader>
+ <Link to={'/' + this.props.team.name + '/integrations/oauth2-apps'}>
+ <FormattedMessage
+ id='installed_oauth_apps.header'
+ defaultMessage='Installed OAuth2 Apps'
+ />
+ </Link>
+ <FormattedMessage
+ id='add_oauth_app.header'
+ defaultMessage='Add'
+ />
+ </BackstageHeader>
+ <div className='backstage-form'>
+ {icon}
+ <form className='form-horizontal'>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='is_trusted'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.trusted'
+ defaultMessage='Is Trusted'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ value='true'
+ name='is_trusted'
+ checked={this.state.is_trusted}
+ onChange={this.updateTrusted}
+ />
+ <FormattedMessage
+ id='installed_oauth_apps.trusted.yes'
+ defaultMessage='Yes'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ value='false'
+ name='is_trusted'
+ checked={!this.state.is_trusted}
+ onChange={this.updateTrusted}
+ />
+ <FormattedMessage
+ id='installed_oauth_apps.trusted.no'
+ defaultMessage='No'
+ />
+ </label>
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.trusted.help'
+ defaultMessage="When true, the OAuth 2.0 application is considered trusted by the Mattermost server and doesn't require the user to accept authorization. When false, an additional window will appear, asking the user to accept or deny the authorization."
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='name'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.name'
+ defaultMessage='Display Name'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='name'
+ type='text'
+ maxLength='64'
+ className='form-control'
+ value={this.state.name}
+ onChange={this.updateName}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.name.help'
+ defaultMessage='Display name for your OAuth 2.0 application made of up to 64 characters.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='description'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.description'
+ defaultMessage='Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='description'
+ type='text'
+ maxLength='512'
+ className='form-control'
+ value={this.state.description}
+ onChange={this.updateDescription}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.description.help'
+ defaultMessage='Description for your OAuth 2.0 application.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='homepage'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.homepage'
+ defaultMessage='Homepage'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='homepage'
+ type='url'
+ maxLength='256'
+ className='form-control'
+ value={this.state.homepage}
+ onChange={this.updateHomepage}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.homepage.help'
+ defaultMessage='The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='icon_url'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.iconUrl'
+ defaultMessage='Icon URL'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='icon_url'
+ ref='icon_url'
+ type='url'
+ maxLength='512'
+ className='form-control'
+ onChange={this.updateIconUrl}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.icon.help'
+ defaultMessage='The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='callbackUrls'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.callbackUrls'
+ defaultMessage='Callback URLs (One Per Line)'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <textarea
+ id='callbackUrls'
+ rows='3'
+ maxLength='1024'
+ className='form-control'
+ value={this.state.callbackUrls}
+ onChange={this.updateCallbackUrls}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.callbackUrls.help'
+ defaultMessage='The redirect URIs to which the service will redirect users after accepting or denying authorization of your application, and which will handle authorization codes or access tokens. Must be a valid URL and start with http:// or https://.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='backstage-form__footer'>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
+ <Link
+ className='btn btn-sm'
+ to={'/' + this.props.team.name + '/integrations/oauth2-apps'}
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.cancel'
+ defaultMessage='Cancel'
+ />
+ </Link>
+ <SpinnerButton
+ className='btn btn-primary'
+ type='submit'
+ spinning={this.state.saving}
+ onClick={this.handleSubmit}
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.save'
+ defaultMessage='Save'
+ />
+ </SpinnerButton>
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/integrations/components/installed_oauth_app.jsx b/webapp/components/integrations/components/installed_oauth_app.jsx
new file mode 100644
index 000000000..37fc061f7
--- /dev/null
+++ b/webapp/components/integrations/components/installed_oauth_app.jsx
@@ -0,0 +1,219 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+const FAKE_SECRET = '***************';
+
+export default class InstalledOAuthApp extends React.Component {
+ static get propTypes() {
+ return {
+ oauthApp: React.PropTypes.object.isRequired,
+ onDelete: React.PropTypes.func.isRequired,
+ filter: React.PropTypes.string
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleShowClientSecret = this.handleShowClientSecret.bind(this);
+ this.handleHideClientScret = this.handleHideClientScret.bind(this);
+ this.handleDelete = this.handleDelete.bind(this);
+
+ this.matchesFilter = this.matchesFilter.bind(this);
+
+ this.state = {
+ clientSecret: FAKE_SECRET
+ };
+ }
+
+ handleShowClientSecret(e) {
+ e.preventDefault();
+ this.setState({clientSecret: this.props.oauthApp.client_secret});
+ }
+
+ handleHideClientScret(e) {
+ e.preventDefault();
+ this.setState({clientSecret: FAKE_SECRET});
+ }
+
+ handleDelete(e) {
+ e.preventDefault();
+
+ this.props.onDelete(this.props.oauthApp);
+ }
+
+ matchesFilter(oauthApp, filter) {
+ if (!filter) {
+ return true;
+ }
+
+ return oauthApp.name.toLowerCase().indexOf(filter) !== -1;
+ }
+
+ render() {
+ const oauthApp = this.props.oauthApp;
+
+ if (!this.matchesFilter(oauthApp, this.props.filter)) {
+ return null;
+ }
+
+ let name;
+ if (oauthApp.name) {
+ name = oauthApp.name;
+ } else {
+ name = (
+ <FormattedMessage
+ id='installed_integrations.unnamed_oauth_app'
+ defaultMessage='Unnamed OAuth 2.0 Application'
+ />
+ );
+ }
+
+ let description;
+ if (oauthApp.description) {
+ description = (
+ <div className='item-details__row'>
+ <span className='item-details__description'>
+ {oauthApp.description}
+ </span>
+ </div>
+ );
+ }
+
+ const urls = (
+ <div className='item-details__row'>
+ <span className='item-details__url'>
+ <FormattedMessage
+ id='installed_integrations.callback_urls'
+ defaultMessage='Callback URLs: {urls}'
+ values={{
+ urls: oauthApp.callback_urls.join(', ')
+ }}
+ />
+ </span>
+ </div>
+ );
+
+ let isTrusted;
+ if (oauthApp.is_trusted) {
+ isTrusted = Utils.localizeMessage('installed_oauth_apps.trusted.yes', 'Yes');
+ } else {
+ isTrusted = Utils.localizeMessage('installed_oauth_apps.trusted.no', 'No');
+ }
+
+ let action;
+ if (this.state.clientSecret === FAKE_SECRET) {
+ action = (
+ <a
+ href='#'
+ onClick={this.handleShowClientSecret}
+ >
+ <FormattedMessage
+ id='installed_integrations.showSecret'
+ defaultMessage='Show Secret'
+ />
+ </a>
+ );
+ } else {
+ action = (
+ <a
+ href='#'
+ onClick={this.handleHideClientScret}
+ >
+ <FormattedMessage
+ id='installed_integrations.hideSecret'
+ defaultMessage='Hide Secret'
+ />
+ </a>
+ );
+ }
+
+ let icon;
+ if (oauthApp.icon_url) {
+ icon = (
+ <div className='integration__icon integration-list__icon'>
+ <img src={oauthApp.icon_url}/>
+ </div>
+ );
+ }
+
+ return (
+ <div className='backstage-list__item'>
+ {icon}
+ <div className='item-details'>
+ <div className='item-details__row'>
+ <span className='item-details__name'>
+ {name}
+ </span>
+ </div>
+ {description}
+ <div className='item-details__row'>
+ <span className='item-details__url'>
+ <FormattedHTMLMessage
+ id='installed_oauth_apps.is_trusted'
+ defaultMessage='Is Trusted: <strong>{isTrusted}</strong>'
+ values={{
+ isTrusted
+ }}
+ />
+ </span>
+ </div>
+ <div className='item-details__row'>
+ <span className='item-details__token'>
+ <FormattedHTMLMessage
+ id='installed_integrations.client_id'
+ defaultMessage='Client ID: <strong>{clientId}</strong>'
+ values={{
+ clientId: oauthApp.id
+ }}
+ />
+ </span>
+ </div>
+ <div className='item-details__row'>
+ <span className='item-details__token'>
+ <FormattedHTMLMessage
+ id='installed_integrations.client_secret'
+ defaultMessage='Client Secret: <strong>{clientSecret}</strong>'
+ values={{
+ clientSecret: this.state.clientSecret
+ }}
+ />
+ </span>
+ </div>
+ {urls}
+ <div className='item-details__row'>
+ <span className='item-details__creation'>
+ <FormattedMessage
+ id='installed_integrations.creation'
+ defaultMessage='Created by {creator} on {createAt, date, full}'
+ values={{
+ creator: Utils.displayUsername(oauthApp.creator_id),
+ createAt: oauthApp.create_at
+ }}
+ />
+ </span>
+ </div>
+ </div>
+ <div className='item-actions'>
+ {action}
+ {' - '}
+ <a
+ href='#'
+ onClick={this.handleDelete}
+ >
+ <FormattedMessage
+ id='installed_integrations.delete'
+ defaultMessage='Delete'
+ />
+ </a>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/integrations/components/installed_oauth_apps.jsx b/webapp/components/integrations/components/installed_oauth_apps.jsx
new file mode 100644
index 000000000..7a3b512dd
--- /dev/null
+++ b/webapp/components/integrations/components/installed_oauth_apps.jsx
@@ -0,0 +1,108 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import UserStore from 'stores/user_store.jsx';
+import IntegrationStore from 'stores/integration_store.jsx';
+import * as OAuthActions from 'actions/oauth_actions.jsx';
+import {localizeMessage} from 'utils/utils.jsx';
+
+import BackstageList from 'components/backstage/components/backstage_list.jsx';
+import {FormattedMessage} from 'react-intl';
+import InstalledOAuthApp from './installed_oauth_app.jsx';
+
+export default class InstalledOAuthApps extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.propTypes.object.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
+
+ this.deleteOAuthApp = this.deleteOAuthApp.bind(this);
+
+ const userId = UserStore.getCurrentId();
+
+ this.state = {
+ oauthApps: IntegrationStore.getOAuthApps(userId),
+ loading: !IntegrationStore.hasReceivedOAuthApps(userId)
+ };
+ }
+
+ componentDidMount() {
+ IntegrationStore.addChangeListener(this.handleIntegrationChange);
+
+ if (window.mm_config.EnableOAuthServiceProvider === 'true') {
+ OAuthActions.listOAuthApps(UserStore.getCurrentId());
+ }
+ }
+
+ componentWillUnmount() {
+ IntegrationStore.removeChangeListener(this.handleIntegrationChange);
+ }
+
+ handleIntegrationChange() {
+ const userId = UserStore.getCurrentId();
+
+ this.setState({
+ oauthApps: IntegrationStore.getOAuthApps(userId),
+ loading: !IntegrationStore.hasReceivedOAuthApps(userId)
+ });
+ }
+
+ deleteOAuthApp(app) {
+ const userId = UserStore.getCurrentId();
+ OAuthActions.deleteOAuthApp(app.id, userId);
+ }
+
+ render() {
+ const oauthApps = this.state.oauthApps.map((app) => {
+ return (
+ <InstalledOAuthApp
+ key={app.id}
+ oauthApp={app}
+ onDelete={this.deleteOAuthApp}
+ />
+ );
+ });
+
+ return (
+ <BackstageList
+ header={
+ <FormattedMessage
+ id='installed_oauth_apps.header'
+ defaultMessage='OAuth 2.0 Applications'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='installed_oauth_apps.help'
+ defaultMessage='OAuth 2.0 Applications are available to everyone on your server.'
+ />
+ }
+ addText={
+ <FormattedMessage
+ id='installed_oauth_apps.add'
+ defaultMessage='Add OAuth 2.0 Application'
+ />
+ }
+ addLink={'/' + this.props.team.name + '/integrations/oauth2-apps/add'}
+ emptyText={
+ <FormattedMessage
+ id='installed_oauth_apps.empty'
+ defaultMessage='No OAuth 2.0 Applications found'
+ />
+ }
+ searchPlaceholder={localizeMessage('installed_oauth_apps.search', 'Search OAuth 2.0 Applications')}
+ loading={this.state.loading}
+ >
+ {oauthApps}
+ </BackstageList>
+ );
+ }
+}
diff --git a/webapp/components/integrations/components/integrations.jsx b/webapp/components/integrations/components/integrations.jsx
index 7894ced5d..ec923c4f0 100644
--- a/webapp/components/integrations/components/integrations.jsx
+++ b/webapp/components/integrations/components/integrations.jsx
@@ -7,6 +7,7 @@ import {FormattedMessage} from 'react-intl';
import IntegrationOption from './integration_option.jsx';
import WebhookIcon from 'images/webhook_icon.jpg';
+import AppIcon from 'images/oauth_icon.png';
export default class Integrations extends React.Component {
static get propTypes() {
@@ -17,8 +18,9 @@ export default class Integrations extends React.Component {
render() {
const options = [];
+ const config = window.mm_config;
- if (window.mm_config.EnableIncomingWebhooks === 'true') {
+ if (config.EnableIncomingWebhooks === 'true') {
options.push(
<IntegrationOption
key='incomingWebhook'
@@ -40,7 +42,7 @@ export default class Integrations extends React.Component {
);
}
- if (window.mm_config.EnableOutgoingWebhooks === 'true') {
+ if (config.EnableOutgoingWebhooks === 'true') {
options.push(
<IntegrationOption
key='outgoingWebhook'
@@ -62,7 +64,7 @@ export default class Integrations extends React.Component {
);
}
- if (window.mm_config.EnableCommands === 'true') {
+ if (config.EnableCommands === 'true') {
options.push(
<IntegrationOption
key='command'
@@ -84,6 +86,28 @@ export default class Integrations extends React.Component {
);
}
+ if (config.EnableOAuthServiceProvider === 'true') {
+ options.push(
+ <IntegrationOption
+ key='oauth2Apps'
+ image={AppIcon}
+ title={
+ <FormattedMessage
+ id='integrations.oauthApps.title'
+ defaultMessage='OAuth 2.0 Applications'
+ />
+ }
+ description={
+ <FormattedMessage
+ id='integrations.oauthApps.description'
+ defaultMessage='Auth 2.0 allows external applications to make authorized requests to the Mattermost API.'
+ />
+ }
+ link={'/' + this.props.team.name + '/integrations/oauth2-apps'}
+ />
+ );
+ }
+
return (
<div className='backstage-content row'>
<div className='backstage-header'>
diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx
index 69981cfd6..f84c30d51 100644
--- a/webapp/components/login/login_controller.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -146,11 +146,12 @@ export default class LoginController extends React.Component {
token,
() => {
// check for query params brought over from signup_user_complete
- if (this.props.location.query.id || this.props.location.query.h) {
+ const query = this.props.location.query;
+ if (query.id || query.h) {
Client.addUserToTeamFromInvite(
- this.props.location.query.d,
- this.props.location.query.h,
- this.props.location.query.id,
+ query.d,
+ query.h,
+ query.id,
() => {
this.finishSignin();
},
@@ -200,8 +201,13 @@ export default class LoginController extends React.Component {
finishSignin() {
GlobalActions.emitInitialLoad(
() => {
+ const query = this.props.location.query;
GlobalActions.loadDefaultLocale();
- browserHistory.push('/select_team');
+ if (query.redirect_to) {
+ browserHistory.push(query.redirect_to);
+ } else {
+ browserHistory.push('/select_team');
+ }
}
);
}
@@ -401,7 +407,7 @@ export default class LoginController extends React.Component {
defaultMessage="Don't have an account? "
/>
<Link
- to={'/signup_user_complete'}
+ to={'/signup_user_complete' + this.props.location.search}
className='signup-team-login'
>
<FormattedMessage
diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx
index f82bd564e..39bd6b159 100644
--- a/webapp/components/navbar_dropdown.jsx
+++ b/webapp/components/navbar_dropdown.jsx
@@ -99,6 +99,7 @@ export default class NavbarDropdown extends React.Component {
}
render() {
+ const config = global.window.mm_config;
var teamLink = '';
var inviteLink = '';
var manageLink = '';
@@ -131,7 +132,7 @@ export default class NavbarDropdown extends React.Component {
</li>
);
- if (this.props.teamType === Constants.OPEN_TEAM && global.window.mm_config.EnableUserCreation === 'true') {
+ if (this.props.teamType === Constants.OPEN_TEAM && config.EnableUserCreation === 'true') {
teamLink = (
<li>
<a
@@ -148,10 +149,10 @@ export default class NavbarDropdown extends React.Component {
}
if (global.window.mm_license.IsLicensed === 'true') {
- if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) {
+ if (config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) {
teamLink = null;
inviteLink = null;
- } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) {
+ } else if (config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) {
teamLink = null;
inviteLink = null;
}
@@ -201,10 +202,11 @@ export default class NavbarDropdown extends React.Component {
);
const integrationsEnabled =
- window.mm_config.EnableIncomingWebhooks === 'true' ||
- window.mm_config.EnableOutgoingWebhooks === 'true' ||
- window.mm_config.EnableCommands === 'true';
- if (integrationsEnabled && (isAdmin || window.mm_config.EnableOnlyAdminIntegrations !== 'true')) {
+ config.EnableIncomingWebhooks === 'true' ||
+ config.EnableOutgoingWebhooks === 'true' ||
+ config.EnableCommands === 'true' ||
+ config.EnableOAuthServiceProvider === 'true';
+ if (integrationsEnabled && (isAdmin || config.EnableOnlyAdminIntegrations !== 'true')) {
integrationsLink = (
<li>
<Link to={'/' + this.props.teamName + '/integrations'}>
@@ -234,7 +236,7 @@ export default class NavbarDropdown extends React.Component {
var teams = [];
- if (global.window.mm_config.EnableTeamCreation === 'true') {
+ if (config.EnableTeamCreation === 'true') {
teams.push(
<li key='newTeam_li'>
<Link
@@ -297,13 +299,13 @@ export default class NavbarDropdown extends React.Component {
}
let helpLink = null;
- if (global.window.mm_config.HelpLink) {
+ if (config.HelpLink) {
helpLink = (
<li>
<Link
target='_blank'
rel='noopener noreferrer'
- to={global.window.mm_config.HelpLink}
+ to={config.HelpLink}
>
<FormattedMessage
id='navbar_dropdown.help'
@@ -315,13 +317,13 @@ export default class NavbarDropdown extends React.Component {
}
let reportLink = null;
- if (global.window.mm_config.ReportAProblemLink) {
+ if (config.ReportAProblemLink) {
reportLink = (
<li>
<Link
target='_blank'
rel='noopener noreferrer'
- to={global.window.mm_config.ReportAProblemLink}
+ to={config.ReportAProblemLink}
>
<FormattedMessage
id='navbar_dropdown.report'
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index 27951db0f..6c023d497 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.jsx
@@ -31,7 +31,6 @@ import DeletePostModal from 'components/delete_post_modal.jsx';
import MoreChannelsModal from 'components/more_channels.jsx';
import TeamSettingsModal from 'components/team_settings_modal.jsx';
import RemovedFromChannelModal from 'components/removed_from_channel_modal.jsx';
-import RegisterAppModal from 'components/register_app_modal.jsx';
import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx';
import InviteMemberModal from 'components/invite_member_modal.jsx';
import LeaveTeamModal from 'components/leave_team_modal.jsx';
@@ -162,7 +161,6 @@ export default class NeedsTeam extends React.Component {
<EditPostModal/>
<DeletePostModal/>
<RemovedFromChannelModal/>
- <RegisterAppModal/>
<SelectTeamModal/>
</div>
</div>
diff --git a/webapp/components/register_app_modal.jsx b/webapp/components/register_app_modal.jsx
deleted file mode 100644
index b9523c3ed..000000000
--- a/webapp/components/register_app_modal.jsx
+++ /dev/null
@@ -1,411 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import Client from 'client/web_client.jsx';
-import ModalStore from 'stores/modal_store.jsx';
-
-import {Modal} from 'react-bootstrap';
-
-import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
-
-const ActionTypes = Constants.ActionTypes;
-
-const holders = defineMessages({
- required: {
- id: 'register_app.required',
- defaultMessage: 'Required'
- },
- optional: {
- id: 'register_app.optional',
- defaultMessage: 'Optional'
- }
-});
-
-import React from 'react';
-
-class RegisterAppModal extends React.Component {
- constructor() {
- super();
-
- this.handleSubmit = this.handleSubmit.bind(this);
- this.onHide = this.onHide.bind(this);
- this.save = this.save.bind(this);
- this.updateShow = this.updateShow.bind(this);
-
- this.state = {
- clientId: '',
- clientSecret: '',
- saved: false,
- show: false
- };
- }
- componentDidMount() {
- ModalStore.addModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
- }
- componentWillUnmount() {
- ModalStore.removeModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
- }
- updateShow(show) {
- if (!show) {
- if (this.state.clientId !== '' && !this.state.saved) {
- return;
- }
-
- this.setState({
- clientId: '',
- clientSecret: '',
- saved: false,
- homepageError: null,
- callbackError: null,
- serverError: null,
- nameError: null
- });
- }
-
- this.setState({show});
- }
- handleSubmit(e) {
- e.preventDefault();
-
- var state = this.state;
- state.serverError = null;
-
- var app = {};
-
- var name = this.refs.name.value;
- if (!name || name.length === 0) {
- state.nameError = true;
- this.setState(state);
- return;
- }
- state.nameError = null;
- app.name = name;
-
- var homepage = this.refs.homepage.value;
- if (!homepage || homepage.length === 0) {
- state.homepageError = true;
- this.setState(state);
- return;
- }
- state.homepageError = null;
- app.homepage = homepage;
-
- var desc = this.refs.desc.value;
- app.description = desc;
-
- var rawCallbacks = this.refs.callback.value.trim();
- if (!rawCallbacks || rawCallbacks.length === 0) {
- state.callbackError = true;
- this.setState(state);
- return;
- }
- state.callbackError = null;
- app.callback_urls = rawCallbacks.split('\n');
-
- Client.registerOAuthApp(app,
- (data) => {
- state.clientId = data.id;
- state.clientSecret = data.client_secret;
- this.setState(state);
- },
- (err) => {
- state.serverError = err.message;
- this.setState(state);
- }
- );
- }
- onHide(e) {
- if (!this.state.saved && this.state.clientId !== '') {
- e.preventDefault();
- return;
- }
-
- this.setState({clientId: '', clientSecret: '', saved: false});
- }
- save() {
- this.setState({saved: this.refs.save.checked});
- }
- render() {
- const {formatMessage} = this.props.intl;
- var nameError;
- if (this.state.nameError) {
- nameError = (
- <div className='form-group has-error'>
- <label className='control-label'>
- <FormattedMessage
- id='register_app.nameError'
- defaultMessage='Application name must be filled in.'
- />
- </label>
- </div>
- );
- }
- var homepageError;
- if (this.state.homepageError) {
- homepageError = (
- <div className='form-group has-error'>
- <label className='control-label'>
- <FormattedMessage
- id='register_app.homepageError'
- defaultMessage='Homepage must be filled in.'
- />
- </label>
- </div>
- );
- }
- var callbackError;
- if (this.state.callbackError) {
- callbackError = (
- <div className='form-group has-error'>
- <label className='control-label'>
- <FormattedMessage
- id='register_app.callbackError'
- defaultMessage='At least one callback URL must be filled in.'
- />
- </label>
- </div>
- );
- }
- var serverError;
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var body = '';
- var footer = '';
- if (this.state.clientId === '') {
- body = (
- <div className='settings-modal'>
- <div className='form-horizontal user-settings'>
- <h4 className='padding-bottom x3'>
- <FormattedMessage
- id='register_app.title'
- defaultMessage='Register a New Application'
- />
- </h4>
- <div className='row'>
- <label className='col-sm-4 control-label'>
- <FormattedMessage
- id='register_app.name'
- defaultMessage='Application Name'
- />
- </label>
- <div className='col-sm-7'>
- <input
- ref='name'
- className='form-control'
- type='text'
- placeholder={formatMessage(holders.required)}
- />
- {nameError}
- </div>
- </div>
- <div className='row padding-top x2'>
- <label className='col-sm-4 control-label'>
- <FormattedMessage
- id='register_app.homepage'
- defaultMessage='Homepage URL'
- />
- </label>
- <div className='col-sm-7'>
- <input
- ref='homepage'
- className='form-control'
- type='text'
- placeholder={formatMessage(holders.required)}
- />
- {homepageError}
- </div>
- </div>
- <div className='row padding-top x2'>
- <label className='col-sm-4 control-label'>
- <FormattedMessage
- id='register_app.description'
- defaultMessage='Description'
- />
- </label>
- <div className='col-sm-7'>
- <input
- ref='desc'
- className='form-control'
- type='text'
- placeholder={formatMessage(holders.optional)}
- />
- </div>
- </div>
- <div className='row padding-top padding-bottom x2'>
- <label className='col-sm-4 control-label'>
- <FormattedMessage
- id='register_app.callback'
- defaultMessage='Callback URL'
- />
- </label>
- <div className='col-sm-7'>
- <textarea
- ref='callback'
- className='form-control'
- type='text'
- placeholder={formatMessage(holders.required)}
- rows='5'
- />
- {callbackError}
- </div>
- </div>
- {serverError}
- </div>
- </div>
- );
-
- footer = (
- <div>
- <button
- type='button'
- className='btn btn-default'
- onClick={() => this.updateShow(false)}
- >
- <FormattedMessage
- id='register_app.cancel'
- defaultMessage='Cancel'
- />
- </button>
- <button
- onClick={this.handleSubmit}
- type='submit'
- className='btn btn-primary'
- tabIndex='3'
- >
- <FormattedMessage
- id='register_app.register'
- defaultMessage='Register'
- />
- </button>
- </div>
- );
- } else {
- var btnClass = ' disabled';
- if (this.state.saved) {
- btnClass = '';
- }
-
- body = (
- <div className='form-horizontal user-settings'>
- <h4 className='padding-bottom x3'>
- <FormattedMessage
- id='register_app.credentialsTitle'
- defaultMessage='Your Application Credentials'
- />
- </h4>
- <br/>
- <div className='row'>
- <label className='col-sm-4 control-label'>
- <FormattedMessage
- id='register_app.clientId'
- defaultMessage='Client ID'
- />
- </label>
- <div className='col-sm-7'>
- <input
- className='form-control'
- type='text'
- value={this.state.clientId}
- readOnly='true'
- />
- </div>
- </div>
- <br/>
- <div className='row padding-top x2'>
- <label className='col-sm-4 control-label'>
- <FormattedMessage
- id='register_app.clientSecret'
- defaultMessage='Client Secret'
- /></label>
- <div className='col-sm-7'>
- <input
- className='form-control'
- type='text'
- value={this.state.clientSecret}
- readOnly='true'
- />
- </div>
- </div>
- <br/>
- <br/>
- <strong>
- <FormattedMessage
- id='register_app.credentialsDescription'
- defaultMessage="Save these somewhere SAFE and SECURE. Treat your Client ID as your app's username and your Client Secret as the app's password."
- />
- </strong>
- <br/>
- <br/>
- <div className='checkbox'>
- <label>
- <input
- ref='save'
- type='checkbox'
- checked={this.state.saved}
- onChange={this.save}
- />
- <FormattedMessage
- id='register_app.credentialsSave'
- defaultMessage='I have saved both my Client Id and Client Secret somewhere safe'
- />
- </label>
- </div>
- </div>
- );
-
- footer = (
- <a
- className={'btn btn-sm btn-primary pull-right' + btnClass}
- href='#'
- onClick={(e) => {
- e.preventDefault();
- this.updateShow(false);
- }}
- >
- <FormattedMessage
- id='register_app.close'
- defaultMessage='Close'
- />
- </a>
- );
- }
-
- return (
- <span>
- <Modal
- show={this.state.show}
- onHide={() => this.updateShow(false)}
- >
- <Modal.Header closeButton={true}>
- <Modal.Title>
- <FormattedMessage
- id='register_app.dev'
- defaultMessage='Developer Applications'
- />
- </Modal.Title>
- </Modal.Header>
- <form
- role='form'
- className='form-horizontal'
- >
- <Modal.Body>
- {body}
- </Modal.Body>
- <Modal.Footer>
- {footer}
- </Modal.Footer>
- </form>
- </Modal>
- </span>
- );
- }
-}
-
-RegisterAppModal.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(RegisterAppModal);
diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx
index 167b41ea1..23e115124 100644
--- a/webapp/components/signup_user_complete.jsx
+++ b/webapp/components/signup_user_complete.jsx
@@ -231,8 +231,13 @@ export default class SignupUserComplete extends React.Component {
finishSignup() {
GlobalActions.emitInitialLoad(
() => {
+ const query = this.props.location.query;
GlobalActions.loadDefaultLocale();
- browserHistory.push('/select_team');
+ if (query.redirect_to) {
+ browserHistory.push(query.redirect_to);
+ } else {
+ browserHistory.push('/select_team');
+ }
}
);
}
@@ -250,7 +255,12 @@ export default class SignupUserComplete extends React.Component {
GlobalActions.emitInitialLoad(
() => {
- browserHistory.push('/select_team');
+ const query = this.props.location.query;
+ if (query.redirect_to) {
+ browserHistory.push(query.redirect_to);
+ } else {
+ browserHistory.push('/select_team');
+ }
}
);
},
diff --git a/webapp/components/user_settings/user_settings.jsx b/webapp/components/user_settings/user_settings.jsx
index cf69a564f..99a7ec93b 100644
--- a/webapp/components/user_settings/user_settings.jsx
+++ b/webapp/components/user_settings/user_settings.jsx
@@ -6,7 +6,6 @@ import * as utils from 'utils/utils.jsx';
import NotificationsTab from './user_settings_notifications.jsx';
import SecurityTab from './user_settings_security.jsx';
import GeneralTab from './user_settings_general.jsx';
-import DeveloperTab from './user_settings_developer.jsx';
import DisplayTab from './user_settings_display.jsx';
import AdvancedTab from './user_settings_advanced.jsx';
@@ -77,17 +76,6 @@ export default class UserSettings extends React.Component {
/>
</div>
);
- } else if (this.props.activeTab === 'developer') {
- return (
- <div>
- <DeveloperTab
- activeSection={this.props.activeSection}
- updateSection={this.props.updateSection}
- closeModal={this.props.closeModal}
- collapseModal={this.props.collapseModal}
- />
- </div>
- );
} else if (this.props.activeTab === 'display') {
return (
<div>
diff --git a/webapp/components/user_settings/user_settings_developer.jsx b/webapp/components/user_settings/user_settings_developer.jsx
deleted file mode 100644
index ae6d60362..000000000
--- a/webapp/components/user_settings/user_settings_developer.jsx
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import SettingItemMin from '../setting_item_min.jsx';
-import SettingItemMax from '../setting_item_max.jsx';
-import * as GlobalActions from 'actions/global_actions.jsx';
-
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
-
-const holders = defineMessages({
- applicationsPreview: {
- id: 'user.settings.developer.applicationsPreview',
- defaultMessage: 'Applications (Preview)'
- },
- thirdParty: {
- id: 'user.settings.developer.thirdParty',
- defaultMessage: 'Open to register a new third-party application'
- }
-});
-
-import React from 'react';
-
-class DeveloperTab extends React.Component {
- constructor(props) {
- super(props);
-
- this.register = this.register.bind(this);
-
- this.state = {};
- }
- register() {
- this.props.closeModal();
- GlobalActions.showRegisterAppModal();
- }
- render() {
- var appSection;
- var self = this;
- const {formatMessage} = this.props.intl;
- if (this.props.activeSection === 'app') {
- var inputs = [];
-
- inputs.push(
- <div
- key='registerbtn'
- className='form-group'
- >
- <div className='col-sm-7'>
- <a
- className='btn btn-sm btn-primary'
- onClick={this.register}
- >
- <FormattedMessage
- id='user.settings.developer.register'
- defaultMessage='Register New Application'
- />
- </a>
- </div>
- </div>
- );
-
- appSection = (
- <SettingItemMax
- title={formatMessage(holders.applicationsPreview)}
- inputs={inputs}
- updateSection={function updateSection(e) {
- self.props.updateSection('');
- e.preventDefault();
- }}
- />
- );
- } else {
- appSection = (
- <SettingItemMin
- title={formatMessage(holders.applicationsPreview)}
- describe={formatMessage(holders.thirdParty)}
- updateSection={function updateSection() {
- self.props.updateSection('app');
- }}
- />
- );
- }
-
- return (
- <div>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- onClick={this.props.closeModal}
- >
- <span aria-hidden='true'>{'×'}</span>
- </button>
- <h4
- className='modal-title'
- ref='title'
- >
- <div className='modal-back'>
- <i
- className='fa fa-angle-left'
- onClick={this.props.collapseModal}
- />
- </div>
- <FormattedMessage
- id='user.settings.developer.title'
- defaultMessage='Developer Settings'
- />
- </h4>
- </div>
- <div className='user-settings'>
- <h3 className='tab-header'>
- <FormattedMessage
- id='user.settings.developer.title'
- defaultMessage='Developer Settings'
- />
- </h3>
- <div className='divider-dark first'/>
- {appSection}
- <div className='divider-dark'/>
- </div>
- </div>
- );
- }
-}
-
-DeveloperTab.defaultProps = {
- activeSection: ''
-};
-DeveloperTab.propTypes = {
- intl: intlShape.isRequired,
- activeSection: React.PropTypes.string,
- updateSection: React.PropTypes.func,
- closeModal: React.PropTypes.func.isRequired,
- collapseModal: React.PropTypes.func.isRequired
-};
-
-export default injectIntl(DeveloperTab);
diff --git a/webapp/components/user_settings/user_settings_modal.jsx b/webapp/components/user_settings/user_settings_modal.jsx
index de4745aac..9112f8711 100644
--- a/webapp/components/user_settings/user_settings_modal.jsx
+++ b/webapp/components/user_settings/user_settings_modal.jsx
@@ -27,10 +27,6 @@ const holders = defineMessages({
id: 'user.settings.modal.notifications',
defaultMessage: 'Notifications'
},
- developer: {
- id: 'user.settings.modal.developer',
- defaultMessage: 'Developer'
- },
display: {
id: 'user.settings.modal.display',
defaultMessage: 'Display'
@@ -214,10 +210,6 @@ class UserSettingsModal extends React.Component {
tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'icon fa fa-gear'});
tabs.push({name: 'security', uiName: formatMessage(holders.security), icon: 'icon fa fa-lock'});
tabs.push({name: 'notifications', uiName: formatMessage(holders.notifications), icon: 'icon fa fa-exclamation-circle'});
- if (global.window.mm_config.EnableOAuthServiceProvider === 'true') {
- tabs.push({name: 'developer', uiName: formatMessage(holders.developer), icon: 'icon fa fa-th'});
- }
-
tabs.push({name: 'display', uiName: formatMessage(holders.display), icon: 'icon fa fa-eye'});
tabs.push({name: 'advanced', uiName: formatMessage(holders.advanced), icon: 'icon fa fa-list-alt'});