* Add user access token UI

* Fix enter press and update mattermost-redux

* Updating UI for access token stuff (#7066)

* Revert segment key
Этот коммит содержится в:
Joram Wilander
2017-08-01 11:06:53 -04:00
коммит произвёл GitHub
родитель 4ef844298f
Коммит 5da5c0bbfb
33 изменённых файлов: 1690 добавлений и 252 удалений

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

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

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

@@ -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: (
<FormattedMessage
id='user.settings.tokens.confirmCreateTitle'
defaultMessage='Create System Admin User Access Token'
/>
),
confirmMessage: (
<div className='alert alert-danger'>
<FormattedHTMLMessage
id='user.settings.tokens.confirmCreateMessage'
defaultMessage='You are generating a user access token with System Admin permissions. Are you sure want to create this token?'
/>
</div>
),
confirmButton: (
<FormattedMessage
id='user.settings.tokens.confirmCreateButton'
defaultMessage='Yes, Create'
/>
),
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: (
<FormattedMessage
id='user.settings.tokens.confirmDeleteTitle'
defaultMessage='Delete {name} Token?'
values={{
name: token.description
}}
/>
),
confirmMessage: (
<div className='alert alert-danger'>
<FormattedHTMLMessage
id='user.settings.tokens.confirmDeleteMessage'
defaultMessage='Any integrations using this token will no longer be able to access the Mattermost API. You cannot undo this action. Are you sure want to delete this token?'
/>
</div>
),
confirmButton: (
<FormattedMessage
id='user.settings.tokens.confirmDeleteButton'
defaultMessage='Yes, Delete'
/>
),
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(
<div
key={token.id}
className='setting-box__item'
>
<div className='whitespace--nowrap overflow--ellipsis'>
<strong>{token.description}</strong>
</div>
<div className='setting-box__token-id whitespace--nowrap overflow--ellipsis'>
<FormattedMessage
id='user.settings.tokens.tokenId'
defaultMessage='Token ID: '
/>
{token.id}
</div>
<div>
<a
name={token.id}
href='#'
onClick={(e) => {
e.preventDefault();
this.confirmRevokeToken(token.id);
}}
>
<FormattedMessage
id='user.settings.tokens.delete'
defaultMessage='Delete'
/>
</a>
</div>
<hr className='margin-bottom margin-top x2'/>
</div>
);
});
if (tokenList.length === 0) {
tokenList.push(
<FormattedMessage
key='notokens'
id='user.settings.tokens.userAccessTokensNone'
defaultMessage='No user access tokens.'
/>
);
}
let extraInfo;
if (isMobile()) {
extraInfo = (
<span>
<FormattedHTMLMessage
id='user.settings.tokens.description_mobile'
defaultMessage='<a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">User access tokens</a> function similar to session tokens and can be used by integrations to <a href="https://about.mattermost.com/default-api-authentication" target="_blank">authenticate against the REST API</a>. Create new tokens on your desktop.'
/>
</span>
);
} else {
extraInfo = (
<span>
<FormattedHTMLMessage
id='user.settings.tokens.description'
defaultMessage='<a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">User access tokens</a> function similar to session tokens and can be used by integrations to <a href="https://about.mattermost.com/default-api-authentication" target="_blank">authenticate against the REST API</a>.'
/>
</span>
);
}
let newTokenSection;
if (this.state.tokenCreationState === TOKEN_CREATING) {
newTokenSection = (
<div className='padding-left x2'>
<div className='row'>
<label className='col-sm-auto control-label padding-right x2'>
<FormattedMessage
id='user.settings.tokens.name'
defaultMessage='Name: '
/>
</label>
<div className='col-sm-5'>
<input
ref='newtokendescription'
className='form-control'
type='text'
maxLength={64}
onKeyPress={this.saveTokenKeyPress}
/>
</div>
</div>
<div>
<div className='padding-top x2'>
<FormattedMessage
id='user.settings.tokens.nameDescription'
defaultMessage='Give a name for your token, so you remember what its used for. A token is generated after you hit "Save".'
/>
</div>
<div>
<label
id='clientError'
className='has-error margin-top margin-bottom'
>
{this.state.tokenError}
</label>
</div>
<button
className='btn btn-primary'
onClick={this.confirmCreateToken}
>
<FormattedMessage
id='user.settings.tokens.save'
defaultMessage='Save'
/>
</button>
<button
className='btn btn-default'
onClick={this.stopCreatingToken}
>
<FormattedMessage
id='user.settings.tokens.cancel'
defaultMessage='Cancel'
/>
</button>
</div>
</div>
);
} else if (this.state.tokenCreationState === TOKEN_CREATED) {
newTokenSection = (
<div
className='alert alert-warning'
>
<i className='fa fa-warning margin-right'/>
<FormattedMessage
id='user.settings.tokens.copy'
defaultMessage="Please copy the token below. You won't be able to see it again!"
/>
<br/>
<br/>
<FormattedMessage
id='user.settings.tokens.name'
defaultMessage='Name: '
/>
{this.state.newToken.description}
<br/>
<FormattedMessage
id='user.settings.tokens.id'
defaultMessage='ID: '
/>
{this.state.newToken.id}
<br/>
<strong>
<FormattedMessage
id='user.settings.tokens.token'
defaultMessage='Token: '
/>
{this.state.newToken.token}
</strong>
</div>
);
} else {
newTokenSection = (
<a
className='btn btn-primary'
href='#'
onClick={this.startCreatingToken}
>
<FormattedMessage
id='user.settings.tokens.create'
defaultMessage='Create New Token'
/>
</a>
);
}
const inputs = [];
inputs.push(
<div
key='tokensSetting'
className='padding-top'
>
<div key='tokenList'>
<div className='alert alert-transparent'>
{tokenList}
</div>
<br/>
{newTokenSection}
</div>
</div>
);
updateSectionStatus = function resetSection(e) {
this.props.updateSection('');
this.setState({newToken: null, tokenCreationState: TOKEN_NOT_CREATING, serverError: null, tokenError: ''});
e.preventDefault();
}.bind(this);
return (
<SettingItemMax
title={Utils.localizeMessage('user.settings.tokens.title', 'User Access Tokens')}
inputs={inputs}
extraInfo={extraInfo}
infoPosition='top'
server_error={this.state.serverError}
updateSection={updateSectionStatus}
width='full'
cancelButtonText={
<FormattedMessage
id='user.settings.security.close'
defaultMessage='Close'
/>
}
/>
);
}
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 (
<SettingItemMin
title={Utils.localizeMessage('user.settings.tokens.title', 'User Access Tokens')}
describe={describe}
updateSection={updateSectionStatus}
/>
);
}
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 (
<div>
<div className='modal-header'>
@@ -1001,6 +1405,8 @@ export default class SecurityTab extends React.Component {
<div className='divider-light'/>
{oauthSection}
<div className='divider-light'/>
{tokensSection}
<div className='divider-light'/>
{signInSection}
<div className='divider-dark'/>
<br/>
@@ -1014,7 +1420,7 @@ export default class SecurityTab extends React.Component {
defaultMessage='View Access History'
/>
</ToggleModalButton>
<b/>
<br/>
<ToggleModalButton
className='security-links theme'
dialogType={ActivityLogModal}
@@ -1026,6 +1432,14 @@ export default class SecurityTab extends React.Component {
/>
</ToggleModalButton>
</div>
<ConfirmModal
title={this.state.confirmTitle}
message={this.state.confirmMessage}
confirmButtonText={this.state.confirmButton}
show={this.state.showConfirmModal}
onConfirm={this.state.confirmComplete || (() => {})} //eslint-disable-line no-empty-function
onCancel={this.handleCancelConfirm}
/>
</div>
);
}