Merge pull request #693 from mattermost/mm-1419
Implement OAuth2 service provider functionality.
Этот коммит содержится в:
72
web/react/components/authorize.jsx
Обычный файл
72
web/react/components/authorize.jsx
Обычный файл
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client = require('../utils/client.jsx');
|
||||
|
||||
export default class Authorize extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleAllow = this.handleAllow.bind(this);
|
||||
this.handleDeny = this.handleDeny.bind(this);
|
||||
|
||||
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,
|
||||
(data) => {
|
||||
if (data.redirect) {
|
||||
window.location.replace(data.redirect);
|
||||
}
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
handleDeny() {
|
||||
window.location.replace(this.props.redirectUri + '?error=access_denied');
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className='authorize-box'>
|
||||
<div className='authorize-inner'>
|
||||
<h3>{'An application would like to connect to your '}{this.props.teamName}{' account'}</h3>
|
||||
<label>{'The app '}{this.props.appName}{' would like the ability to access and modify your basic information.'}</label>
|
||||
<br/>
|
||||
<br/>
|
||||
<label>{'Allow '}{this.props.appName}{' access?'}</label>
|
||||
<br/>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn authorize-btn'
|
||||
onClick={this.handleDeny}
|
||||
>
|
||||
{'Deny'}
|
||||
</button>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary authorize-btn'
|
||||
onClick={this.handleAllow}
|
||||
>
|
||||
{'Allow'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
@@ -25,7 +25,7 @@ export default class PopoverListMembers extends React.Component {
|
||||
|
||||
$('#member_popover').popover({placement: 'bottom', trigger: 'click', html: true});
|
||||
$('body').on('click', function onClick(e) {
|
||||
if ($(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) {
|
||||
if (e.target.parentNode && $(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) {
|
||||
$('#member_popover').popover('hide');
|
||||
}
|
||||
});
|
||||
|
||||
249
web/react/components/register_app_modal.jsx
Обычный файл
249
web/react/components/register_app_modal.jsx
Обычный файл
@@ -0,0 +1,249 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client = require('../utils/client.jsx');
|
||||
|
||||
export default class RegisterAppModal extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.register = this.register.bind(this);
|
||||
this.onHide = this.onHide.bind(this);
|
||||
this.save = this.save.bind(this);
|
||||
|
||||
this.state = {clientId: '', clientSecret: '', saved: false};
|
||||
}
|
||||
componentDidMount() {
|
||||
$(React.findDOMNode(this)).on('hide.bs.modal', this.onHide);
|
||||
}
|
||||
register() {
|
||||
var state = this.state;
|
||||
state.serverError = null;
|
||||
|
||||
var app = {};
|
||||
|
||||
var name = this.refs.name.getDOMNode().value;
|
||||
if (!name || name.length === 0) {
|
||||
state.nameError = 'Application name must be filled in.';
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
state.nameError = null;
|
||||
app.name = name;
|
||||
|
||||
var homepage = this.refs.homepage.getDOMNode().value;
|
||||
if (!homepage || homepage.length === 0) {
|
||||
state.homepageError = 'Homepage must be filled in.';
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
state.homepageError = null;
|
||||
app.homepage = homepage;
|
||||
|
||||
var desc = this.refs.desc.getDOMNode().value;
|
||||
app.description = desc;
|
||||
|
||||
var rawCallbacks = this.refs.callback.getDOMNode().value.trim();
|
||||
if (!rawCallbacks || rawCallbacks.length === 0) {
|
||||
state.callbackError = 'At least one callback URL must be filled in.';
|
||||
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.getDOMNode().checked});
|
||||
}
|
||||
render() {
|
||||
var nameError;
|
||||
if (this.state.nameError) {
|
||||
nameError = <div className='form-group has-error'><label className='control-label'>{this.state.nameError}</label></div>;
|
||||
}
|
||||
var homepageError;
|
||||
if (this.state.homepageError) {
|
||||
homepageError = <div className='form-group has-error'><label className='control-label'>{this.state.homepageError}</label></div>;
|
||||
}
|
||||
var callbackError;
|
||||
if (this.state.callbackError) {
|
||||
callbackError = <div className='form-group has-error'><label className='control-label'>{this.state.callbackError}</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 = '';
|
||||
if (this.state.clientId === '') {
|
||||
body = (
|
||||
<div className='form-group user-settings'>
|
||||
<h3>{'Register a New Application'}</h3>
|
||||
<br/>
|
||||
<label className='col-sm-4 control-label'>{'Application Name'}</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
ref='name'
|
||||
className='form-control'
|
||||
type='text'
|
||||
placeholder='Required'
|
||||
/>
|
||||
{nameError}
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<label className='col-sm-4 control-label'>{'Homepage URL'}</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
ref='homepage'
|
||||
className='form-control'
|
||||
type='text'
|
||||
placeholder='Required'
|
||||
/>
|
||||
{homepageError}
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<label className='col-sm-4 control-label'>{'Description'}</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
ref='desc'
|
||||
className='form-control'
|
||||
type='text'
|
||||
placeholder='Optional'
|
||||
/>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<label className='col-sm-4 control-label'>{'Callback URL'}</label>
|
||||
<div className='col-sm-7'>
|
||||
<textarea
|
||||
ref='callback'
|
||||
className='form-control'
|
||||
type='text'
|
||||
placeholder='Required'
|
||||
rows='5'
|
||||
/>
|
||||
{callbackError}
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
{serverError}
|
||||
<a
|
||||
className='btn btn-sm theme pull-right'
|
||||
href='#'
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
>
|
||||
{'Cancel'}
|
||||
</a>
|
||||
<a
|
||||
className='btn btn-sm btn-primary pull-right'
|
||||
onClick={this.register}
|
||||
>
|
||||
{'Register'}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
var btnClass = ' disabled';
|
||||
if (this.state.saved) {
|
||||
btnClass = '';
|
||||
}
|
||||
|
||||
body = (
|
||||
<div className='form-group user-settings'>
|
||||
<h3>{'Your Application Credentials'}</h3>
|
||||
<br/>
|
||||
<br/>
|
||||
<label className='col-sm-12 control-label'>{'Client ID: '}{this.state.clientId}</label>
|
||||
<label className='col-sm-12 control-label'>{'Client Secret: '}{this.state.clientSecret}</label>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<strong>{'Save these somewhere SAFE and SECURE. We can retrieve your Client Id if you lose it, but your Client Secret will be lost forever if you were to lose it.'}</strong>
|
||||
<br/>
|
||||
<br/>
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
ref='save'
|
||||
type='checkbox'
|
||||
checked={this.state.saved}
|
||||
onClick={this.save}
|
||||
>
|
||||
{'I have saved both my Client Id and Client Secret somewhere safe'}
|
||||
</input>
|
||||
</label>
|
||||
</div>
|
||||
<a
|
||||
className={'btn btn-sm btn-primary pull-right' + btnClass}
|
||||
href='#'
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
>
|
||||
{'Close'}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='modal fade'
|
||||
ref='modal'
|
||||
id='register_app'
|
||||
role='dialog'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<div className='modal-dialog'>
|
||||
<div className='modal-content'>
|
||||
<div className='modal-header'>
|
||||
<button
|
||||
type='button'
|
||||
className='close'
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
>
|
||||
<span aria-hidden='true'>{'x'}</span>
|
||||
</button>
|
||||
<h4
|
||||
className='modal-title'
|
||||
ref='title'
|
||||
>
|
||||
{'Developer Applications'}
|
||||
</h4>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ var NotificationsTab = require('./user_settings_notifications.jsx');
|
||||
var SecurityTab = require('./user_settings_security.jsx');
|
||||
var GeneralTab = require('./user_settings_general.jsx');
|
||||
var AppearanceTab = require('./user_settings_appearance.jsx');
|
||||
var DeveloperTab = require('./user_settings_developer.jsx');
|
||||
|
||||
export default class UserSettings extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -76,6 +77,15 @@ 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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div/>;
|
||||
|
||||
93
web/react/components/user_settings_developer.jsx
Обычный файл
93
web/react/components/user_settings_developer.jsx
Обычный файл
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var SettingItemMin = require('./setting_item_min.jsx');
|
||||
var SettingItemMax = require('./setting_item_max.jsx');
|
||||
|
||||
export default class DeveloperTab extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
register() {
|
||||
$('#user_settings1').modal('hide');
|
||||
$('#register_app').modal('show');
|
||||
}
|
||||
render() {
|
||||
var appSection;
|
||||
var self = this;
|
||||
if (this.props.activeSection === 'app') {
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-7'>
|
||||
<a
|
||||
className='btn btn-sm btn-primary'
|
||||
onClick={this.register}
|
||||
>
|
||||
{'Register New Application'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
appSection = (
|
||||
<SettingItemMax
|
||||
title='Applications (Preview)'
|
||||
inputs={inputs}
|
||||
updateSection={function updateSection(e) {
|
||||
self.props.updateSection('');
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
appSection = (
|
||||
<SettingItemMin
|
||||
title='Applications (Preview)'
|
||||
describe='Open to register a new third-party application'
|
||||
updateSection={function updateSection() {
|
||||
self.props.updateSection('app');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='modal-header'>
|
||||
<button
|
||||
type='button'
|
||||
className='close'
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
>
|
||||
<span aria-hidden='true'>{'x'}</span>
|
||||
</button>
|
||||
<h4
|
||||
className='modal-title'
|
||||
ref='title'
|
||||
>
|
||||
<i className='modal-back'></i>{'Developer Settings'}
|
||||
</h4>
|
||||
</div>
|
||||
<div className='user-settings'>
|
||||
<h3 className='tab-header'>{'Developer Settings'}</h3>
|
||||
<div className='divider-dark first'/>
|
||||
{appSection}
|
||||
<div className='divider-dark'/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeveloperTab.defaultProps = {
|
||||
activeSection: ''
|
||||
};
|
||||
DeveloperTab.propTypes = {
|
||||
activeSection: React.PropTypes.string,
|
||||
updateSection: React.PropTypes.func
|
||||
};
|
||||
@@ -17,8 +17,8 @@ export default class UserSettingsModal extends React.Component {
|
||||
$('body').on('click', '.modal-back', function changeDisplay() {
|
||||
$(this).closest('.modal-dialog').removeClass('display--content');
|
||||
});
|
||||
$('body').on('click', '.modal-header .close', function closeModal() {
|
||||
setTimeout(function finishClose() {
|
||||
$('body').on('click', '.modal-header .close', () => {
|
||||
setTimeout(() => {
|
||||
$('.modal-dialog.display--content').removeClass('display--content');
|
||||
}, 500);
|
||||
});
|
||||
@@ -35,6 +35,9 @@ export default class UserSettingsModal extends React.Component {
|
||||
tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'});
|
||||
tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'});
|
||||
tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'});
|
||||
if (global.window.config.EnableOAuthServiceProvider) {
|
||||
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -54,13 +57,13 @@ export default class UserSettingsModal extends React.Component {
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
>
|
||||
<span aria-hidden='true'>×</span>
|
||||
<span aria-hidden='true'>{'x'}</span>
|
||||
</button>
|
||||
<h4
|
||||
className='modal-title'
|
||||
ref='title'
|
||||
>
|
||||
Account Settings
|
||||
{'Account Settings'}
|
||||
</h4>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
|
||||
21
web/react/pages/authorize.jsx
Обычный файл
21
web/react/pages/authorize.jsx
Обычный файл
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Authorize = require('../components/authorize.jsx');
|
||||
|
||||
function setupAuthorizePage(teamName, appName, responseType, clientId, redirectUri, scope, state) {
|
||||
React.render(
|
||||
<Authorize
|
||||
teamName={teamName}
|
||||
appName={appName}
|
||||
responseType={responseType}
|
||||
clientId={clientId}
|
||||
redirectUri={redirectUri}
|
||||
scope={scope}
|
||||
state={state}
|
||||
/>,
|
||||
document.getElementById('authorize')
|
||||
);
|
||||
}
|
||||
|
||||
global.window.setup_authorize_page = setupAuthorizePage;
|
||||
@@ -33,6 +33,7 @@ var AccessHistoryModal = require('../components/access_history_modal.jsx');
|
||||
var ActivityLogModal = require('../components/activity_log_modal.jsx');
|
||||
var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx');
|
||||
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
|
||||
var RegisterAppModal = require('../components/register_app_modal.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
@@ -222,6 +223,11 @@ function setupChannelPage(props) {
|
||||
/>,
|
||||
document.getElementById('file_upload_overlay')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<RegisterAppModal />,
|
||||
document.getElementById('register_app_modal')
|
||||
);
|
||||
}
|
||||
|
||||
global.window.setup_channel_page = setupChannelPage;
|
||||
|
||||
@@ -987,3 +987,36 @@ export function updateValetFeature(data, success, error) {
|
||||
|
||||
track('api', 'api_teams_update_valet_feature');
|
||||
}
|
||||
|
||||
export function registerOAuthApp(app, success, error) {
|
||||
$.ajax({
|
||||
url: '/api/v1/oauth/register',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(app),
|
||||
success: success,
|
||||
error: (xhr, status, err) => {
|
||||
const e = handleError('registerApp', xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_apps_register');
|
||||
}
|
||||
|
||||
export function allowOAuth2(responseType, clientId, redirectUri, state, scope, success, error) {
|
||||
$.ajax({
|
||||
url: '/api/v1/oauth/allow?response_type=' + responseType + '&client_id=' + clientId + '&redirect_uri=' + redirectUri + '&scope=' + scope + '&state=' + state,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success: success,
|
||||
error: (xhr, status, err) => {
|
||||
const e = handleError('allowOAuth2', xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.track('api', 'api_users_allow_oauth2');
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user