summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoram Wilander <jwawilander@gmail.com>2017-08-01 11:06:53 -0400
committerGitHub <noreply@github.com>2017-08-01 11:06:53 -0400
commit5da5c0bbfb80cb5c9cf2699f42d17decc2d60f5b (patch)
treefdd886332e75a9ae8138a31d3c34b240adb094b5
parent4ef844298fdb0d6fb41eac760f8ac00cee82b3bc (diff)
downloadchat-5da5c0bbfb80cb5c9cf2699f42d17decc2d60f5b.tar.gz
chat-5da5c0bbfb80cb5c9cf2699f42d17decc2d60f5b.tar.bz2
chat-5da5c0bbfb80cb5c9cf2699f42d17decc2d60f5b.zip
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
-rw-r--r--api4/post_test.go55
-rw-r--r--app/authorization.go2
-rw-r--r--app/diagnostics.go1
-rw-r--r--i18n/en.json16
-rw-r--r--model/authorization.go22
-rw-r--r--store/sql_upgrade.go6
-rw-r--r--store/sql_user_store.go2
-rw-r--r--webapp/components/admin_console/custom_integrations_settings.jsx21
-rw-r--r--webapp/components/admin_console/manage_roles_modal/index.js25
-rw-r--r--webapp/components/admin_console/manage_roles_modal/manage_roles_modal.jsx349
-rw-r--r--webapp/components/admin_console/manage_teams_modal/manage_teams_modal.jsx21
-rw-r--r--webapp/components/admin_console/manage_teams_modal/remove_from_team_button.jsx2
-rw-r--r--webapp/components/admin_console/manage_tokens_modal/index.js27
-rw-r--r--webapp/components/admin_console/manage_tokens_modal/manage_tokens_modal.jsx181
-rw-r--r--webapp/components/admin_console/revoke_token_button/index.js24
-rw-r--r--webapp/components/admin_console/revoke_token_button/revoke_token_button.jsx56
-rw-r--r--webapp/components/admin_console/system_users/index.js5
-rw-r--r--webapp/components/admin_console/system_users/system_users.jsx25
-rw-r--r--webapp/components/admin_console/system_users/system_users_dropdown.jsx175
-rw-r--r--webapp/components/admin_console/system_users/system_users_list.jsx103
-rw-r--r--webapp/components/setting_item_max.jsx55
-rw-r--r--webapp/components/user_settings/user_settings_security/index.js16
-rw-r--r--webapp/components/user_settings/user_settings_security/user_settings_security.jsx470
-rwxr-xr-xwebapp/i18n/en.json47
-rw-r--r--webapp/sass/components/_alerts.scss1
-rw-r--r--webapp/sass/layout/_content.scss6
-rw-r--r--webapp/sass/layout/_forms.scss32
-rw-r--r--webapp/sass/responsive/_tablet.scss6
-rw-r--r--webapp/sass/routes/_admin-console.scss59
-rw-r--r--webapp/sass/routes/_settings.scss16
-rw-r--r--webapp/sass/utils/_modifiers.scss96
-rw-r--r--webapp/utils/utils.jsx4
-rw-r--r--webapp/yarn.lock2
33 files changed, 1683 insertions, 245 deletions
diff --git a/api4/post_test.go b/api4/post_test.go
index 53babc6e6..f136ba676 100644
--- a/api4/post_test.go
+++ b/api4/post_test.go
@@ -302,6 +302,61 @@ func TestCreatePostPublic(t *testing.T) {
CheckNoError(t, resp)
}
+func TestCreatePostAll(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer TearDown()
+ Client := th.Client
+
+ post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "#hashtag a" + model.NewId() + "a"}
+
+ user := model.User{Email: GenerateTestEmail(), Nickname: "Joram Wilander", Password: "hello1", Username: GenerateTestUsername(), Roles: model.ROLE_SYSTEM_USER.Id}
+
+ directChannel, _ := app.CreateDirectChannel(th.BasicUser.Id, th.BasicUser2.Id)
+
+ ruser, resp := Client.CreateUser(&user)
+ CheckNoError(t, resp)
+
+ Client.Login(user.Email, user.Password)
+
+ _, resp = Client.CreatePost(post)
+ CheckForbiddenStatus(t, resp)
+
+ app.UpdateUserRoles(ruser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_POST_ALL.Id)
+ app.InvalidateAllCaches()
+
+ Client.Login(user.Email, user.Password)
+
+ _, resp = Client.CreatePost(post)
+ CheckNoError(t, resp)
+
+ post.ChannelId = th.BasicPrivateChannel.Id
+ _, resp = Client.CreatePost(post)
+ CheckNoError(t, resp)
+
+ post.ChannelId = directChannel.Id
+ _, resp = Client.CreatePost(post)
+ CheckNoError(t, resp)
+
+ app.UpdateUserRoles(ruser.Id, model.ROLE_SYSTEM_USER.Id)
+ app.JoinUserToTeam(th.BasicTeam, ruser, "")
+ app.UpdateTeamMemberRoles(th.BasicTeam.Id, ruser.Id, model.ROLE_TEAM_USER.Id+" "+model.ROLE_TEAM_POST_ALL.Id)
+ app.InvalidateAllCaches()
+
+ Client.Login(user.Email, user.Password)
+
+ post.ChannelId = th.BasicPrivateChannel.Id
+ _, resp = Client.CreatePost(post)
+ CheckNoError(t, resp)
+
+ post.ChannelId = th.BasicChannel.Id
+ _, resp = Client.CreatePost(post)
+ CheckNoError(t, resp)
+
+ post.ChannelId = directChannel.Id
+ _, resp = Client.CreatePost(post)
+ CheckForbiddenStatus(t, resp)
+}
+
func TestUpdatePost(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer TearDown()
diff --git a/app/authorization.go b/app/authorization.go
index 9fc2edfb9..28f968f68 100644
--- a/app/authorization.go
+++ b/app/authorization.go
@@ -48,7 +48,7 @@ func SessionHasPermissionToChannel(session model.Session, channelId string, perm
}
channel, err := GetChannel(channelId)
- if err == nil {
+ if err == nil && channel.TeamId != "" {
return SessionHasPermissionToTeam(session, channel.TeamId, permission)
}
diff --git a/app/diagnostics.go b/app/diagnostics.go
index 54fe843ac..603ceb8a5 100644
--- a/app/diagnostics.go
+++ b/app/diagnostics.go
@@ -165,6 +165,7 @@ func trackConfig() {
"enable_post_username_override": utils.Cfg.ServiceSettings.EnablePostUsernameOverride,
"enable_post_icon_override": utils.Cfg.ServiceSettings.EnablePostIconOverride,
"enable_apiv3": *utils.Cfg.ServiceSettings.EnableAPIv3,
+ "enable_user_access_tokens": *utils.Cfg.ServiceSettings.EnableUserAccessTokens,
"enable_custom_emoji": *utils.Cfg.ServiceSettings.EnableCustomEmoji,
"enable_emoji_picker": *utils.Cfg.ServiceSettings.EnableEmojiPicker,
"restrict_custom_emoji_creation": *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation,
diff --git a/i18n/en.json b/i18n/en.json
index c4940295b..3287af260 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -3504,6 +3504,14 @@
"translation": "Team name missing from User's Team Membership."
},
{
+ "id": "authentication.roles.system_post_all.name",
+ "translation": "Post in Public, Private and Direct Channels"
+ },
+ {
+ "id": "authentication.roles.system_post_all.description",
+ "translation": "A role with the permission to post in any public, private or direct channel on the system"
+ },
+ {
"id": "authentication.roles.system_post_all_public.name",
"translation": "Post in Public Channels"
},
@@ -3512,6 +3520,14 @@
"translation": "A role with the permission to post in any public channel on the system"
},
{
+ "id": "authentication.roles.team_post_all.name",
+ "translation": "Post in Public and Private Channels"
+ },
+ {
+ "id": "authentication.roles.team_post_all.description",
+ "translation": "A role with the permission to post in any public or private channel on the team"
+ },
+ {
"id": "authentication.roles.team_post_all_public.name",
"translation": "Post in Public Channels"
},
diff --git a/model/authorization.go b/model/authorization.go
index cf7e2b481..d413e294c 100644
--- a/model/authorization.go
+++ b/model/authorization.go
@@ -71,11 +71,13 @@ var PERMISSION_MANAGE_SYSTEM *Permission
var ROLE_SYSTEM_USER *Role
var ROLE_SYSTEM_ADMIN *Role
+var ROLE_SYSTEM_POST_ALL *Role
var ROLE_SYSTEM_POST_ALL_PUBLIC *Role
var ROLE_SYSTEM_USER_ACCESS_TOKEN *Role
var ROLE_TEAM_USER *Role
var ROLE_TEAM_ADMIN *Role
+var ROLE_TEAM_POST_ALL *Role
var ROLE_TEAM_POST_ALL_PUBLIC *Role
var ROLE_CHANNEL_USER *Role
@@ -376,6 +378,16 @@ func InitalizeRoles() {
}
BuiltInRoles[ROLE_TEAM_USER.Id] = ROLE_TEAM_USER
+ ROLE_TEAM_POST_ALL = &Role{
+ "team_post_all",
+ "authentication.roles.team_post_all.name",
+ "authentication.roles.team_post_all.description",
+ []string{
+ PERMISSION_CREATE_POST.Id,
+ },
+ }
+ BuiltInRoles[ROLE_TEAM_POST_ALL.Id] = ROLE_TEAM_POST_ALL
+
ROLE_TEAM_POST_ALL_PUBLIC = &Role{
"team_post_all_public",
"authentication.roles.team_post_all_public.name",
@@ -417,6 +429,16 @@ func InitalizeRoles() {
}
BuiltInRoles[ROLE_SYSTEM_USER.Id] = ROLE_SYSTEM_USER
+ ROLE_SYSTEM_POST_ALL = &Role{
+ "system_post_all",
+ "authentication.roles.system_post_all.name",
+ "authentication.roles.system_post_all.description",
+ []string{
+ PERMISSION_CREATE_POST.Id,
+ },
+ }
+ BuiltInRoles[ROLE_SYSTEM_POST_ALL.Id] = ROLE_SYSTEM_POST_ALL
+
ROLE_SYSTEM_POST_ALL_PUBLIC = &Role{
"system_post_all_public",
"authentication.roles.system_post_all_public.name",
diff --git a/store/sql_upgrade.go b/store/sql_upgrade.go
index a7b72124e..157a85507 100644
--- a/store/sql_upgrade.go
+++ b/store/sql_upgrade.go
@@ -282,6 +282,12 @@ func UpgradeDatabaseToVersion40(sqlStore SqlStore) {
func UpgradeDatabaseToVersion41(sqlStore SqlStore) {
// TODO: Uncomment following condition when version 4.1.0 is released
// if shouldPerformUpgrade(sqlStore, VERSION_4_0_0, VERSION_4_1_0) {
+
+ // Increase maximum length of the Users table Roles column.
+ if sqlStore.GetMaxLengthOfColumnIfExists("Users", "Roles") != "256" {
+ sqlStore.AlterColumnTypeIfExists("Users", "Roles", "varchar(256)", "varchar(256)")
+ }
+
sqlStore.RemoveTableIfExists("JobStatuses")
// saveSchemaVersion(sqlStore, VERSION_4_1_0)
// }
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index ab031ea19..64079c8d3 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -62,7 +62,7 @@ func NewSqlUserStore(sqlStore SqlStore) UserStore {
table.ColMap("Nickname").SetMaxSize(64)
table.ColMap("FirstName").SetMaxSize(64)
table.ColMap("LastName").SetMaxSize(64)
- table.ColMap("Roles").SetMaxSize(64)
+ table.ColMap("Roles").SetMaxSize(256)
table.ColMap("Props").SetMaxSize(4000)
table.ColMap("NotifyProps").SetMaxSize(2000)
table.ColMap("Locale").SetMaxSize(5)
diff --git a/webapp/components/admin_console/custom_integrations_settings.jsx b/webapp/components/admin_console/custom_integrations_settings.jsx
index 18fdd22fd..3b5c51171 100644
--- a/webapp/components/admin_console/custom_integrations_settings.jsx
+++ b/webapp/components/admin_console/custom_integrations_settings.jsx
@@ -25,6 +25,7 @@ export default class WebhookSettings extends AdminSettings {
config.ServiceSettings.EnablePostUsernameOverride = this.state.enablePostUsernameOverride;
config.ServiceSettings.EnablePostIconOverride = this.state.enablePostIconOverride;
config.ServiceSettings.EnableOAuthServiceProvider = this.state.enableOAuthServiceProvider;
+ config.ServiceSettings.EnableUserAccessTokens = this.state.enableUserAccessTokens;
return config;
}
@@ -37,7 +38,8 @@ export default class WebhookSettings extends AdminSettings {
enableOnlyAdminIntegrations: config.ServiceSettings.EnableOnlyAdminIntegrations,
enablePostUsernameOverride: config.ServiceSettings.EnablePostUsernameOverride,
enablePostIconOverride: config.ServiceSettings.EnablePostIconOverride,
- enableOAuthServiceProvider: config.ServiceSettings.EnableOAuthServiceProvider
+ enableOAuthServiceProvider: config.ServiceSettings.EnableOAuthServiceProvider,
+ enableUserAccessTokens: config.ServiceSettings.EnableUserAccessTokens
};
}
@@ -172,6 +174,23 @@ export default class WebhookSettings extends AdminSettings {
value={this.state.enablePostIconOverride}
onChange={this.handleChange}
/>
+ <BooleanSetting
+ id='enableUserAccessTokens'
+ label={
+ <FormattedMessage
+ id='admin.service.userAccessTokensTitle'
+ defaultMessage='Enable User Access Tokens: '
+ />
+ }
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.service.userAccessTokensDescription'
+ defaultMessage='When true, users can create <a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">user access tokens</a> for integrations in <strong>Account Settings > Security</strong>. They can be used to authenticate against the API and give full access to the account.<br/><br/>To manage who can create user access tokens, go to the <strong>System Console > Users</strong> page.'
+ />
+ }
+ value={this.state.enableUserAccessTokens}
+ onChange={this.handleChange}
+ />
</SettingsGroup>
);
}
diff --git a/webapp/components/admin_console/manage_roles_modal/index.js b/webapp/components/admin_console/manage_roles_modal/index.js
new file mode 100644
index 000000000..1ca243621
--- /dev/null
+++ b/webapp/components/admin_console/manage_roles_modal/index.js
@@ -0,0 +1,25 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {updateUserRoles} from 'mattermost-redux/actions/users';
+
+import ManageRolesModal from './manage_roles_modal.jsx';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ ...ownProps,
+ userAccessTokensEnabled: state.entities.admin.config.ServiceSettings.EnableUserAccessTokens
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ updateUserRoles
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ManageRolesModal);
diff --git a/webapp/components/admin_console/manage_roles_modal/manage_roles_modal.jsx b/webapp/components/admin_console/manage_roles_modal/manage_roles_modal.jsx
new file mode 100644
index 000000000..2358f0241
--- /dev/null
+++ b/webapp/components/admin_console/manage_roles_modal/manage_roles_modal.jsx
@@ -0,0 +1,349 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as UserUtils from 'mattermost-redux/utils/user_utils';
+import {Client4} from 'mattermost-redux/client';
+import {General} from 'mattermost-redux/constants';
+
+import {trackEvent} from 'actions/diagnostics_actions.jsx';
+
+import React from 'react';
+import {Modal} from 'react-bootstrap';
+import PropTypes from 'prop-types';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+function getStateFromProps(props) {
+ const roles = props.user && props.user.roles ? props.user.roles : '';
+
+ return {
+ error: null,
+ hasPostAllRole: UserUtils.hasPostAllRole(roles),
+ hasPostAllPublicRole: UserUtils.hasPostAllPublicRole(roles),
+ hasUserAccessTokenRole: UserUtils.hasUserAccessTokenRole(roles),
+ isSystemAdmin: UserUtils.isSystemAdmin(roles)
+ };
+}
+
+export default class ManageRolesModal extends React.PureComponent {
+ static propTypes = {
+
+ /**
+ * Set to render the modal
+ */
+ show: PropTypes.bool.isRequired,
+
+ /**
+ * The user the roles are being managed for
+ */
+ user: PropTypes.object,
+
+ /**
+ * Set if user access tokens are enabled
+ */
+ userAccessTokensEnabled: PropTypes.bool.isRequired,
+
+ /**
+ * Function called when modal is dismissed
+ */
+ onModalDismissed: PropTypes.func.isRequired,
+
+ actions: PropTypes.shape({
+
+ /**
+ * Function to update a user's roles
+ */
+ updateUserRoles: PropTypes.func.isRequired
+ }).isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = getStateFromProps(props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const user = this.props.user || {};
+ const nextUser = nextProps.user || {};
+ if (user.id !== nextUser.id) {
+ this.setState(getStateFromProps(nextProps));
+ }
+ }
+
+ handleError = (error) => {
+ this.setState({
+ error
+ });
+ }
+
+ handleSystemAdminChange = (e) => {
+ if (e.target.name === 'systemadmin') {
+ this.setState({isSystemAdmin: true});
+ } else if (e.target.name === 'systemmember') {
+ this.setState({isSystemAdmin: false});
+ }
+ };
+
+ handleUserAccessTokenChange = (e) => {
+ this.setState({
+ hasUserAccessTokenRole: e.target.checked
+ });
+ };
+
+ handlePostAllChange = (e) => {
+ this.setState({
+ hasPostAllRole: e.target.checked
+ });
+ };
+
+ handlePostAllPublicChange = (e) => {
+ this.setState({
+ hasPostAllPublicRole: e.target.checked
+ });
+ };
+
+ trackRoleChanges = (roles, oldRoles) => {
+ if (UserUtils.hasUserAccessTokenRole(roles) && !UserUtils.hasUserAccessTokenRole(oldRoles)) {
+ trackEvent('actions', 'add_roles', {role: General.SYSTEM_USER_ACCESS_TOKEN_ROLE});
+ } else if (!UserUtils.hasUserAccessTokenRole(roles) && UserUtils.hasUserAccessTokenRole(oldRoles)) {
+ trackEvent('actions', 'remove_roles', {role: General.SYSTEM_USER_ACCESS_TOKEN_ROLE});
+ }
+
+ if (UserUtils.hasPostAllRole(roles) && !UserUtils.hasPostAllRole(oldRoles)) {
+ trackEvent('actions', 'add_roles', {role: General.SYSTEM_POST_ALL_ROLE});
+ } else if (!UserUtils.hasPostAllRole(roles) && UserUtils.hasPostAllRole(oldRoles)) {
+ trackEvent('actions', 'remove_roles', {role: General.SYSTEM_POST_ALL_ROLE});
+ }
+
+ if (UserUtils.hasPostAllPublicRole(roles) && !UserUtils.hasPostAllPublicRole(oldRoles)) {
+ trackEvent('actions', 'add_roles', {role: General.SYSTEM_POST_ALL_PUBLIC_ROLE});
+ } else if (!UserUtils.hasPostAllPublicRole(roles) && UserUtils.hasPostAllPublicRole(oldRoles)) {
+ trackEvent('actions', 'remove_roles', {role: General.SYSTEM_POST_ALL_PUBLIC_ROLE});
+ }
+ }
+
+ handleSave = async () => {
+ this.setState({error: null});
+
+ let roles = General.SYSTEM_USER_ROLE;
+
+ if (this.state.isSystemAdmin) {
+ roles += ' ' + General.SYSTEM_ADMIN_ROLE;
+ } else if (this.state.hasUserAccessTokenRole) {
+ roles += ' ' + General.SYSTEM_USER_ACCESS_TOKEN_ROLE;
+ if (this.state.hasPostAllRole) {
+ roles += ' ' + General.SYSTEM_POST_ALL_ROLE;
+ } else if (this.state.hasPostAllPublicRole) {
+ roles += ' ' + General.SYSTEM_POST_ALL_PUBLIC_ROLE;
+ }
+ }
+
+ const data = await this.props.actions.updateUserRoles(this.props.user.id, roles);
+
+ this.trackRoleChanges(roles, this.props.user.roles);
+
+ if (data) {
+ this.props.onModalDismissed();
+ } else {
+ this.handleError(
+ <FormattedMessage
+ id='admin.manage_roles.saveError'
+ defaultMessage='Unable to save roles.'
+ />
+ );
+ }
+ }
+
+ renderContents = () => {
+ const {user} = this.props;
+
+ if (user == null) {
+ return <div/>;
+ }
+
+ let name = UserUtils.getFullName(user);
+ if (name) {
+ name += ` (@${user.username})`;
+ } else {
+ name = `@${user.username}`;
+ }
+
+ let additionalRoles;
+ if (this.state.hasUserAccessTokenRole || this.state.isSystemAdmin) {
+ additionalRoles = (
+ <div>
+ <p>
+ <FormattedHTMLMessage
+ id='admin.manage_roles.additionalRoles'
+ defaultMessage='Select additional permissions for the account. <a href="https://about.mattermost.com/default-permissions" target="_blank">Read more about roles and permissions</a>.'
+ />
+ </p>
+ <div className='checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ ref='postall'
+ checked={this.state.hasPostAllRole || this.state.isSystemAdmin}
+ disabled={this.state.isSystemAdmin}
+ onChange={this.handlePostAllChange}
+ />
+ <strong>
+ <FormattedMessage
+ id='admin.manage_roles.postAllRoleTitle'
+ defaultMessage='post:all'
+ />
+ </strong>
+ <FormattedMessage
+ id='admin.manage_roles.postAllRole'
+ defaultMessage='Access to post to all Mattermost channels including direct messages.'
+ />
+ </label>
+ </div>
+ <div className='checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ ref='postallpublic'
+ checked={this.state.hasPostAllPublicRole || this.state.hasPostAllRole || this.state.isSystemAdmin}
+ disabled={this.state.hasPostAllRole || this.state.isSystemAdmin}
+ onChange={this.handlePostAllPublicChange}
+ />
+ <strong>
+ <FormattedMessage
+ id='admin.manage_roles.postAllPublicRoleTitle'
+ defaultMessage='post:channels'
+ />
+ </strong>
+ <FormattedMessage
+ id='admin.manage_roles.postAllPublicRole'
+ defaultMessage='Access to post to all Mattermost public channels.'
+ />
+ </label>
+ </div>
+ </div>
+ );
+ }
+
+ let userAccessTokenContent;
+ if (this.props.userAccessTokensEnabled) {
+ userAccessTokenContent = (
+ <div>
+ <div className='checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ ref='postall'
+ checked={this.state.hasUserAccessTokenRole || this.state.isSystemAdmin}
+ disabled={this.state.isSystemAdmin}
+ onChange={this.handleUserAccessTokenChange}
+ />
+ <FormattedHTMLMessage
+ id='admin.manage_roles.allowUserAccessTokens'
+ defaultMessage='Allow this account to generate <a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">user access tokens</a>.'
+ />
+ </label>
+ </div>
+ <div className='member-row--padded'>
+ {additionalRoles}
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div>
+ <div className='manage-teams__user'>
+ <img
+ className='manage-teams__profile-picture'
+ src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
+ />
+ <div className='manage-teams__info'>
+ <div className='manage-teams__name'>
+ {name}
+ </div>
+ <div className='manage-teams__email'>
+ {user.email}
+ </div>
+ </div>
+ </div>
+ <div>
+ <div className='manage-row--inner'>
+ <div className='radio-inline'>
+ <label>
+ <input
+ name='systemadmin'
+ type='radio'
+ checked={this.state.isSystemAdmin}
+ onChange={this.handleSystemAdminChange}
+ />
+ <FormattedMessage
+ id='admin.manage_roles.systemAdmin'
+ defaultMessage='System Admin'
+ />
+ </label>
+ </div>
+ <div className='radio-inline'>
+ <label>
+ <input
+ name='systemmember'
+ type='radio'
+ checked={!this.state.isSystemAdmin}
+ onChange={this.handleSystemAdminChange}
+ />
+ <FormattedMessage
+ id='admin.manage_roles.systemMember'
+ defaultMessage='Member'
+ />
+ </label>
+ </div>
+ </div>
+ {userAccessTokenContent}
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onModalDismissed}
+ dialogClassName='manage-teams'
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>
+ <FormattedMessage
+ id='admin.manage_roles.manageRolesTitle'
+ defaultMessage='Manage Roles'
+ />
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ {this.renderContents()}
+ {this.state.error}
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-link'
+ onClick={this.props.onModalDismissed}
+ >
+ <FormattedMessage
+ id='admin.manage_roles.cancel'
+ defaultMessage='Cancel'
+ />
+ </button>
+ <button
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handleSave}
+ >
+ <FormattedMessage
+ id='admin.manage_roles.save'
+ defaultMessage='Save'
+ />
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/manage_teams_modal/manage_teams_modal.jsx b/webapp/components/admin_console/manage_teams_modal/manage_teams_modal.jsx
index a579ab03c..21f9d762d 100644
--- a/webapp/components/admin_console/manage_teams_modal/manage_teams_modal.jsx
+++ b/webapp/components/admin_console/manage_teams_modal/manage_teams_modal.jsx
@@ -1,11 +1,10 @@
-import PropTypes from 'prop-types';
-
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
+import PropTypes from 'prop-types';
import * as TeamActions from 'actions/team_actions.jsx';
@@ -29,14 +28,6 @@ export default class ManageTeamsModal extends React.Component {
constructor(props) {
super(props);
- this.loadTeamsAndTeamMembers = this.loadTeamsAndTeamMembers.bind(this);
-
- this.handleError = this.handleError.bind(this);
- this.handleMemberChange = this.handleMemberChange.bind(this);
- this.handleMemberRemove = this.handleMemberRemove.bind(this);
-
- this.renderContents = this.renderContents.bind(this);
-
this.state = {
error: null,
teams: null,
@@ -66,7 +57,7 @@ export default class ManageTeamsModal extends React.Component {
}
}
- loadTeamsAndTeamMembers(user = this.props.user) {
+ loadTeamsAndTeamMembers = (user = this.props.user) => {
TeamActions.getTeamsForUser(user.id, (teams) => {
this.setState({
teams: teams.sort(sortTeamsByDisplayName)
@@ -80,13 +71,13 @@ export default class ManageTeamsModal extends React.Component {
});
}
- handleError(error) {
+ handleError = (error) => {
this.setState({
error
});
}
- handleMemberChange() {
+ handleMemberChange = () => {
TeamActions.getTeamMembersForUser(this.props.user.id, (teamMembers) => {
this.setState({
teamMembers
@@ -94,14 +85,14 @@ export default class ManageTeamsModal extends React.Component {
});
}
- handleMemberRemove(teamId) {
+ handleMemberRemove = (teamId) => {
this.setState({
teams: this.state.teams.filter((team) => team.id !== teamId),
teamMembers: this.state.teamMembers.filter((teamMember) => teamMember.team_id !== teamId)
});
}
- renderContents() {
+ renderContents = () => {
const {user} = this.props;
const {teams, teamMembers} = this.state;
diff --git a/webapp/components/admin_console/manage_teams_modal/remove_from_team_button.jsx b/webapp/components/admin_console/manage_teams_modal/remove_from_team_button.jsx
index 28e9fde8f..69579d46f 100644
--- a/webapp/components/admin_console/manage_teams_modal/remove_from_team_button.jsx
+++ b/webapp/components/admin_console/manage_teams_modal/remove_from_team_button.jsx
@@ -41,7 +41,7 @@ export default class RemoveFromTeamButton extends React.PureComponent {
render() {
return (
<button
- className='btn btn-default'
+ className='btn btn-danger'
onClick={this.handleClick}
>
<FormattedMessage
diff --git a/webapp/components/admin_console/manage_tokens_modal/index.js b/webapp/components/admin_console/manage_tokens_modal/index.js
new file mode 100644
index 000000000..9f7a31141
--- /dev/null
+++ b/webapp/components/admin_console/manage_tokens_modal/index.js
@@ -0,0 +1,27 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {getUserAccessTokensForUser} from 'mattermost-redux/actions/users';
+
+import ManageTokensModal from './manage_tokens_modal.jsx';
+
+function mapStateToProps(state, ownProps) {
+ const userId = ownProps.user ? ownProps.user.id : '';
+
+ return {
+ ...ownProps,
+ userAccessTokens: state.entities.admin.userAccessTokens[userId]
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ getUserAccessTokensForUser
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ManageTokensModal);
diff --git a/webapp/components/admin_console/manage_tokens_modal/manage_tokens_modal.jsx b/webapp/components/admin_console/manage_tokens_modal/manage_tokens_modal.jsx
new file mode 100644
index 000000000..c31325291
--- /dev/null
+++ b/webapp/components/admin_console/manage_tokens_modal/manage_tokens_modal.jsx
@@ -0,0 +1,181 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LoadingScreen from 'components/loading_screen.jsx';
+import RevokeTokenButton from 'components/admin_console/revoke_token_button';
+
+import {Client4} from 'mattermost-redux/client';
+import * as UserUtils from 'mattermost-redux/utils/user_utils';
+
+import React from 'react';
+import {Modal} from 'react-bootstrap';
+import PropTypes from 'prop-types';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+export default class ManageTokensModal extends React.PureComponent {
+ static propTypes = {
+
+ /**
+ * Set to render the modal
+ */
+ show: PropTypes.bool.isRequired,
+
+ /**
+ * The user the roles are being managed for
+ */
+ user: PropTypes.object,
+
+ /**
+ * The user access tokens for a user, object with token ids as keys
+ */
+ userAccessTokens: PropTypes.object,
+
+ /**
+ * Function called when modal is dismissed
+ */
+ onModalDismissed: PropTypes.func.isRequired,
+
+ actions: PropTypes.shape({
+
+ /**
+ * Function to get a user's access tokens
+ */
+ getUserAccessTokensForUser: PropTypes.func.isRequired
+ }).isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const userId = this.props.user ? this.props.user.id : null;
+ const nextUserId = nextProps.user ? nextProps.user.id : null;
+ if (nextUserId && nextUserId !== userId) {
+ this.props.actions.getUserAccessTokensForUser(nextUserId, 0, 200);
+ }
+ }
+
+ handleError = (error) => {
+ this.setState({
+ error
+ });
+ }
+
+ renderContents = () => {
+ const {user, userAccessTokens} = this.props;
+
+ if (!user) {
+ return <LoadingScreen/>;
+ }
+
+ let name = UserUtils.getFullName(user);
+ if (name) {
+ name += ` (@${user.username})`;
+ } else {
+ name = `@${user.username}`;
+ }
+
+ let tokenList;
+ if (userAccessTokens) {
+ const userAccessTokensList = Object.values(userAccessTokens);
+
+ if (userAccessTokensList.length === 0) {
+ tokenList = (
+ <div className='manage-row__empty'>
+ <FormattedMessage
+ id='admin.manage_tokens.userAccessTokensNone'
+ defaultMessage='No user access tokens.'
+ />
+ </div>
+ );
+ } else {
+ tokenList = userAccessTokensList.map((token) => {
+ return (
+ <div
+ key={token.id}
+ className='manage-teams__team'
+ >
+ <div className='manage-teams__team-name'>
+ <div>
+ <FormattedMessage
+ id='admin.manage_tokens.userAccessTokensNameLabel'
+ defaultMessage='Name: '
+ />
+ {token.description}
+ </div>
+ <div>
+ <FormattedMessage
+ id='admin.manage_tokens.userAccessTokensIdLabel'
+ defaultMessage='Token ID: '
+ />
+ {token.id}
+ </div>
+ </div>
+ <div className='manage-teams__team-actions'>
+ <RevokeTokenButton
+ tokenId={token.id}
+ onError={this.handleError}
+ />
+ </div>
+ </div>
+ );
+ });
+ }
+ } else {
+ tokenList = <LoadingScreen/>;
+ }
+
+ return (
+ <div>
+ <div className='manage-teams__user'>
+ <img
+ className='manage-teams__profile-picture'
+ src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
+ />
+ <div className='manage-teams__info'>
+ <div className='manage-teams__name'>
+ {name}
+ </div>
+ <div className='manage-teams__email'>
+ {user.email}
+ </div>
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <FormattedHTMLMessage
+ id='admin.manage_tokens.userAccessTokensDescription'
+ defaultMessage='User access tokens 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>. Learn more about <a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">user access tokens</a>.'
+ />
+ </div>
+ <div className='manage-teams__teams'>
+ {tokenList}
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onModalDismissed}
+ dialogClassName='manage-teams'
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>
+ <FormattedMessage
+ id='admin.manage_tokens.manageTokensTitle'
+ defaultMessage='Manage User Access Tokens'
+ />
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ {this.renderContents()}
+ {this.state.error}
+ </Modal.Body>
+ </Modal>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/revoke_token_button/index.js b/webapp/components/admin_console/revoke_token_button/index.js
new file mode 100644
index 000000000..6fada1bcc
--- /dev/null
+++ b/webapp/components/admin_console/revoke_token_button/index.js
@@ -0,0 +1,24 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {revokeUserAccessToken} from 'mattermost-redux/actions/users';
+
+import RevokeTokenButton from './revoke_token_button.jsx';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ ...ownProps
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ revokeUserAccessToken
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(RevokeTokenButton);
diff --git a/webapp/components/admin_console/revoke_token_button/revoke_token_button.jsx b/webapp/components/admin_console/revoke_token_button/revoke_token_button.jsx
new file mode 100644
index 000000000..4829a0cde
--- /dev/null
+++ b/webapp/components/admin_console/revoke_token_button/revoke_token_button.jsx
@@ -0,0 +1,56 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import {FormattedMessage} from 'react-intl';
+
+import {trackEvent} from 'actions/diagnostics_actions.jsx';
+
+export default class RevokeTokenButton extends React.PureComponent {
+ static propTypes = {
+
+ /*
+ * Token id to revoke
+ */
+ tokenId: PropTypes.string.isRequired,
+
+ /*
+ * Function to call on error
+ */
+ onError: PropTypes.func.isRequired,
+
+ actions: PropTypes.shape({
+
+ /**
+ * Function to revoke a user access token
+ */
+ revokeUserAccessToken: PropTypes.func.isRequired
+ }).isRequired
+ };
+
+ handleClick = async (e) => {
+ e.preventDefault();
+
+ const {error} = await this.props.actions.revokeUserAccessToken(this.props.tokenId);
+ trackEvent('system_console', 'revoke_user_access_token');
+
+ if (error) {
+ this.props.onError(error.message);
+ }
+ }
+
+ render() {
+ return (
+ <button
+ className='btn btn-danger'
+ onClick={this.handleClick}
+ >
+ <FormattedMessage
+ id='admin.revoke_token_button.delete'
+ defaultMessage='Delete'
+ />
+ </button>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/system_users/index.js b/webapp/components/admin_console/system_users/index.js
index 8f1c0dc35..261a11d7e 100644
--- a/webapp/components/admin_console/system_users/index.js
+++ b/webapp/components/admin_console/system_users/index.js
@@ -4,7 +4,7 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getTeams, getTeamStats} from 'mattermost-redux/actions/teams';
-import {getUser} from 'mattermost-redux/actions/users';
+import {getUser, getUserAccessToken} from 'mattermost-redux/actions/users';
import {getTeamsList} from 'mattermost-redux/selectors/entities/teams';
@@ -22,7 +22,8 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
getTeams,
getTeamStats,
- getUser
+ getUser,
+ getUserAccessToken
}, dispatch)
};
}
diff --git a/webapp/components/admin_console/system_users/system_users.jsx b/webapp/components/admin_console/system_users/system_users.jsx
index 5c8aa9bfd..4fbdc26d8 100644
--- a/webapp/components/admin_console/system_users/system_users.jsx
+++ b/webapp/components/admin_console/system_users/system_users.jsx
@@ -54,7 +54,12 @@ export default class SystemUsers extends React.Component {
/*
* Function to get a user
*/
- getUser: PropTypes.func.isRequired
+ getUser: PropTypes.func.isRequired,
+
+ /*
+ * Function to get a user access token
+ */
+ getUserAccessToken: PropTypes.func.isRequired
}).isRequired
}
@@ -240,7 +245,7 @@ export default class SystemUsers extends React.Component {
(users) => {
if (users.length === 0 && term.length === USER_ID_LENGTH) {
// This term didn't match any users name, but it does look like it might be a user's ID
- this.getUserById(term);
+ this.getUserByTokenOrId(term);
} else {
this.setState({loading: false});
}
@@ -269,6 +274,22 @@ export default class SystemUsers extends React.Component {
);
}
+ getUserByTokenOrId = async (id) => {
+ if (global.window.mm_config.EnableUserAccessTokens === 'true') {
+ const {data} = await this.props.actions.getUserAccessToken(id);
+
+ if (data) {
+ this.term = data.user_id;
+ this.setState({term: data.user_id});
+ this.updateUsersFromStore(this.state.teamId, data.user_id);
+ this.getUserById(data.user_id);
+ return;
+ }
+ }
+
+ this.getUserById(id);
+ }
+
renderFilterRow(doSearch) {
const teams = this.props.teams.map((team) => {
return (
diff --git a/webapp/components/admin_console/system_users/system_users_dropdown.jsx b/webapp/components/admin_console/system_users/system_users_dropdown.jsx
index fe53ade44..1dbb6b325 100644
--- a/webapp/components/admin_console/system_users/system_users_dropdown.jsx
+++ b/webapp/components/admin_console/system_users/system_users_dropdown.jsx
@@ -8,7 +8,7 @@ import UserStore from 'stores/user_store.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
-import {updateUserRoles, updateActive} from 'actions/user_actions.jsx';
+import {updateActive} from 'actions/user_actions.jsx';
import {adminResetMfa} from 'actions/admin_actions.jsx';
import {FormattedMessage} from 'react-intl';
@@ -19,28 +19,36 @@ import React from 'react';
export default class SystemUsersDropdown extends React.Component {
static propTypes = {
+
+ /*
+ * User to manage with dropdown
+ */
user: PropTypes.object.isRequired,
+
+ /*
+ * Function to open password reset, takes user as an argument
+ */
doPasswordReset: PropTypes.func.isRequired,
- doManageTeams: PropTypes.func.isRequired
+
+ /*
+ * Function to open manage teams, takes user as an argument
+ */
+ doManageTeams: PropTypes.func.isRequired,
+
+ /*
+ * Function to open manage roles, takes user as an argument
+ */
+ doManageRoles: PropTypes.func.isRequired,
+
+ /*
+ * Function to open manage tokens, takes user as an argument
+ */
+ doManageTokens: PropTypes.func.isRequired
};
constructor(props) {
super(props);
- this.handleMakeMember = this.handleMakeMember.bind(this);
- this.handleMakeActive = this.handleMakeActive.bind(this);
- this.handleShowDeactivateMemberModal = this.handleShowDeactivateMemberModal.bind(this);
- this.handleDeactivateMember = this.handleDeactivateMember.bind(this);
- this.handleDeactivateCancel = this.handleDeactivateCancel.bind(this);
- this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
- this.handleManageTeams = this.handleManageTeams.bind(this);
- this.handleResetPassword = this.handleResetPassword.bind(this);
- this.handleResetMfa = this.handleResetMfa.bind(this);
- this.handleDemoteSystemAdmin = this.handleDemoteSystemAdmin.bind(this);
- this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this);
- this.handleDemoteCancel = this.handleDemoteCancel.bind(this);
- this.renderDeactivateMemberModal = this.renderDeactivateMemberModal.bind(this);
-
this.state = {
serverError: null,
showDemoteModal: false,
@@ -50,61 +58,39 @@ export default class SystemUsersDropdown extends React.Component {
};
}
- doMakeMember() {
- updateUserRoles(
- this.props.user.id,
- 'system_user',
- null,
+ handleMakeActive = (e) => {
+ e.preventDefault();
+ updateActive(this.props.user.id, true, null,
(err) => {
this.setState({serverError: err.message});
}
);
}
- handleMakeMember(e) {
+ handleManageTeams = (e) => {
e.preventDefault();
- const me = UserStore.getCurrentUser();
- if (this.props.user.id === me.id && me.roles.includes('system_admin')) {
- this.handleDemoteSystemAdmin(this.props.user, 'member');
- } else {
- this.doMakeMember();
- }
- }
- handleMakeActive(e) {
- e.preventDefault();
- updateActive(this.props.user.id, true, null,
- (err) => {
- this.setState({serverError: err.message});
- }
- );
+ this.props.doManageTeams(this.props.user);
}
- handleMakeSystemAdmin(e) {
+ handleManageRoles = (e) => {
e.preventDefault();
- updateUserRoles(
- this.props.user.id,
- 'system_user system_admin',
- null,
- (err) => {
- this.setState({serverError: err.message});
- }
- );
+ this.props.doManageRoles(this.props.user);
}
- handleManageTeams(e) {
+ handleManageTokens = (e) => {
e.preventDefault();
- this.props.doManageTeams(this.props.user);
+ this.props.doManageTokens(this.props.user);
}
- handleResetPassword(e) {
+ handleResetPassword = (e) => {
e.preventDefault();
this.props.doPasswordReset(this.props.user);
}
- handleResetMfa(e) {
+ handleResetMfa = (e) => {
e.preventDefault();
adminResetMfa(this.props.user.id,
@@ -115,7 +101,7 @@ export default class SystemUsersDropdown extends React.Component {
);
}
- handleDemoteSystemAdmin(user, role) {
+ handleDemoteSystemAdmin = (user, role) => {
this.setState({
serverError: this.state.serverError,
showDemoteModal: true,
@@ -124,7 +110,7 @@ export default class SystemUsersDropdown extends React.Component {
});
}
- handleDemoteCancel() {
+ handleDemoteCancel = () => {
this.setState({
serverError: null,
showDemoteModal: false,
@@ -133,7 +119,7 @@ export default class SystemUsersDropdown extends React.Component {
});
}
- handleDemoteSubmit() {
+ handleDemoteSubmit = () => {
if (this.state.role === 'member') {
this.doMakeMember();
}
@@ -147,13 +133,13 @@ export default class SystemUsersDropdown extends React.Component {
}
}
- handleShowDeactivateMemberModal(e) {
+ handleShowDeactivateMemberModal = (e) => {
e.preventDefault();
this.setState({showDeactivateMemberModal: true});
}
- handleDeactivateMember() {
+ handleDeactivateMember = () => {
updateActive(this.props.user.id, false, null,
(err) => {
this.setState({serverError: err.message});
@@ -163,11 +149,11 @@ export default class SystemUsersDropdown extends React.Component {
this.setState({showDeactivateMemberModal: false});
}
- handleDeactivateCancel() {
+ handleDeactivateCancel = () => {
this.setState({showDeactivateMemberModal: false});
}
- renderDeactivateMemberModal() {
+ renderDeactivateMemberModal = () => {
const title = (
<FormattedMessage
id='deactivate_member_modal.title'
@@ -240,8 +226,6 @@ export default class SystemUsersDropdown extends React.Component {
}
const me = UserStore.getCurrentUser();
- let showMakeMember = Utils.isSystemAdmin(user.roles);
- let showMakeSystemAdmin = !Utils.isSystemAdmin(user.roles);
let showMakeActive = false;
let showMakeNotActive = !Utils.isSystemAdmin(user.roles);
let showManageTeams = true;
@@ -255,8 +239,6 @@ export default class SystemUsersDropdown extends React.Component {
defaultMessage='Inactive'
/>
);
- showMakeMember = false;
- showMakeSystemAdmin = false;
showMakeActive = true;
showMakeNotActive = false;
showManageTeams = false;
@@ -267,44 +249,6 @@ export default class SystemUsersDropdown extends React.Component {
disableActivationToggle = true;
}
- let makeSystemAdmin = null;
- if (showMakeSystemAdmin) {
- makeSystemAdmin = (
- <li role='presentation'>
- <a
- id='makeSystemAdmin'
- role='menuitem'
- href='#'
- onClick={this.handleMakeSystemAdmin}
- >
- <FormattedMessage
- id='admin.user_item.makeSysAdmin'
- defaultMessage='Make System Admin'
- />
- </a>
- </li>
- );
- }
-
- let makeMember = null;
- if (showMakeMember) {
- makeMember = (
- <li role='presentation'>
- <a
- id='makeMember'
- role='menuitem'
- href='#'
- onClick={this.handleMakeMember}
- >
- <FormattedMessage
- id='admin.user_item.makeMember'
- defaultMessage='Make Member'
- />
- </a>
- </li>
- );
- }
-
let menuClass = '';
if (disableActivationToggle) {
menuClass = 'disabled';
@@ -427,6 +371,25 @@ export default class SystemUsersDropdown extends React.Component {
);
}
+ let manageTokens;
+ if (global.window.mm_config.EnableUserAccessTokens === 'true') {
+ manageTokens = (
+ <li role='presentation'>
+ <a
+ id='manageTokens'
+ role='menuitem'
+ href='#'
+ onClick={this.handleManageTokens}
+ >
+ <FormattedMessage
+ id='admin.user_item.manageTokens'
+ defaultMessage='Manage Tokens'
+ />
+ </a>
+ </li>
+ );
+ }
+
let makeDemoteModal = null;
if (this.props.user.id === me.id) {
const title = (
@@ -498,11 +461,23 @@ export default class SystemUsersDropdown extends React.Component {
className='dropdown-menu member-menu'
role='menu'
>
- {makeMember}
{makeActive}
{makeNotActive}
- {makeSystemAdmin}
+ <li role='presentation'>
+ <a
+ id='manageRoles'
+ role='menuitem'
+ href='#'
+ onClick={this.handleManageRoles}
+ >
+ <FormattedMessage
+ id='admin.user_item.manageRoles'
+ defaultMessage='Manage Roles'
+ />
+ </a>
+ </li>
{manageTeams}
+ {manageTokens}
{mfaReset}
{passwordReset}
</ul>
diff --git a/webapp/components/admin_console/system_users/system_users_list.jsx b/webapp/components/admin_console/system_users/system_users_list.jsx
index 6d58137ff..2863f9cec 100644
--- a/webapp/components/admin_console/system_users/system_users_list.jsx
+++ b/webapp/components/admin_console/system_users/system_users_list.jsx
@@ -6,6 +6,8 @@ import PropTypes from 'prop-types';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import ManageTeamsModal from 'components/admin_console/manage_teams_modal/manage_teams_modal.jsx';
+import ManageRolesModal from 'components/admin_console/manage_roles_modal';
+import ManageTokensModal from 'components/admin_console/manage_tokens_modal';
import ResetPasswordModal from 'components/admin_console/reset_password_modal.jsx';
import SearchableUserList from 'components/searchable_user_list/searchable_user_list.jsx';
@@ -14,6 +16,7 @@ const dispatch = store.dispatch;
const getState = store.getState;
import {getUser} from 'mattermost-redux/actions/users';
+import * as UserUtils from 'mattermost-redux/utils/user_utils';
import {Constants} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -37,21 +40,12 @@ export default class SystemUsersList extends React.Component {
constructor(props) {
super(props);
- this.nextPage = this.nextPage.bind(this);
- this.previousPage = this.previousPage.bind(this);
- this.search = this.search.bind(this);
-
- this.doManageTeams = this.doManageTeams.bind(this);
- this.doManageTeamsDismiss = this.doManageTeamsDismiss.bind(this);
-
- this.doPasswordReset = this.doPasswordReset.bind(this);
- this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
- this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
-
this.state = {
page: 0,
showManageTeamsModal: false,
+ showManageRolesModal: false,
+ showManageTokensModal: false,
showPasswordModal: false,
user: null
};
@@ -63,17 +57,17 @@ export default class SystemUsersList extends React.Component {
}
}
- nextPage() {
+ nextPage = () => {
this.setState({page: this.state.page + 1});
this.props.nextPage(this.state.page + 1);
}
- previousPage() {
+ previousPage = () => {
this.setState({page: this.state.page - 1});
}
- search(term) {
+ search = (term) => {
this.props.search(term);
if (term !== '') {
@@ -81,35 +75,63 @@ export default class SystemUsersList extends React.Component {
}
}
- doManageTeams(user) {
+ doManageTeams = (user) => {
this.setState({
showManageTeamsModal: true,
user
});
}
- doManageTeamsDismiss() {
+ doManageRoles = (user) => {
+ this.setState({
+ showManageRolesModal: true,
+ user
+ });
+ }
+
+ doManageTokens = (user) => {
+ this.setState({
+ showManageTokensModal: true,
+ user
+ });
+ }
+
+ doManageTeamsDismiss = () => {
this.setState({
showManageTeamsModal: false,
user: null
});
}
- doPasswordReset(user) {
+ doManageRolesDismiss = () => {
+ this.setState({
+ showManageRolesModal: false,
+ user: null
+ });
+ }
+
+ doManageTokensDismiss = () => {
+ this.setState({
+ showManageTokensModal: false,
+ user: null
+ });
+ }
+
+ doPasswordReset = (user) => {
this.setState({
showPasswordModal: true,
user
});
}
- doPasswordResetDismiss() {
+ doPasswordResetDismiss = () => {
this.setState({
showPasswordModal: false,
user: null
});
}
- doPasswordResetSubmit(user) {
+ doPasswordResetSubmit = (user) => {
getUser(user.id)(dispatch, getState);
this.setState({
@@ -174,6 +196,35 @@ export default class SystemUsersList extends React.Component {
}
}
+ const userAccessTokensEnabled = global.window.mm_config.EnableUserAccessTokens === 'true';
+ if (userAccessTokensEnabled) {
+ const hasPostAllRole = UserUtils.hasPostAllRole(user.roles);
+ const hasPostAllPublicRole = UserUtils.hasPostAllPublicRole(user.roles);
+ const hasUserAccessTokenRole = UserUtils.hasUserAccessTokenRole(user.roles);
+ const isSystemAdmin = UserUtils.isSystemAdmin(user.roles);
+
+ let messageId = 'admin.user_item.userAccessTokenNo';
+ if (hasUserAccessTokenRole || isSystemAdmin) {
+ if (isSystemAdmin) {
+ messageId = 'admin.user_item.userAccessTokenAdmin';
+ } else if (hasPostAllRole) {
+ messageId = 'admin.user_item.userAccessTokenPostAll';
+ } else if (hasPostAllPublicRole) {
+ messageId = 'admin.user_item.userAccessTokenPostAllPublic';
+ } else {
+ messageId = 'admin.user_item.userAccessTokenYes';
+ }
+ }
+
+ info.push(', ');
+ info.push(
+ <FormattedHTMLMessage
+ key='admin.user_item.userAccessToken'
+ id={messageId}
+ />
+ );
+ }
+
return info;
}
@@ -236,7 +287,9 @@ export default class SystemUsersList extends React.Component {
actions={[SystemUsersDropdown]}
actionProps={{
doPasswordReset: this.doPasswordReset,
- doManageTeams: this.doManageTeams
+ doManageTeams: this.doManageTeams,
+ doManageRoles: this.doManageRoles,
+ doManageTokens: this.doManageTokens
}}
nextPage={this.nextPage}
previousPage={this.previousPage}
@@ -250,6 +303,16 @@ export default class SystemUsersList extends React.Component {
show={this.state.showManageTeamsModal}
onModalDismissed={this.doManageTeamsDismiss}
/>
+ <ManageRolesModal
+ user={this.state.user}
+ show={this.state.showManageRolesModal}
+ onModalDismissed={this.doManageRolesDismiss}
+ />
+ <ManageTokensModal
+ user={this.state.user}
+ show={this.state.showManageTokensModal}
+ onModalDismissed={this.doManageTokensDismiss}
+ />
<ResetPasswordModal
user={this.state.user}
show={this.state.showPasswordModal}
diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx
index 8e3aaf12c..1f0af181e 100644
--- a/webapp/components/setting_item_max.jsx
+++ b/webapp/components/setting_item_max.jsx
@@ -17,7 +17,7 @@ export default class SettingItemMax extends React.Component {
}
onKeyDown(e) {
- if (e.keyCode === Constants.KeyCodes.ENTER) {
+ if (e.keyCode === Constants.KeyCodes.ENTER && this.props.submit) {
this.props.submit(e);
}
}
@@ -60,8 +60,13 @@ export default class SettingItemMax extends React.Component {
}
var extraInfo = null;
+ let hintClass = 'setting-list__hint';
+ if (this.props.infoPosition === 'top') {
+ hintClass = 'padding-bottom x2';
+ }
+
if (this.props.extraInfo) {
- extraInfo = (<div className='setting-list__hint'>{this.props.extraInfo}</div>);
+ extraInfo = (<div className={hintClass}>{this.props.extraInfo}</div>);
}
var submit = '';
@@ -95,15 +100,40 @@ export default class SettingItemMax extends React.Component {
titleProp = this.props.title;
}
+ let listContent = (
+ <li className='setting-list-item'>
+ {inputs}
+ {extraInfo}
+ </li>
+ );
+
+ if (this.props.infoPosition === 'top') {
+ listContent = (
+ <li>
+ {extraInfo}
+ {inputs}
+ </li>
+ );
+ }
+
+ let cancelButtonText;
+ if (this.props.cancelButtonText) {
+ cancelButtonText = this.props.cancelButtonText;
+ } else {
+ cancelButtonText = (
+ <FormattedMessage
+ id='setting_item_max.cancel'
+ defaultMessage='Cancel'
+ />
+ );
+ }
+
return (
<ul className='section-max form-horizontal'>
{title}
<li className={widthClass}>
<ul className='setting-list'>
- <li className='setting-list-item'>
- {inputs}
- {extraInfo}
- </li>
+ {listContent}
<li className='setting-list-item'>
<hr/>
{this.props.submitExtra}
@@ -116,10 +146,7 @@ export default class SettingItemMax extends React.Component {
href='#'
onClick={this.props.updateSection}
>
- <FormattedMessage
- id='setting_item_max.cancel'
- defaultMessage='Cancel'
- />
+ {cancelButtonText}
</a>
</li>
</ul>
@@ -134,9 +161,15 @@ SettingItemMax.propTypes = {
client_error: PropTypes.string,
server_error: PropTypes.string,
extraInfo: PropTypes.element,
+ infoPosition: PropTypes.string,
updateSection: PropTypes.func,
submit: PropTypes.func,
title: PropTypes.node,
width: PropTypes.string,
- submitExtra: PropTypes.node
+ submitExtra: PropTypes.node,
+ cancelButtonText: PropTypes.node
+};
+
+SettingItemMax.defaultProps = {
+ infoPosition: 'bottom'
};
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: (
+ <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 it’s 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>
);
}
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 79d291e47..58674dd3c 100755
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -745,6 +745,10 @@
"admin.select_team.close": "Close",
"admin.select_team.select": "Select",
"admin.select_team.selectTeam": "Select Team",
+ "admin.service.userAccessTokensTitle": "Enable User Access Tokens: ",
+ "admin.service.userAccessTokensDescription": "When true, users can create <a href=\"https://about.mattermost.com/default-user-access-tokens\" target=\"_blank\">user access tokens</a> for integrations in <strong>Account Settings > Security</strong>. They can be used to authenticate against the API and give full access to the account.<br/><br/>To manage who can create user access tokens, go to the <strong>System Console > Users</strong> page.",
+ "admin.service.userAccessTokensNameLabel": "Name: ",
+ "admin.service.userAccessTokensIdLabel": "Token ID: ",
"admin.service.attemptDescription": "Number of login attempts allowed before a user is locked out and required to reset their password via email.",
"admin.service.attemptExample": "E.g.: \"10\"",
"admin.service.attemptTitle": "Maximum Login Attempts:",
@@ -954,6 +958,26 @@
"admin.team_analytics.activeUsers": "Active Users With Posts",
"admin.team_analytics.totalPosts": "Total Posts",
"admin.true": "true",
+ "admin.manage_tokens.userAccessTokensNone": "No user access tokens.",
+ "admin.manage_tokens.manageTokensTitle": "Manage User Access Tokens",
+ "admin.manage_tokens.userAccessTokensDescription": "User access tokens 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>. Learn more about <a href=\"https://about.mattermost.com/default-user-access-tokens\" target=\"_blank\">user access tokens</a>.",
+ "admin.manage_roles.saveError": "Unable to save roles.",
+ "admin.manage_roles.additionalRoles": "Select additional permissions for the account. <a href=\"https://about.mattermost.com/default-permissions\" target=\"_blank\">Read more about roles and permissions</a>.",
+ "admin.manage_roles.postAllRoleTitle": "post:all",
+ "admin.manage_roles.postAllRole": "Access to post to all Mattermost channels including direct messages.",
+ "admin.manage_roles.postAllPublicRoleTitle": "post:channels",
+ "admin.manage_roles.postAllPublicRole": "Access to post to all Mattermost public channels.",
+ "admin.manage_roles.allowUserAccessTokens": "Allow this account to generate <a href=\"https://about.mattermost.com/default-user-access-tokens\" target=\"_blank\">user access tokens</a>.",
+ "admin.manage_roles.systemAdmin": "System Admin",
+ "admin.manage_roles.systemMember": "Member",
+ "admin.manage_roles.manageRolesTitle": "Manage Roles",
+ "admin.manage_roles.cancel": "Cancel",
+ "admin.manage_roles.save": "Save",
+ "admin.user_item.userAccessTokenNo": "<strong>User Access Tokens:</strong> No",
+ "admin.user_item.userAccessTokenAdmin": "<strong>User Access Tokens:</strong> Yes (with system_admin)",
+ "admin.user_item.userAccessTokenPostAll": "<strong>User Access Tokens:</strong> Yes (with post:all)",
+ "admin.user_item.userAccessTokenPostAllPublic": "<strong>User Access Tokens:</strong> Yes (with post:channels)",
+ "admin.user_item.userAccessTokenYes": "<strong>User Access Tokens:</strong> Yes",
"admin.user_item.authServiceEmail": "<strong>Sign-in Method:</strong> Email",
"admin.user_item.authServiceNotEmail": "<strong>Sign-in Method:</strong> {service}",
"admin.user_item.confirmDemoteDescription": "If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you'll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command.",
@@ -968,6 +992,8 @@
"admin.user_item.makeSysAdmin": "Make System Admin",
"admin.user_item.makeTeamAdmin": "Make Team Admin",
"admin.user_item.manageTeams": "Manage Teams",
+ "admin.user_item.manageRoles": "Manage Roles",
+ "admin.user_item.manageTokens": "Manage Tokens",
"admin.user_item.member": "Member",
"admin.user_item.mfaNo": "<strong>MFA</strong>: No",
"admin.user_item.mfaYes": "<strong>MFA</strong>: Yes",
@@ -2434,6 +2460,27 @@
"user.settings.push_notification.send": "Send mobile push notifications",
"user.settings.push_notification.status": "Trigger push notifications when",
"user.settings.push_notification.status_info": "Notification alerts are only pushed to your mobile device when your online status matches the selection above.",
+ "user.settings.tokens.confirmCreateTitle": "Create System Admin User Access Token",
+ "user.settings.tokens.confirmCreateMessage": "You are generating a user access token with System Admin permissions. Are you sure want to create this token?",
+ "user.settings.tokens.confirmCreateButton": "Yes, Create",
+ "user.settings.tokens.confirmDeleteTitle": "Delete {name} Token?",
+ "user.settings.tokens.confirmDeleteMessage": "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?",
+ "user.settings.tokens.confirmDeleteButton": "Yes, Delete",
+ "user.settings.tokens.tokenId": "Token ID: ",
+ "user.settings.tokens.delete": "Delete",
+ "user.settings.tokens.userAccessTokensNone": "No user access tokens.",
+ "user.settings.tokens.description": "<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>.",
+ "user.settings.tokens.description_mobile": "<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.",
+ "user.settings.tokens.name": "Name: ",
+ "user.settings.tokens.nameDescription": "Give a name for your token, so you remember what it’s used for. A token is generated after you hit \"Save\".",
+ "user.settings.tokens.save": "Save",
+ "user.settings.tokens.cancel": "Cancel",
+ "user.settings.tokens.id": "ID: ",
+ "user.settings.tokens.token": "Token: ",
+ "user.settings.tokens.copy": "Please copy the token below. You won't be able to see it again!",
+ "user.settings.tokens.create": "Create New Token",
+ "user.settings.tokens.title": "User Access Tokens",
+ "user.settings.tokens.clickToEdit": "Click 'Edit' to manage your user access tokens",
"user.settings.security.active": "Active",
"user.settings.security.close": "Close",
"user.settings.security.currentPassword": "Current Password",
diff --git a/webapp/sass/components/_alerts.scss b/webapp/sass/components/_alerts.scss
index cb4c9c9e1..e0444de39 100644
--- a/webapp/sass/components/_alerts.scss
+++ b/webapp/sass/components/_alerts.scss
@@ -11,3 +11,4 @@
margin: 1px 0 0 10px;
padding: 4px 10px;
}
+
diff --git a/webapp/sass/layout/_content.scss b/webapp/sass/layout/_content.scss
index 933f57c32..7bf6c08ad 100644
--- a/webapp/sass/layout/_content.scss
+++ b/webapp/sass/layout/_content.scss
@@ -87,3 +87,9 @@
.delete-message-text {
margin-top: 10px;
}
+
+.col-sm-auto {
+ padding-left: 15px;
+ padding-right: 15px;
+}
+
diff --git a/webapp/sass/layout/_forms.scss b/webapp/sass/layout/_forms.scss
index 143879e2c..a49acf3e3 100644
--- a/webapp/sass/layout/_forms.scss
+++ b/webapp/sass/layout/_forms.scss
@@ -91,35 +91,3 @@
}
}
}
-
-.padding-top {
- padding-top: 7px;
-
- &.x2 {
- padding-top: 14px;
- }
-
- &.x3 {
- padding-top: 21px;
- }
-}
-
-.padding-bottom {
- padding-bottom: 7px;
-
- &.x2 {
- padding-bottom: 14px;
- }
-
- &.x3 {
- padding-bottom: 21px;
- }
-
- .control-label {
- font-weight: 600;
-
- &.text-left {
- text-align: left;
- }
- }
-}
diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss
index ef4c2e8e5..ef68d5175 100644
--- a/webapp/sass/responsive/_tablet.scss
+++ b/webapp/sass/responsive/_tablet.scss
@@ -212,12 +212,14 @@
}
}
}
+
.post {
.attachment {
.attachment__image {
&.attachment__image--openraph {
max-height: 70px;
max-width: 300px;
+
&.loading {
height: 70px;
}
@@ -229,6 +231,10 @@
// Tablet and desktop
@media screen and (min-width: 768px) {
+ .col-sm-auto {
+ float: left;
+ }
+
.second-bar {
display: none;
}
diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss
index 4fe45d9b8..2d4ca6be1 100644
--- a/webapp/sass/routes/_admin-console.scss
+++ b/webapp/sass/routes/_admin-console.scss
@@ -78,7 +78,7 @@
.log__panel {
background-color: white;
- border: 1px solid #ddd;
+ border: $border-gray;
height: calc(100vh - 200px);
margin-top: 10px;
overflow: scroll;
@@ -180,7 +180,7 @@
.banner {
background: $white;
- border: 1px solid #ddd;
+ border: $border-gray;
font-size: .95em;
margin: 2em 0;
padding: .8em 1.5rem;
@@ -535,11 +535,34 @@
.manage-teams {
.manage-teams__user {
align-items: center;
- border-bottom-color: lightgray;
- border-bottom-style: solid;
- border-bottom-width: 1px;
display: flex;
- padding-bottom: 15px;
+ }
+
+ .manage-teams__teams {
+ border-top: $border-gray;
+ margin: 1em 0 .3em;
+
+ .btn-link {
+ &.danger {
+ color: #c55151;
+ }
+ }
+ }
+
+ .member-row--padded {
+ padding-left: 20px;
+
+ strong {
+ margin-right: 10px;
+ }
+ }
+
+ .manage-row--inner {
+ padding: 15px 0 4px;
+
+ & + div {
+ border-top: $border-gray;
+ }
}
.manage-teams__profile-picture {
@@ -573,21 +596,31 @@
padding-right: 10px;
}
+ .manage-teams__teams {
+ margin-top: 1em;
+
+ .manage-row__empty {
+ padding: 9px 0;
+ }
+ }
+
.manage-teams__team {
align-items: center;
+ border-bottom: $border-gray;
display: flex;
- padding: 10px;
- }
+ padding: 7px 10px;
- .manage-teams__team + .manage-teams__team {
- border-top-color: lightgray;
- border-top-style: solid;
- border-top-width: 1px;
+ .btn {
+ font-size: .9em;
+ }
+
+ .dropdown {
+ padding: 6px 0;
+ }
}
.manage-teams__team-name {
flex: 1;
- font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}
diff --git a/webapp/sass/routes/_settings.scss b/webapp/sass/routes/_settings.scss
index 3c5565194..f33417200 100644
--- a/webapp/sass/routes/_settings.scss
+++ b/webapp/sass/routes/_settings.scss
@@ -360,6 +360,22 @@
padding: 0;
}
+ .setting-box__item {
+ &:first-child {
+ padding-top: 3px;
+ }
+
+ &:last-child {
+ hr {
+ display: none;
+ }
+ }
+ }
+
+ .setting-box__token-id {
+ margin: 4px 0;
+ }
+
.setting-list__hint {
margin-top: 20px;
}
diff --git a/webapp/sass/utils/_modifiers.scss b/webapp/sass/utils/_modifiers.scss
index aa89fc107..467b9a086 100644
--- a/webapp/sass/utils/_modifiers.scss
+++ b/webapp/sass/utils/_modifiers.scss
@@ -1,33 +1,105 @@
@charset 'UTF-8';
-.margin--right {
- margin-right: 5px;
+.padding-top {
+ padding-top: 7px;
&.x2 {
- margin-right: 10px;
+ padding-top: 14px;
+ }
+
+ &.x3 {
+ padding-top: 21px;
+ }
+}
+
+.padding-bottom {
+ padding-bottom: 7px;
+
+ &.x2 {
+ padding-bottom: 14px;
+ }
+
+ &.x3 {
+ padding-bottom: 21px;
+ }
+
+ .control-label {
+ font-weight: 600;
+
+ &.text-left {
+ text-align: left;
+ }
}
}
-.margin--left {
- margin-left: 5px;
+.padding-left {
+ padding-left: 7px;
&.x2 {
- margin-left: 10px;
+ padding-left: 14px;
+ }
+
+ &.x3 {
+ padding-left: 21px;
}
}
-.padding--right {
- padding-right: 5px;
+.padding-right {
+ padding-right: 7px;
&.x2 {
- padding-right: 10px;
+ padding-right: 14px;
+ }
+
+ &.x3 {
+ padding-right: 21px;
}
}
-.padding--left {
- padding-left: 5px;
+.margin-right {
+ margin-right: 7px;
&.x2 {
- padding-left: 10px;
+ margin-right: 14px;
+ }
+
+ &.x3 {
+ margin-right: 21px;
+ }
+}
+
+.margin-left {
+ margin-left: 7px;
+
+ &.x2 {
+ margin-left: 14px;
+ }
+
+ &.x3 {
+ margin-left: 21px;
+ }
+}
+
+.margin-top {
+ margin-top: 7px;
+
+ &.x2 {
+ margin-top: 14px;
+ }
+
+ &.x3 {
+ margin-top: 21px;
+ }
+}
+
+.margin-bottom {
+ margin-bottom: 7px;
+
+ &.x2 {
+ margin-bottom: 14px;
+ }
+
+ &.x3 {
+ margin-bottom: 21px;
}
}
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 658ccd74b..94a2cf286 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -579,7 +579,7 @@ export function applyTheme(theme) {
if (theme.centerChannelBg) {
changeCss('@media(min-width: 768px){.app__body .post:hover .post__header .col__reply, .app__body .post.post--hovered .post__header .col__reply', 'background:' + theme.centerChannelBg);
changeCss('@media(max-width: 320px){.tutorial-steps__container', 'background:' + theme.centerChannelBg);
- changeCss('.app__body .status-wrapper .status_dropdown__toggle .status .icon__container:after, .app__body .app__content, .app__body .markdown__table, .app__body .markdown__table tbody tr, .app__body .suggestion-list__content, .app__body .modal .modal-content, .app__body .modal .modal-footer, .app__body .post.post--compact .post-image__column, .app__body .suggestion-list__divider > span, .app__body .status-wrapper .status', 'background:' + theme.centerChannelBg);
+ changeCss('.app__body .status-wrapper .status_dropdown__toggle .status .icon__container:after, .app__body .app__content, .app__body .markdown__table, .app__body .markdown__table tbody tr, .app__body .suggestion-list__content, .app__body .modal .modal-content, .app__body .modal .modal-footer, .app__body .post.post--compact .post-image__column, .app__body .suggestion-list__divider > span, .app__body .status-wrapper .status, .app__body .alert.alert-transparent', 'background:' + theme.centerChannelBg);
changeCss('#post-list .post-list-holder-by-time, .app__body .post .dropdown-menu a', 'background:' + theme.centerChannelBg);
changeCss('#post-create', 'background:' + theme.centerChannelBg);
changeCss('.app__body .date-separator .separator__text, .app__body .new-separator .separator__text', 'background:' + theme.centerChannelBg);
@@ -601,7 +601,7 @@ export function applyTheme(theme) {
if (theme.centerChannelColor) {
changeCss('.app__body .mentions__name .status.status--group, .app__body .multi-select__note', 'background:' + changeOpacity(theme.centerChannelColor, 0.12));
- changeCss('.app__body .channel-header .channel-header__icon, .app__body .search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.12));
+ changeCss('.app__body .alert.alert-transparent, .app__body .channel-header .channel-header__icon, .app__body .search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.12));
changeCss('.app__body .post-list__arrows, .app__body .post .flag-icon__container', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3));
changeCss('@media(min-width: 768px){.app__body .search__icon svg', 'stroke:' + changeOpacity(theme.centerChannelColor, 0.4));
changeCss('.app__body .channel-header__icon svg', 'fill:' + changeOpacity(theme.centerChannelColor, 0.4));
diff --git a/webapp/yarn.lock b/webapp/yarn.lock
index 50d83248b..13d9c630c 100644
--- a/webapp/yarn.lock
+++ b/webapp/yarn.lock
@@ -5004,7 +5004,7 @@ math-expression-evaluator@^1.2.14:
mattermost-redux@mattermost/mattermost-redux#master:
version "0.0.1"
- resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/9797cb8bd8fa61252336a7c6150bd364f7ca28b1"
+ resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/d3a8c94d59a687a957ca8808fbe1b9cb76077bce"
dependencies:
deep-equal "1.0.1"
harmony-reflect "1.5.1"