From 5da5c0bbfb80cb5c9cf2699f42d17decc2d60f5b Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Tue, 1 Aug 2017 11:06:53 -0400 Subject: PLT-6987 User access token UI (#7007) * Add user access token UI * Fix enter press and update mattermost-redux * Updating UI for access token stuff (#7066) * Revert segment key --- .../user_settings/user_settings_security/index.js | 16 +- .../user_settings_security.jsx | 470 +++++++++++++++++++-- 2 files changed, 455 insertions(+), 31 deletions(-) (limited to 'webapp/components/user_settings/user_settings_security') diff --git a/webapp/components/user_settings/user_settings_security/index.js b/webapp/components/user_settings/user_settings_security/index.js index cdbabd055..a3e83d7de 100644 --- a/webapp/components/user_settings/user_settings_security/index.js +++ b/webapp/components/user_settings/user_settings_security/index.js @@ -3,20 +3,30 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {getMe} from 'mattermost-redux/actions/users'; +import {getMe, getUserAccessTokensForUser, createUserAccessToken, revokeUserAccessToken, clearUserAccessTokens} from 'mattermost-redux/actions/users'; +import * as UserUtils from 'mattermost-redux/utils/user_utils'; import SecurityTab from './user_settings_security.jsx'; function mapStateToProps(state, ownProps) { + const tokensEnabled = state.entities.general.config.EnableUserAccessTokens === 'true'; + const userHasTokenRole = UserUtils.hasUserAccessTokenRole(ownProps.user.roles) || UserUtils.isSystemAdmin(ownProps.user.roles); + return { - ...ownProps + ...ownProps, + userAccessTokens: state.entities.users.myUserAccessTokens, + canUseAccessTokens: tokensEnabled && userHasTokenRole }; } function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - getMe + getMe, + getUserAccessTokensForUser, + createUserAccessToken, + revokeUserAccessToken, + clearUserAccessTokens }, dispatch) }; } diff --git a/webapp/components/user_settings/user_settings_security/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security/user_settings_security.jsx index b8ec690a4..5c9ad67e3 100644 --- a/webapp/components/user_settings/user_settings_security/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security/user_settings_security.jsx @@ -6,6 +6,7 @@ import SettingItemMax from 'components/setting_item_max.jsx'; import AccessHistoryModal from 'components/access_history_modal'; import ActivityLogModal from 'components/activity_log_modal'; import ToggleModalButton from 'components/toggle_modal_button.jsx'; +import ConfirmModal from 'components/confirm_modal.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; @@ -13,15 +14,22 @@ import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; import {updatePassword, getAuthorizedApps, deactivateMfa, deauthorizeOAuthApp} from 'actions/user_actions.jsx'; +import {trackEvent} from 'actions/diagnostics_actions.jsx'; +import {isMobile} from 'utils/user_agent.jsx'; import $ from 'jquery'; import PropTypes from 'prop-types'; import React from 'react'; -import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl'; +import * as UserUtils from 'mattermost-redux/utils/user_utils'; +import {FormattedMessage, FormattedTime, FormattedDate, FormattedHTMLMessage} from 'react-intl'; import {browserHistory, Link} from 'react-router/es6'; import icon50 from 'images/icon50x50.png'; +const TOKEN_CREATING = 'creating'; +const TOKEN_CREATED = 'created'; +const TOKEN_NOT_CREATING = 'not_creating'; + export default class SecurityTab extends React.Component { static propTypes = { user: PropTypes.object, @@ -31,26 +39,45 @@ export default class SecurityTab extends React.Component { closeModal: PropTypes.func.isRequired, collapseModal: PropTypes.func.isRequired, setEnforceFocus: PropTypes.func.isRequired, + + /* + * The user access tokens for the user + */ + userAccessTokens: PropTypes.object, + + /* + * Set if access tokens are enabled and this user can use them + */ + canUseAccessTokens: PropTypes.bool, + actions: PropTypes.shape({ - getMe: PropTypes.func.isRequired + getMe: PropTypes.func.isRequired, + + /* + * Function to get user access tokens for a user + */ + getUserAccessTokensForUser: PropTypes.func.isRequired, + + /* + * Function to create a user access token + */ + createUserAccessToken: PropTypes.func.isRequired, + + /* + * Function to revoke a user access token + */ + revokeUserAccessToken: PropTypes.func.isRequired, + + /* + * Function to clear user access tokens locally + */ + clearUserAccessTokens: PropTypes.func.isRequired }).isRequired } constructor(props) { super(props); - this.submitPassword = this.submitPassword.bind(this); - this.setupMfa = this.setupMfa.bind(this); - this.removeMfa = this.removeMfa.bind(this); - this.updateCurrentPassword = this.updateCurrentPassword.bind(this); - this.updateNewPassword = this.updateNewPassword.bind(this); - this.updateConfirmPassword = this.updateConfirmPassword.bind(this); - 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.deauthorizeApp = this.deauthorizeApp.bind(this); - this.state = this.getDefaultState(); } @@ -61,6 +88,8 @@ export default class SecurityTab extends React.Component { confirmPassword: '', passwordError: '', serverError: '', + tokenError: '', + showConfirmModal: false, authService: this.props.user.auth_service }; } @@ -73,11 +102,18 @@ export default class SecurityTab extends React.Component { }, (err) => { this.setState({serverError: err.message}); //eslint-disable-line react/no-did-mount-set-state - }); + } + ); + } + + if (this.props.canUseAccessTokens) { + this.props.actions.clearUserAccessTokens(); + const userId = this.props.user ? this.props.user.id : ''; + this.props.actions.getUserAccessTokensForUser(userId, 0, 200); } } - submitPassword(e) { + submitPassword = (e) => { e.preventDefault(); var user = this.props.user; @@ -127,12 +163,12 @@ export default class SecurityTab extends React.Component { ); } - setupMfa(e) { + setupMfa = (e) => { e.preventDefault(); browserHistory.push('/mfa/setup'); } - removeMfa() { + removeMfa = () => { deactivateMfa( () => { if (global.window.mm_license.MFA === 'true' && @@ -157,19 +193,19 @@ export default class SecurityTab extends React.Component { ); } - updateCurrentPassword(e) { + updateCurrentPassword = (e) => { this.setState({currentPassword: e.target.value}); } - updateNewPassword(e) { + updateNewPassword = (e) => { this.setState({newPassword: e.target.value}); } - updateConfirmPassword(e) { + updateConfirmPassword = (e) => { this.setState({confirmPassword: e.target.value}); } - deauthorizeApp(e) { + deauthorizeApp = (e) => { e.preventDefault(); const appId = e.currentTarget.getAttribute('data-app'); deauthorizeOAuthApp( @@ -183,10 +219,11 @@ export default class SecurityTab extends React.Component { }, (err) => { this.setState({serverError: err.message}); - }); + } + ); } - createMfaSection() { + createMfaSection = () => { let updateSectionStatus; let submit; @@ -321,7 +358,7 @@ export default class SecurityTab extends React.Component { ); } - createPasswordSection() { + createPasswordSection = () => { let updateSectionStatus; if (this.props.activeSection === 'password') { @@ -578,7 +615,7 @@ export default class SecurityTab extends React.Component { ); } - createSignInSection() { + createSignInSection = () => { let updateSectionStatus; const user = this.props.user; @@ -793,7 +830,7 @@ export default class SecurityTab extends React.Component { ); } - createOAuthAppsSection() { + createOAuthAppsSection = () => { let updateSectionStatus; if (this.props.activeSection === 'apps') { @@ -929,6 +966,368 @@ export default class SecurityTab extends React.Component { ); } + startCreatingToken = () => { + this.setState({tokenCreationState: TOKEN_CREATING}); + } + + stopCreatingToken = () => { + this.setState({tokenCreationState: TOKEN_NOT_CREATING}); + } + + handleCreateToken = async () => { + this.handleCancelConfirm(); + + const description = this.refs.newtokendescription ? this.refs.newtokendescription.value : ''; + + if (description === '') { + this.setState({tokenError: Utils.localizeMessage('user.settings.tokens.nameRequired', 'Please enter a name.')}); + return; + } + + this.setState({tokenError: ''}); + + const userId = this.props.user ? this.props.user.id : ''; + const {data, error} = await this.props.actions.createUserAccessToken(userId, description); + + if (data) { + this.setState({tokenCreationState: TOKEN_CREATED, newToken: data}); + } else if (error) { + this.setState({serverError: error.message}); + } + } + + handleCancelConfirm = () => { + this.setState({ + showConfirmModal: false, + confirmTitle: null, + confirmMessage: null, + confirmButton: null, + confirmComplete: null + }); + } + + confirmCreateToken = () => { + if (UserUtils.isSystemAdmin(this.props.user.roles)) { + this.setState({ + showConfirmModal: true, + confirmTitle: ( + + ), + confirmMessage: ( +
+ +
+ ), + confirmButton: ( + + ), + confirmComplete: () => { + this.handleCreateToken(); + trackEvent('settings', 'system_admin_create_user_access_token'); + } + }); + + return; + } + + this.handleCreateToken(); + } + + saveTokenKeyPress = (e) => { + if (e.which === Constants.KeyCodes.ENTER) { + this.confirmCreateToken(); + } + } + + confirmRevokeToken = (tokenId) => { + const token = this.props.userAccessTokens[tokenId]; + + this.setState({ + showConfirmModal: true, + confirmTitle: ( + + ), + confirmMessage: ( +
+ +
+ ), + confirmButton: ( + + ), + confirmComplete: () => { + this.revokeToken(tokenId); + trackEvent('settings', 'revoke_user_access_token'); + } + }); + } + + revokeToken = async (tokenId) => { + const {error} = await this.props.actions.revokeUserAccessToken(tokenId); + if (error) { + this.setState({serverError: error.message}); + } + this.handleCancelConfirm(); + } + + createTokensSection = () => { + let updateSectionStatus; + + if (this.props.activeSection === 'tokens') { + const tokenList = []; + Object.values(this.props.userAccessTokens).forEach((token) => { + if (this.state.newToken && this.state.newToken.id === token.id) { + return; + } + + tokenList.push( +
+
+ {token.description} +
+
+ + {token.id} +
+ +
+
+ ); + }); + + if (tokenList.length === 0) { + tokenList.push( + + ); + } + let extraInfo; + + if (isMobile()) { + extraInfo = ( + + + + ); + } else { + extraInfo = ( + + + + ); + } + + let newTokenSection; + if (this.state.tokenCreationState === TOKEN_CREATING) { + newTokenSection = ( +
+
+ +
+ +
+
+
+
+ +
+
+ +
+ + +
+
+ ); + } else if (this.state.tokenCreationState === TOKEN_CREATED) { + newTokenSection = ( +
+ + +
+
+ + {this.state.newToken.description} +
+ + {this.state.newToken.id} +
+ + + {this.state.newToken.token} + +
+ ); + } else { + newTokenSection = ( + + + + ); + } + + const inputs = []; + inputs.push( +
+
+
+ {tokenList} +
+
+ {newTokenSection} +
+
+ ); + + updateSectionStatus = function resetSection(e) { + this.props.updateSection(''); + this.setState({newToken: null, tokenCreationState: TOKEN_NOT_CREATING, serverError: null, tokenError: ''}); + e.preventDefault(); + }.bind(this); + + return ( + + } + /> + ); + } + + const describe = Utils.localizeMessage('user.settings.tokens.clickToEdit', "Click 'Edit' to manage your user access tokens"); + + updateSectionStatus = function updateSection() { + this.props.updateSection('tokens'); + }.bind(this); + + return ( + + ); + } + render() { const user = this.props.user; const config = window.mm_config; @@ -959,6 +1358,11 @@ export default class SecurityTab extends React.Component { oauthSection = this.createOAuthAppsSection(); } + let tokensSection; + if (this.props.canUseAccessTokens) { + tokensSection = this.createTokensSection(); + } + return (
@@ -1001,6 +1405,8 @@ export default class SecurityTab extends React.Component {
{oauthSection}
+ {tokensSection} +
{signInSection}

@@ -1014,7 +1420,7 @@ export default class SecurityTab extends React.Component { defaultMessage='View Access History' /> - +
+ {})} //eslint-disable-line no-empty-function + onCancel={this.handleCancelConfirm} + />
); } -- cgit v1.2.3-1-g7c22