From 9ab5a7996247c98ed6267b638e1b313e7c4eb8ff Mon Sep 17 00:00:00 2001 From: enahum Date: Tue, 23 Aug 2016 19:06:17 -0300 Subject: PLT-3745 - Deauthorize OAuth Apps (#3852) * Deauthorize OAuth APIs * Deautorize OAuth Apps Account Settings * Fix typo in client method * Fix issues found by PM * Show help text only when there is at least one authorized app --- webapp/client/client.jsx | 20 ++ .../user_settings/user_settings_security.jsx | 228 +++++++++++++++++---- webapp/i18n/en.json | 7 +- webapp/sass/routes/_settings.scss | 36 ++++ 4 files changed, 251 insertions(+), 40 deletions(-) (limited to 'webapp') diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 28d121011..5dda975f6 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -1553,6 +1553,26 @@ export default class Client { end(this.handleResponse.bind(this, 'getOAuthAppInfo', success, error)); } + getAuthorizedApps(success, error) { + request. + get(`${this.getOAuthRoute()}/authorized`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(). + end(this.handleResponse.bind(this, 'getAuthorizedApps', success, error)); + } + + deauthorizeOAuthApp(id, success, error) { + request. + post(`${this.getOAuthRoute()}/${id}/deauthorize`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(). + end(this.handleResponse.bind(this, 'deauthorizeOAuthApp', success, error)); + } + // Routes for Hooks addIncomingHook(hook, success, error) { diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx index 769959432..bcffa157c 100644 --- a/webapp/components/user_settings/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security.jsx @@ -16,33 +16,12 @@ import Constants from 'utils/constants.jsx'; import $ from 'jquery'; import React from 'react'; -import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedTime, FormattedDate} from 'react-intl'; +import {FormattedMessage, FormattedHTMLMessage, FormattedTime, FormattedDate} from 'react-intl'; import {Link} from 'react-router/es6'; -const holders = defineMessages({ - currentPasswordError: { - id: 'user.settings.security.currentPasswordError', - defaultMessage: 'Please enter your current password.' - }, - passwordLengthError: { - id: 'user.settings.security.passwordLengthError', - defaultMessage: 'New passwords must be at least {min} characters and at most {max} characters.' - }, - passwordMatchError: { - id: 'user.settings.security.passwordMatchError', - defaultMessage: 'The new passwords you entered do not match.' - }, - method: { - id: 'user.settings.security.method', - defaultMessage: 'Sign-in Method' - }, - close: { - id: 'user.settings.security.close', - defaultMessage: 'Close' - } -}); +import icon50 from 'images/icon50x50.png'; -class SecurityTab extends React.Component { +export default class SecurityTab extends React.Component { constructor(props) { super(props); @@ -56,7 +35,9 @@ class SecurityTab extends React.Component { this.getDefaultState = this.getDefaultState.bind(this); this.createPasswordSection = this.createPasswordSection.bind(this); this.createSignInSection = this.createSignInSection.bind(this); + this.createOAuthAppsSection = this.createOAuthAppsSection.bind(this); this.showQrCode = this.showQrCode.bind(this); + this.deauthorizeApp = this.deauthorizeApp.bind(this); this.state = this.getDefaultState(); } @@ -74,6 +55,16 @@ class SecurityTab extends React.Component { }; } + componentDidMount() { + Client.getAuthorizedApps( + (authorizedApps) => { + this.setState({authorizedApps, serverError: null}); //eslint-disable-line react/no-did-mount-set-state + }, + (err) => { + this.setState({serverError: err.message}); //eslint-disable-line react/no-did-mount-set-state + }); + } + submitPassword(e) { e.preventDefault(); @@ -82,9 +73,8 @@ class SecurityTab extends React.Component { var newPassword = this.state.newPassword; var confirmPassword = this.state.confirmPassword; - const {formatMessage} = this.props.intl; if (currentPassword === '') { - this.setState({passwordError: formatMessage(holders.currentPasswordError), serverError: ''}); + this.setState({passwordError: Utils.localizeMessage('user.settings.security.currentPasswordError', 'Please enter your current password.'), serverError: ''}); return; } @@ -98,7 +88,7 @@ class SecurityTab extends React.Component { } if (newPassword !== confirmPassword) { - var defaultState = Object.assign(this.getDefaultState(), {passwordError: formatMessage(holders.passwordMatchError), serverError: ''}); + var defaultState = Object.assign(this.getDefaultState(), {passwordError: Utils.localizeMessage('user.settings.security.passwordMatchError', 'The new passwords you entered do not match.'), serverError: ''}); this.setState(defaultState); return; } @@ -190,6 +180,23 @@ class SecurityTab extends React.Component { this.setState({mfaShowQr: true}); } + deauthorizeApp(e) { + e.preventDefault(); + const appId = e.currentTarget.getAttribute('data-app'); + Client.deauthorizeOAuthApp( + appId, + () => { + const authorizedApps = this.state.authorizedApps.filter((app) => { + return app.id !== appId; + }); + + this.setState({authorizedApps, serverError: null}); + }, + (err) => { + this.setState({serverError: err.message}); + }); + } + createMfaSection() { let updateSectionStatus; let submit; @@ -686,7 +693,7 @@ class SecurityTab extends React.Component { return ( ); } + createOAuthAppsSection() { + let updateSectionStatus; + + if (this.props.activeSection === 'apps') { + let apps; + if (this.state.authorizedApps && this.state.authorizedApps.length > 0) { + apps = this.state.authorizedApps.map((app) => { + const homepage = ( + + {app.homepage} + + ); + + return ( +
+
+
+ {app.name} + + {' -'} {homepage} + +
+
{app.description}
+
+ + + +
+
+
+ {app.name} +
+
+
+ ); + }); + } else { + apps = ( +
+
+
+ +
+
+
+ ); + } + + const inputs = []; + let wrapperClass; + let helpText; + if (Array.isArray(apps)) { + wrapperClass = 'authorized-apps__wrapper'; + + helpText = ( +
+ +
+ ); + } + + inputs.push( +
+ {apps} +
+ ); + + updateSectionStatus = function updateSection(e) { + this.props.updateSection(''); + this.setState({serverError: null}); + e.preventDefault(); + }.bind(this); + + const title = ( +
+ + {helpText} +
+ ); + + return ( + + ); + } + + updateSectionStatus = function updateSection() { + this.props.updateSection('apps'); + }.bind(this); + + return ( + + } + updateSection={updateSectionStatus} + /> + ); + } + render() { const user = this.props.user; + const config = window.mm_config; const passwordSection = this.createPasswordSection(); let numMethods = 0; - numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; - numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; - numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods; - numMethods = global.window.mm_config.EnableSaml === 'true' ? numMethods + 1 : numMethods; + numMethods = config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; + numMethods = config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; + numMethods = config.EnableLdap === 'true' ? numMethods + 1 : numMethods; + numMethods = config.EnableSaml === 'true' ? numMethods + 1 : numMethods; let signInSection; - if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) { + if (config.EnableSignUpWithEmail === 'true' && numMethods > 0) { signInSection = this.createSignInSection(); } let mfaSection; - if (global.window.mm_config.EnableMultifactorAuthentication === 'true' && + if (config.EnableMultifactorAuthentication === 'true' && global.window.mm_license.IsLicensed === 'true' && (user.auth_service === '' || user.auth_service === Constants.LDAP_SERVICE)) { mfaSection = this.createMfaSection(); } + let oauthSection; + if (config.EnableOAuthServiceProvider === 'true') { + oauthSection = this.createOAuthAppsSection(); + } + return (
@@ -781,7 +932,7 @@ class SecurityTab extends React.Component { type='button' className='close' data-dismiss='modal' - aria-label={this.props.intl.formatMessage(holders.close)} + aria-label={Utils.localizeMessage('user.settings.security.close', 'Close')} onClick={this.props.closeModal} > @@ -814,6 +965,8 @@ class SecurityTab extends React.Component {
{mfaSection}
+ {oauthSection} +
{signInSection}


@@ -849,7 +1002,6 @@ SecurityTab.defaultProps = { activeSection: '' }; SecurityTab.propTypes = { - intl: intlShape.isRequired, user: React.PropTypes.object, activeSection: React.PropTypes.string, updateSection: React.PropTypes.func, @@ -858,5 +1010,3 @@ SecurityTab.propTypes = { collapseModal: React.PropTypes.func.isRequired, setEnforceFocus: React.PropTypes.func.isRequired }; - -export default injectIntl(SecurityTab); diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 378812455..0fb75e8b8 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1961,5 +1961,10 @@ "web.footer.terms": "Terms", "web.header.back": "Back", "web.root.signup_info": "All team communication in one place, searchable and accessible anywhere", - "youtube_video.notFound": "Video not found" + "youtube_video.notFound": "Video not found", + "user.settings.security.deauthorize": "Deauthorize", + "user.settings.security.noApps": "No OAuth 2.0 Applications are authorized.", + "user.settings.security.oauthApps": "OAuth 2.0 Applications", + "user.settings.security.oauthAppsDescription": "Click 'Edit' to manage your OAuth 2.0 Applications", + "user.settings.security.oauthAppsHelp": "Applications act on your behalf to access your data based on the permissions you grant them." } diff --git a/webapp/sass/routes/_settings.scss b/webapp/sass/routes/_settings.scss index 36a1acf76..6fa8c26a7 100644 --- a/webapp/sass/routes/_settings.scss +++ b/webapp/sass/routes/_settings.scss @@ -7,6 +7,42 @@ max-height: 300px; max-width: 560px; } + + .authorized-apps__help { + font-size: 13px; + font-weight: 400; + margin-top: 7px; + } + + .authorized-apps__wrapper { + background-color: #fff; + padding: 10px 0; + } + + .authorized-app { + display: inline-block; + width: 100%; + + &:not(:last-child) { + border-bottom: 1px solid #ccc; + margin-bottom: 10px; + } + + .authorized-app__name { + font-weight: 600; + } + + .authorized-app__url { + font-size: 13px; + font-weight: 400; + } + + .authorized-app__description, + .authorized-app__deauthorize { + font-size: 13px; + margin: 5px 0; + } + } } .modal { -- cgit v1.2.3-1-g7c22