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
This commit is contained in:
Joram Wilander 2017-08-01 11:06:53 -04:00 committed by GitHub
parent 4ef844298f
commit 5da5c0bbfb
33 changed files with 1690 additions and 252 deletions

View file

@ -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()

View file

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

View file

@ -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,

View file

@ -3503,6 +3503,14 @@
"id": "app.import.validate_user_teams_import_data.team_name_missing.error",
"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"
@ -3511,6 +3519,14 @@
"id": "authentication.roles.system_post_all_public.description",
"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"

View file

@ -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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 (

View file

@ -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,28 +58,7 @@ export default class SystemUsersDropdown extends React.Component {
};
}
doMakeMember() {
updateUserRoles(
this.props.user.id,
'system_user',
null,
(err) => {
this.setState({serverError: err.message});
}
);
}
handleMakeMember(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) {
handleMakeActive = (e) => {
e.preventDefault();
updateActive(this.props.user.id, true, null,
(err) => {
@ -80,31 +67,30 @@ export default class SystemUsersDropdown extends React.Component {
);
}
handleMakeSystemAdmin(e) {
e.preventDefault();
updateUserRoles(
this.props.user.id,
'system_user system_admin',
null,
(err) => {
this.setState({serverError: err.message});
}
);
}
handleManageTeams(e) {
handleManageTeams = (e) => {
e.preventDefault();
this.props.doManageTeams(this.props.user);
}
handleResetPassword(e) {
handleManageRoles = (e) => {
e.preventDefault();
this.props.doManageRoles(this.props.user);
}
handleManageTokens = (e) => {
e.preventDefault();
this.props.doManageTokens(this.props.user);
}
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>

View file

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

View file

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

View file

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

View file

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

View file

@ -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 its 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",

View file

@ -11,3 +11,4 @@
margin: 1px 0 0 10px;
padding: 4px 10px;
}

View file

@ -87,3 +87,9 @@
.delete-message-text {
margin-top: 10px;
}
.col-sm-auto {
padding-left: 15px;
padding-right: 15px;
}

View file

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

View file

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

View file

@ -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__team {
align-items: center;
display: flex;
padding: 10px;
.manage-teams__teams {
margin-top: 1em;
.manage-row__empty {
padding: 9px 0;
}
}
.manage-teams__team + .manage-teams__team {
border-top-color: lightgray;
border-top-style: solid;
border-top-width: 1px;
.manage-teams__team {
align-items: center;
border-bottom: $border-gray;
display: flex;
padding: 7px 10px;
.btn {
font-size: .9em;
}
.dropdown {
padding: 6px 0;
}
}
.manage-teams__team-name {
flex: 1;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}

View file

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

View file

@ -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;
}
}
.margin--left {
margin-left: 5px;
.padding-bottom {
padding-bottom: 7px;
&.x2 {
margin-left: 10px;
padding-bottom: 14px;
}
&.x3 {
padding-bottom: 21px;
}
.control-label {
font-weight: 600;
&.text-left {
text-align: left;
}
}
}
.padding--right {
padding-right: 5px;
.padding-left {
padding-left: 7px;
&.x2 {
padding-right: 10px;
padding-left: 14px;
}
&.x3 {
padding-left: 21px;
}
}
.padding--left {
padding-left: 5px;
.padding-right {
padding-right: 7px;
&.x2 {
padding-left: 10px;
padding-right: 14px;
}
&.x3 {
padding-right: 21px;
}
}
.margin-right {
margin-right: 7px;
&.x2 {
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;
}
}

View file

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

View file

@ -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"