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
Этот коммит содержится в:
enahum
2016-08-03 12:19:27 -05:00
коммит произвёл Harrison Healey
родитель ea027c8de4
Коммит 5bc3cea6fe
50 изменённых файлов: 2334 добавлений и 1158 удалений

Просмотреть файл

@@ -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'>

Просмотреть файл

@@ -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'
/>
}
/>

Просмотреть файл

@@ -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>
);
@@ -104,6 +106,23 @@ export default class WebhookSettings extends AdminSettings {
value={this.state.enableCommands}
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={

Просмотреть файл

@@ -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 = {};
}
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;
Client.allowOAuth2(responseType, clientId, redirectUri, state, scope,
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 params = this.props.location.query;
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
};

Просмотреть файл

@@ -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>
);
}

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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'>

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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'

Просмотреть файл

@@ -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>

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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');
}
}
);
},

Просмотреть файл

@@ -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>

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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'});