summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
authorJoramWilander <jwawilander@gmail.com>2016-03-30 12:49:29 -0400
committerJoramWilander <jwawilander@gmail.com>2016-03-30 12:49:29 -0400
commitf9a3a4b3949dddecae413b97904c895b2cd887bf (patch)
treebb77628b0aba959feeab28a5a10fe0bc0e6b4ecc /webapp/components
parent2aa0d9b8fc2e31e51bbddc8d90fe801c089f7c4b (diff)
downloadchat-f9a3a4b3949dddecae413b97904c895b2cd887bf.tar.gz
chat-f9a3a4b3949dddecae413b97904c895b2cd887bf.tar.bz2
chat-f9a3a4b3949dddecae413b97904c895b2cd887bf.zip
Add MFA functionality
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/admin_console/service_settings.jsx55
-rw-r--r--webapp/components/login/components/login_email.jsx (renamed from webapp/components/login_email.jsx)74
-rw-r--r--webapp/components/login/components/login_ldap.jsx (renamed from webapp/components/login_ldap.jsx)76
-rw-r--r--webapp/components/login/components/login_mfa.jsx92
-rw-r--r--webapp/components/login/components/login_username.jsx (renamed from webapp/components/login_username.jsx)91
-rw-r--r--webapp/components/login/login.jsx (renamed from webapp/components/login.jsx)364
-rw-r--r--webapp/components/signup_user_complete.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx223
8 files changed, 649 insertions, 328 deletions
diff --git a/webapp/components/admin_console/service_settings.jsx b/webapp/components/admin_console/service_settings.jsx
index 881d22d76..41ea5ea34 100644
--- a/webapp/components/admin_console/service_settings.jsx
+++ b/webapp/components/admin_console/service_settings.jsx
@@ -84,6 +84,7 @@ class ServiceSettings extends React.Component {
config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked;
config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
config.ServiceSettings.EnableInsecureOutgoingConnections = ReactDOM.findDOMNode(this.refs.EnableInsecureOutgoingConnections).checked;
+ config.ServiceSettings.EnableMultifactorAuthentication = ReactDOM.findDOMNode(this.refs.EnableMultifactorAuthentication).checked;
config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked;
config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked;
@@ -173,6 +174,58 @@ class ServiceSettings extends React.Component {
saveClass = 'btn btn-primary';
}
+ let mfaSetting;
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
+ mfaSetting = (
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableMultifactorAuthentication'
+ >
+ <FormattedMessage
+ id='admin.service.mfaTitle'
+ defaultMessage='Enable Multi-factor Authentication:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableMultifactorAuthentication'
+ value='true'
+ ref='EnableMultifactorAuthentication'
+ defaultChecked={this.props.config.ServiceSettings.EnableMultifactorAuthentication}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableMultifactorAuthentication'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableMultifactorAuthentication}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.service.mfaDesc'
+ defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.'
+ />
+ </p>
+ </div>
+ </div>
+ );
+ }
+
return (
<div className='wrapper--fixed'>
@@ -773,6 +826,8 @@ class ServiceSettings extends React.Component {
</div>
</div>
+ {mfaSetting}
+
<div className='form-group'>
<label
className='control-label col-sm-4'
diff --git a/webapp/components/login_email.jsx b/webapp/components/login/components/login_email.jsx
index d54c32ff9..b1f484c08 100644
--- a/webapp/components/login_email.jsx
+++ b/webapp/components/login/components/login_email.jsx
@@ -2,69 +2,40 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import * as Client from 'utils/client.jsx';
import UserStore from 'stores/user_store.jsx';
-import {browserHistory} from 'react-router';
+import Constants from 'utils/constants.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-
-var holders = defineMessages({
- badTeam: {
- id: 'login_email.badTeam',
- defaultMessage: 'Bad team name'
- },
- emailReq: {
- id: 'login_email.emailReq',
- defaultMessage: 'An email is required'
- },
- pwdReq: {
- id: 'login_email.pwdReq',
- defaultMessage: 'A password is required'
- },
- email: {
- id: 'login_email.email',
- defaultMessage: 'Email'
- },
- pwd: {
- id: 'login_email.pwd',
- defaultMessage: 'Password'
- }
-});
+import {FormattedMessage} from 'react-intl';
import React from 'react';
-class LoginEmail extends React.Component {
+export default class LoginEmail extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
- serverError: ''
+ serverError: props.serverError
};
}
+ componentWillReceiveProps(nextProps) {
+ this.setState({serverError: nextProps.serverError});
+ }
handleSubmit(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
var state = {};
- const name = this.props.teamName;
- if (!name) {
- state.serverError = formatMessage(holders.badTeam);
- this.setState(state);
- return;
- }
-
const email = this.refs.email.value.trim();
if (!email) {
- state.serverError = formatMessage(holders.emailReq);
+ state.serverError = Utils.localizeMessage('login_email.emailReq', 'An email is required');
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
- state.serverError = formatMessage(holders.pwdReq);
+ state.serverError = Utils.localizeMessage('login_email.pwdReq', 'A password is required');
this.setState(state);
return;
}
@@ -72,21 +43,7 @@ class LoginEmail extends React.Component {
state.serverError = '';
this.setState(state);
- Client.loginByEmail(name, email, password,
- () => {
- UserStore.setLastEmail(email);
- browserHistory.push('/' + name + '/channels/town-square');
- },
- (err) => {
- if (err.id === 'api.user.login.not_verified.app_error') {
- browserHistory.push('/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email));
- return;
- }
- state.serverError = err.message;
- this.valid = false;
- this.setState(state);
- }
- );
+ this.props.submit(Constants.EMAIL_SERVICE, email, password);
}
render() {
let serverError;
@@ -110,7 +67,6 @@ class LoginEmail extends React.Component {
priorEmail = decodeURIComponent(emailParam);
}
- const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
@@ -125,7 +81,7 @@ class LoginEmail extends React.Component {
name='email'
defaultValue={priorEmail}
ref='email'
- placeholder={formatMessage(holders.email)}
+ placeholder={Utils.localizeMessage('login_email.email', 'Email')}
spellCheck='false'
/>
</div>
@@ -136,7 +92,7 @@ class LoginEmail extends React.Component {
className='form-control'
name='password'
ref='password'
- placeholder={formatMessage(holders.pwd)}
+ placeholder={Utils.localizeMessage('login_email.pwd', 'Password')}
spellCheck='false'
/>
</div>
@@ -160,8 +116,6 @@ LoginEmail.defaultProps = {
};
LoginEmail.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string.isRequired
+ submit: React.PropTypes.func.isRequired,
+ serverError: React.PropTypes.string
};
-
-export default injectIntl(LoginEmail);
diff --git a/webapp/components/login_ldap.jsx b/webapp/components/login/components/login_ldap.jsx
index 59ff973dc..a2013710f 100644
--- a/webapp/components/login_ldap.jsx
+++ b/webapp/components/login/components/login_ldap.jsx
@@ -2,68 +2,39 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import * as Client from 'utils/client.jsx';
+import Constants from 'utils/constants.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router';
-
-const holders = defineMessages({
- badTeam: {
- id: 'login_ldap.badTeam',
- defaultMessage: 'Bad team name'
- },
- idReq: {
- id: 'login_ldap.idlReq',
- defaultMessage: 'An LDAP ID is required'
- },
- pwdReq: {
- id: 'login_ldap.pwdReq',
- defaultMessage: 'An LDAP password is required'
- },
- username: {
- id: 'login_ldap.username',
- defaultMessage: 'LDAP Username'
- },
- pwd: {
- id: 'login_ldap.pwd',
- defaultMessage: 'LDAP Password'
- }
-});
+import {FormattedMessage} from 'react-intl';
import React from 'react';
-class LoginLdap extends React.Component {
+export default class LoginLdap extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
- serverError: ''
+ serverError: props.serverError
};
}
+ componentWillReceiveProps(nextProps) {
+ this.setState({serverError: nextProps.serverError});
+ }
handleSubmit(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
- var state = {};
-
- const teamName = this.props.teamName;
- if (!teamName) {
- state.serverError = formatMessage(holders.badTeam);
- this.setState(state);
- return;
- }
+ const state = {};
const id = this.refs.id.value.trim();
if (!id) {
- state.serverError = formatMessage(holders.idReq);
+ state.serverError = Utils.localizeMessage('login_ldap.idlReq', 'An LDAP ID is required');
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
- state.serverError = formatMessage(holders.pwdReq);
+ state.serverError = Utils.localizeMessage('login_ldap.pwdReq', 'An LDAP password is required');
this.setState(state);
return;
}
@@ -71,20 +42,7 @@ class LoginLdap extends React.Component {
state.serverError = '';
this.setState(state);
- Client.loginByLdap(teamName, id, password,
- () => {
- const redirect = Utils.getUrlParameter('redirect');
- if (redirect) {
- browserHistory.push(decodeURIComponent(redirect));
- } else {
- browserHistory.push('/' + teamName + '/channels/town-square');
- }
- },
- (err) => {
- state.serverError = err.message;
- this.setState(state);
- }
- );
+ this.props.submit(Constants.LDAP_SERVICE, id, password);
}
render() {
let serverError;
@@ -93,7 +51,7 @@ class LoginLdap extends React.Component {
serverError = <label className='control-label'>{this.state.serverError}</label>;
errorClass = ' has-error';
}
- const {formatMessage} = this.props.intl;
+
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
@@ -105,7 +63,7 @@ class LoginLdap extends React.Component {
autoFocus={true}
className='form-control'
ref='id'
- placeholder={formatMessage(holders.username)}
+ placeholder={Utils.localizeMessage('login_ldap.username', 'LDAP Username')}
spellCheck='false'
/>
</div>
@@ -114,7 +72,7 @@ class LoginLdap extends React.Component {
type='password'
className='form-control'
ref='password'
- placeholder={formatMessage(holders.pwd)}
+ placeholder={Utils.localizeMessage('login_ldap.pwd', 'LDAP Password')}
spellCheck='false'
/>
</div>
@@ -138,8 +96,6 @@ LoginLdap.defaultProps = {
};
LoginLdap.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string.isRequired
+ serverError: React.PropTypes.string,
+ submit: React.PropTypes.func.isRequired
};
-
-export default injectIntl(LoginLdap);
diff --git a/webapp/components/login/components/login_mfa.jsx b/webapp/components/login/components/login_mfa.jsx
new file mode 100644
index 000000000..f8ebf1e82
--- /dev/null
+++ b/webapp/components/login/components/login_mfa.jsx
@@ -0,0 +1,92 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class LoginMfa extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ serverError: ''
+ };
+ }
+ handleSubmit(e) {
+ e.preventDefault();
+ const state = {};
+
+ const token = this.refs.token.value.trim();
+ if (!token) {
+ state.serverError = Utils.localizeMessage('login_mfa.tokenReq', 'Please enter an MFA token');
+ this.setState(state);
+ return;
+ }
+
+ state.serverError = '';
+ this.setState(state);
+
+ this.props.submit(this.props.method, this.props.loginId, this.props.password, token);
+ }
+ render() {
+ let serverError;
+ let errorClass = '';
+ if (this.state.serverError) {
+ serverError = <label className='control-label'>{this.state.serverError}</label>;
+ errorClass = ' has-error';
+ }
+
+ return (
+ <form onSubmit={this.handleSubmit}>
+ <div className='signup__email-container'>
+ <p>
+ <FormattedMessage
+ id='login_mfa.enterToken'
+ defaultMessage="To complete the sign in process, please enter a token from your smartphone's authenticator"
+ />
+ </p>
+ <div className={'form-group' + errorClass}>
+ {serverError}
+ </div>
+ <div className={'form-group' + errorClass}>
+ <input
+ type='text'
+ className='form-control'
+ name='token'
+ ref='token'
+ placeholder={Utils.localizeMessage('login_mfa.token', 'MFA Token')}
+ spellCheck='false'
+ autoComplete='off'
+ autoFocus={true}
+ />
+ </div>
+ <div className='form-group'>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ >
+ <FormattedMessage
+ id='login_mfa.submit'
+ defaultMessage='Submit'
+ />
+ </button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+}
+LoginMfa.defaultProps = {
+};
+
+LoginMfa.propTypes = {
+ method: React.PropTypes.string.isRequired,
+ loginId: React.PropTypes.string.isRequired,
+ password: React.PropTypes.string.isRequired,
+ submit: React.PropTypes.func.isRequired
+};
diff --git a/webapp/components/login_username.jsx b/webapp/components/login/components/login_username.jsx
index 71874fa1a..3cb213994 100644
--- a/webapp/components/login_username.jsx
+++ b/webapp/components/login/components/login_username.jsx
@@ -2,42 +2,10 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import * as Client from 'utils/client.jsx';
import UserStore from 'stores/user_store.jsx';
+import Constants from 'utils/constants.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router';
-
-var holders = defineMessages({
- badTeam: {
- id: 'login_username.badTeam',
- defaultMessage: 'Bad team name'
- },
- usernameReq: {
- id: 'login_username.usernameReq',
- defaultMessage: 'A username is required'
- },
- pwdReq: {
- id: 'login_username.pwdReq',
- defaultMessage: 'A password is required'
- },
- verifyEmailError: {
- id: 'login_username.verifyEmailError',
- defaultMessage: 'Please verify your email address. Check your inbox for an email.'
- },
- userNotFoundError: {
- id: 'login_username.userNotFoundError',
- defaultMessage: "We couldn't find an existing account matching your username for this team."
- },
- username: {
- id: 'login_username.username',
- defaultMessage: 'Username'
- },
- pwd: {
- id: 'login_username.pwd',
- defaultMessage: 'Password'
- }
-});
+import {FormattedMessage} from 'react-intl';
import React from 'react';
@@ -48,31 +16,26 @@ export default class LoginUsername extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
- serverError: ''
+ serverError: props.serverError
};
}
+ componentWillReceiveProps(nextProps) {
+ this.setState({serverError: nextProps.serverError});
+ }
handleSubmit(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
- var state = {};
-
- const name = this.props.teamName;
- if (!name) {
- state.serverError = formatMessage(holders.badTeam);
- this.setState(state);
- return;
- }
+ const state = {};
const username = this.refs.username.value.trim();
if (!username) {
- state.serverError = formatMessage(holders.usernameReq);
+ state.serverError = Utils.localizeMessage('login_username.usernameReq', 'A username is required');
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
- state.serverError = formatMessage(holders.pwdReq);
+ state.serverError = Utils.localizeMessage('login_username.pwdReq', 'A password is required');
this.setState(state);
return;
}
@@ -80,30 +43,7 @@ export default class LoginUsername extends React.Component {
state.serverError = '';
this.setState(state);
- Client.loginByUsername(name, username, password,
- () => {
- UserStore.setLastUsername(username);
-
- const redirect = Utils.getUrlParameter('redirect');
- if (redirect) {
- browserHistory.push(decodeURIComponent(redirect));
- } else {
- browserHistory.push('/' + name + '/channels/town-square');
- }
- },
- (err) => {
- if (err.id === 'api.user.login.not_verified.app_error') {
- state.serverError = formatMessage(holders.verifyEmailError);
- } else if (err.id === 'store.sql_user.get_by_username.app_error') {
- state.serverError = formatMessage(holders.userNotFoundError);
- } else {
- state.serverError = err.message;
- }
-
- this.valid = false;
- this.setState(state);
- }
- );
+ this.props.submit(Constants.USERNAME_SERVICE, username, password);
}
render() {
let serverError;
@@ -127,7 +67,6 @@ export default class LoginUsername extends React.Component {
priorUsername = decodeURIComponent(emailParam);
}
- const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
@@ -142,7 +81,7 @@ export default class LoginUsername extends React.Component {
name='username'
defaultValue={priorUsername}
ref='username'
- placeholder={formatMessage(holders.username)}
+ placeholder={Utils.localizeMessage('login_username.username', 'Username')}
spellCheck='false'
/>
</div>
@@ -153,7 +92,7 @@ export default class LoginUsername extends React.Component {
className='form-control'
name='password'
ref='password'
- placeholder={formatMessage(holders.pwd)}
+ placeholder={Utils.localizeMessage('login_username.pwd', 'Password')}
spellCheck='false'
/>
</div>
@@ -177,8 +116,6 @@ LoginUsername.defaultProps = {
};
LoginUsername.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string.isRequired
+ serverError: React.PropTypes.string,
+ submit: React.PropTypes.func.isRequired
};
-
-export default injectIntl(LoginUsername);
diff --git a/webapp/components/login.jsx b/webapp/components/login/login.jsx
index ff9cd74a8..7b370a939 100644
--- a/webapp/components/login.jsx
+++ b/webapp/components/login/login.jsx
@@ -1,14 +1,17 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import LoginEmail from './login_email.jsx';
-import LoginUsername from './login_username.jsx';
-import LoginLdap from './login_ldap.jsx';
+import LoginEmail from './components/login_email.jsx';
+import LoginUsername from './components/login_username.jsx';
+import LoginLdap from './components/login_ldap.jsx';
+import LoginMfa from './components/login_mfa.jsx';
+
+import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
-import * as Utils from 'utils/utils.jsx';
import * as Client from 'utils/client.jsx';
+import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import TeamStore from 'stores/team_store.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router';
@@ -21,6 +24,8 @@ export default class Login extends React.Component {
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
+ this.preSubmit = this.preSubmit.bind(this);
+ this.submit = this.submit.bind(this);
this.state = this.getStateFromStores();
}
@@ -46,54 +51,89 @@ export default class Login extends React.Component {
onTeamChange() {
this.setState(this.getStateFromStores());
}
- render() {
- const currentTeam = this.state.currentTeam;
- if (currentTeam == null || !this.state.doneCheckLogin) {
- return <div/>;
+ preSubmit(method, loginId, password) {
+ if (global.window.mm_config.EnableMultifactorAuthentication !== 'true') {
+ this.submit(method, loginId, password, '');
+ return;
}
- const teamDisplayName = currentTeam.display_name;
- const teamName = currentTeam.name;
- const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
- const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
+ Client.checkMfa(method, this.state.currentTeam.name, loginId,
+ (data) => {
+ if (data.mfa_required === 'true') {
+ this.setState({showMfa: true, method, loginId, password});
+ } else {
+ this.submit(method, loginId, password, '');
+ }
+ },
+ (err) => {
+ if (method === Constants.EMAIL_SERVICE) {
+ this.setState({serverEmailError: err.message});
+ } else if (method === Constants.USERNAME_SERVICE) {
+ this.setState({serverUsernameError: err.message});
+ } else if (method === Constants.LDAP_SERVICE) {
+ this.setState({serverLdapError: err.message});
+ }
+ }
+ );
+ }
+ submit(method, loginId, password, token) {
+ this.setState({showMfa: false, serverEmailError: null, serverUsernameError: null, serverLdapError: null});
- let loginMessage = [];
- if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
- loginMessage.push(
- <a
- className='btn btn-custom-login gitlab'
- key='gitlab'
- href={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
- >
- <span className='icon'/>
- <span>
- <FormattedMessage
- id='login.gitlab'
- defaultMessage='with GitLab'
- />
- </span>
- </a>
+ const team = this.state.currentTeam.name;
+
+ if (method === Constants.EMAIL_SERVICE) {
+ Client.loginByEmail(team, loginId, password, token,
+ () => {
+ UserStore.setLastEmail(loginId);
+ browserHistory.push('/' + team + '/channels/town-square');
+ },
+ (err) => {
+ if (err.id === 'api.user.login.not_verified.app_error') {
+ browserHistory.push('/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(loginId));
+ return;
+ }
+ this.setState({serverEmailError: err.message});
+ }
);
- }
+ } else if (method === Constants.USERNAME_SERVICE) {
+ Client.loginByUsername(team, loginId, password, token,
+ () => {
+ UserStore.setLastUsername(loginId);
- if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
- loginMessage.push(
- <a
- className='btn btn-custom-login google'
- key='google'
- href={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
- >
- <span className='icon'/>
- <span>
- <FormattedMessage
- id='login.google'
- defaultMessage='with Google Apps'
- />
- </span>
- </a>
+ const redirect = Utils.getUrlParameter('redirect');
+ if (redirect) {
+ browserHistory.push(decodeURIComponent(redirect));
+ } else {
+ browserHistory.push('/' + team + '/channels/town-square');
+ }
+ },
+ (err) => {
+ if (err.id === 'api.user.login.not_verified.app_error') {
+ this.setState({serverUsernameError: Utils.localizeMessage('login_username.verifyEmailError', 'Please verify your email address. Check your inbox for an email.')});
+ } else if (err.id === 'store.sql_user.get_by_username.app_error') {
+ this.setState({serverUsernameError: Utils.localizeMessage('login_username.userNotFoundError', 'We couldn\'t find an existing account matching your username for this team.')});
+ } else {
+ this.setState({serverUsernameError: err.message});
+ }
+ }
+ );
+ } else if (method === Constants.LDAP_SERVICE) {
+ Client.loginByLdap(team, loginId, password, token,
+ () => {
+ const redirect = Utils.getUrlParameter('redirect');
+ if (redirect) {
+ browserHistory.push(decodeURIComponent(redirect));
+ } else {
+ browserHistory.push('/' + team + '/channels/town-square');
+ }
+ },
+ (err) => {
+ this.setState({serverLdapError: err.message});
+ }
);
}
-
+ }
+ createLoginOptions(currentTeam) {
const extraParam = Utils.getUrlParameter('extra');
let extraBox = '';
if (extraParam) {
@@ -130,44 +170,126 @@ export default class Login extends React.Component {
}
}
- let emailSignup;
- if (global.window.mm_config.EnableSignInWithEmail === 'true') {
- emailSignup = (
- <LoginEmail
- teamName={teamName}
- />
+ const teamName = currentTeam.name;
+ const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
+ const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true';
+ const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true';
+ const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
+ const emailSigninEnabled = global.window.mm_config.EnableSignInWithEmail === 'true';
+
+ const oauthLogins = [];
+ if (gitlabSigninEnabled) {
+ oauthLogins.push(
+ <Link
+ className='btn btn-custom-login gitlab'
+ key='gitlab'
+ to={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
+ >
+ <span className='icon'/>
+ <span>
+ <FormattedMessage
+ id='login.gitlab'
+ defaultMessage='with GitLab'
+ />
+ </span>
+ </Link>
);
}
- if (loginMessage.length > 0 && emailSignup) {
- loginMessage = (
- <div>
- {loginMessage}
- <div className='or__container'>
+ if (googleSigninEnabled) {
+ oauthLogins.push(
+ <Link
+ className='btn btn-custom-login google'
+ key='google'
+ to={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
+ >
+ <span className='icon'/>
+ <span>
<FormattedMessage
- id='login.or'
- defaultMessage='or'
+ id='login.google'
+ defaultMessage='with Google Apps'
/>
+ </span>
+ </Link>
+ );
+ }
+
+ let emailLogin;
+ if (emailSigninEnabled) {
+ emailLogin = (
+ <LoginEmail
+ teamName={teamName}
+ serverError={this.state.serverEmailError}
+ submit={this.preSubmit}
+ />
+ );
+
+ if (oauthLogins.length > 0) {
+ emailLogin = (
+ <div>
+ <div className='or__container'>
+ <FormattedMessage
+ id='login.or'
+ defaultMessage='or'
+ />
+ </div>
+ {emailLogin}
</div>
- </div>
+ );
+ }
+ }
+
+ let usernameLogin;
+ if (usernameSigninEnabled) {
+ usernameLogin = (
+ <LoginUsername
+ teamName={teamName}
+ serverError={this.state.serverUsernameError}
+ submit={this.preSubmit}
+ />
);
+
+ if (emailSigninEnabled || oauthLogins.length > 0) {
+ usernameLogin = (
+ <div>
+ <div className='or__container'>
+ <FormattedMessage
+ id='login.or'
+ defaultMessage='or'
+ />
+ </div>
+ {usernameLogin}
+ </div>
+ );
+ }
}
- let forgotPassword;
- if (emailSignup) {
- forgotPassword = (
- <div className='form-group'>
- <Link to={'/' + teamName + '/reset_password'}>
- <FormattedMessage
- id='login.forgot'
- defaultMessage='I forgot my password'
- />
- </Link>
- </div>
+ let ldapLogin;
+ if (ldapEnabled) {
+ ldapLogin = (
+ <LoginLdap
+ teamName={teamName}
+ serverError={this.state.serverLdapError}
+ submit={this.preSubmit}
+ />
);
+
+ if (emailSigninEnabled || usernameSigninEnabled || oauthLogins.length > 0) {
+ ldapLogin = (
+ <div>
+ <div className='or__container'>
+ <FormattedMessage
+ id='login.or'
+ defaultMessage='or'
+ />
+ </div>
+ {ldapLogin}
+ </div>
+ );
+ }
}
- let userSignUp = null;
+ let userSignUp;
if (currentTeam.allow_open_invite) {
userSignUp = (
<div>
@@ -190,7 +312,21 @@ export default class Login extends React.Component {
);
}
- let teamSignUp = null;
+ let forgotPassword;
+ if (usernameSigninEnabled || emailSigninEnabled) {
+ forgotPassword = (
+ <div className='form-group'>
+ <Link to={'/' + teamName + '/reset_password'}>
+ <FormattedMessage
+ id='login.forgot'
+ defaultMessage='I forgot my password'
+ />
+ </Link>
+ </div>
+ );
+ }
+
+ let teamSignUp;
if (global.window.mm_config.EnableTeamCreation === 'true' && !Utils.isMobileApp()) {
teamSignUp = (
<div className='margin--extra'>
@@ -207,54 +343,37 @@ export default class Login extends React.Component {
);
}
- let ldapLogin = null;
- if (global.window.mm_config.EnableLdap === 'true') {
- ldapLogin = (
- <LoginLdap
- teamName={teamName}
- />
- );
- }
-
- if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) {
- ldapLogin = (
- <div>
- <div className='or__container'>
- <FormattedMessage
- id='login.or'
- defaultMessage='or'
- />
- </div>
- <LoginLdap
- teamName={teamName}
- />
- </div>
- );
+ return (
+ <div>
+ {extraBox}
+ {oauthLogins}
+ {emailLogin}
+ {usernameLogin}
+ {ldapLogin}
+ {userSignUp}
+ {forgotPassword}
+ {teamSignUp}
+ </div>
+ );
+ }
+ render() {
+ const currentTeam = this.state.currentTeam;
+ if (currentTeam == null || !this.state.doneCheckLogin) {
+ return <div/>;
}
- let usernameLogin = null;
- if (global.window.mm_config.EnableSignInWithUsername === 'true') {
- usernameLogin = (
- <LoginUsername
- teamName={teamName}
+ let content;
+ if (this.state.showMfa) {
+ content = (
+ <LoginMfa
+ method={this.state.method}
+ loginId={this.state.loginId}
+ password={this.state.password}
+ submit={this.submit}
/>
);
- }
-
- if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) {
- usernameLogin = (
- <div>
- <div className='or__container'>
- <FormattedMessage
- id='login.or'
- defaultMessage='or'
- />
- </div>
- <LoginUsername
- teamName={teamName}
- />
- </div>
- );
+ } else {
+ content = this.createLoginOptions(currentTeam);
}
return (
@@ -275,7 +394,7 @@ export default class Login extends React.Component {
defaultMessage='Sign in to:'
/>
</h5>
- <h2 className='signup-team__name'>{teamDisplayName}</h2>
+ <h2 className='signup-team__name'>{currentTeam.display_name}</h2>
<h2 className='signup-team__subdomain'>
<FormattedMessage
id='login.on'
@@ -285,14 +404,7 @@ export default class Login extends React.Component {
}}
/>
</h2>
- {extraBox}
- {loginMessage}
- {emailSignup}
- {usernameLogin}
- {ldapLogin}
- {userSignUp}
- {forgotPassword}
- {teamSignUp}
+ {content}
</div>
</div>
</div>
diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx
index 549ba8439..e9f9d9d88 100644
--- a/webapp/components/signup_user_complete.jsx
+++ b/webapp/components/signup_user_complete.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
import LoadingScreen from 'components/loading_screen.jsx';
-import LoginLdap from 'components/login_ldap.jsx';
+import LoginLdap from 'components/login/components/login_ldap.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import UserStore from 'stores/user_store.jsx';
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index f24beb6b3..e4044e6d0 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -47,12 +47,16 @@ class SecurityTab extends React.Component {
super(props);
this.submitPassword = this.submitPassword.bind(this);
+ this.activateMfa = this.activateMfa.bind(this);
+ this.deactivateMfa = this.deactivateMfa.bind(this);
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
+ this.updateMfaToken = this.updateMfaToken.bind(this);
this.getDefaultState = this.getDefaultState.bind(this);
this.createPasswordSection = this.createPasswordSection.bind(this);
this.createSignInSection = this.createSignInSection.bind(this);
+ this.showQrCode = this.showQrCode.bind(this);
this.state = this.getDefaultState();
}
@@ -61,7 +65,9 @@ class SecurityTab extends React.Component {
currentPassword: '',
newPassword: '',
confirmPassword: '',
- authService: this.props.user.auth_service
+ authService: this.props.user.auth_service,
+ mfaShowQr: false,
+ mfaToken: ''
};
}
submitPassword(e) {
@@ -112,6 +118,51 @@ class SecurityTab extends React.Component {
}
);
}
+ activateMfa() {
+ const data = {};
+ data.activate = true;
+ data.token = this.state.mfaToken;
+
+ Client.updateMfa(data,
+ () => {
+ this.props.updateSection('');
+ AsyncClient.getMe();
+ this.setState(this.getDefaultState());
+ },
+ (err) => {
+ const state = this.getDefaultState();
+ if (err.message) {
+ state.serverError = err.message;
+ } else {
+ state.serverError = err;
+ }
+ state.mfaError = '';
+ this.setState(state);
+ }
+ );
+ }
+ deactivateMfa() {
+ const data = {};
+ data.activate = false;
+
+ Client.updateMfa(data,
+ () => {
+ this.props.updateSection('');
+ AsyncClient.getMe();
+ this.setState(this.getDefaultState());
+ },
+ (err) => {
+ const state = this.getDefaultState();
+ if (err.message) {
+ state.serverError = err.message;
+ } else {
+ state.serverError = err;
+ }
+ state.mfaError = '';
+ this.setState(state);
+ }
+ );
+ }
updateCurrentPassword(e) {
this.setState({currentPassword: e.target.value});
}
@@ -121,6 +172,163 @@ class SecurityTab extends React.Component {
updateConfirmPassword(e) {
this.setState({confirmPassword: e.target.value});
}
+ updateMfaToken(e) {
+ this.setState({mfaToken: e.target.value});
+ }
+ showQrCode(e) {
+ e.preventDefault();
+ this.setState({mfaShowQr: true});
+ }
+ createMfaSection() {
+ let updateSectionStatus;
+ let submit;
+
+ if (this.props.activeSection === 'mfa') {
+ let content;
+ let extraInfo;
+ if (this.props.user.mfa_active) {
+ content = (
+ <div key='mfaQrCode'>
+ <a
+ className='btn btn-primary'
+ href='#'
+ onClick={this.deactivateMfa}
+ >
+ <FormattedMessage
+ id='user.settings.mfa.remove'
+ defaultMessage='Remove MFA from your account'
+ />
+ </a>
+ <br/>
+ </div>
+ );
+
+ extraInfo = (
+ <span>
+ <FormattedMessage
+ id='user.settings.mfa.removeHelp'
+ defaultMessage='Removing multi-factor authentication will make your account more vulnerable to attacks.'
+ />
+ </span>
+ );
+ } else if (this.state.mfaShowQr) {
+ content = (
+ <div key='mfaButton'>
+ <label className='col-sm-5 control-label'>
+ <FormattedMessage
+ id='user.settings.mfa.qrCode'
+ defaultMessage='QR Code'
+ />
+ </label>
+ <div className='col-sm-7'>
+ <img
+ className='qr-code-img'
+ src={'/api/v1/users/generate_mfa_qr?time=' + this.props.user.update_at}
+ />
+ </div>
+ <br/>
+ <label className='col-sm-5 control-label'>
+ <FormattedMessage
+ id='user.settings.mfa.enterToken'
+ defaultMessage='Token'
+ />
+ </label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateMfaToken}
+ value={this.state.mfaToken}
+ />
+ </div>
+ </div>
+ );
+
+ extraInfo = (
+ <span>
+ <FormattedMessage
+ id='user.settings.mfa.addHelp'
+ defaultMessage='Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app.'
+ />
+ </span>
+ );
+
+ submit = this.activateMfa;
+ } else {
+ content = (
+ <div key='mfaQrCode'>
+ <a
+ className='btn btn-primary'
+ href='#'
+ onClick={this.showQrCode}
+ >
+ <FormattedMessage
+ id='user.settings.mfa.add'
+ defaultMessage='Add MFA to your account'
+ />
+ </a>
+ <br/>
+ </div>
+ );
+
+ extraInfo = (
+ <span>
+ <FormattedMessage
+ id='user.settings.mfa.addHelp'
+ defaultMessage='To add multi-factor authentication to your account you must have a smartphone with Google Authenticator installed.'
+ />
+ </span>
+ );
+ }
+
+ const inputs = [];
+ inputs.push(
+ <div
+ key='mfaSetting'
+ className='form-group'
+ >
+ {content}
+ </div>
+ );
+
+ updateSectionStatus = function resetSection(e) {
+ this.props.updateSection('');
+ this.setState({mfaToken: '', mfaShowQr: false, mfaError: null});
+ e.preventDefault();
+ }.bind(this);
+
+ return (
+ <SettingItemMax
+ title={Utils.localizeMessage('user.settings.mfa.title', 'Multi-factor Authentication')}
+ inputs={inputs}
+ extraInfo={extraInfo}
+ submit={submit}
+ server_error={this.state.serverError}
+ client_error={this.state.mfaError}
+ updateSection={updateSectionStatus}
+ />
+ );
+ }
+
+ let describe;
+ if (this.props.user.mfa_active) {
+ describe = Utils.localizeMessage('user.settings.security.active', 'Active');
+ } else {
+ describe = Utils.localizeMessage('user.settings.security.inactive', 'Inactive');
+ }
+
+ updateSectionStatus = function updateSection() {
+ this.props.updateSection('mfa');
+ }.bind(this);
+
+ return (
+ <SettingItemMin
+ title={Utils.localizeMessage('user.settings.mfa.title', 'Multi-factor Authentication')}
+ describe={describe}
+ updateSection={updateSectionStatus}
+ />
+ );
+ }
createPasswordSection() {
let updateSectionStatus;
@@ -316,7 +524,6 @@ class SecurityTab extends React.Component {
const user = this.props.user;
if (this.props.activeSection === 'signin') {
- const inputs = [];
const teamName = TeamStore.getCurrent().name;
let emailOption;
@@ -398,6 +605,7 @@ class SecurityTab extends React.Component {
);
}
+ const inputs = [];
inputs.push(
<div key='userSignInOption'>
{emailOption}
@@ -463,17 +671,22 @@ class SecurityTab extends React.Component {
}
render() {
const passwordSection = this.createPasswordSection();
- let signInSection;
let numMethods = 0;
numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods;
- if (global.window.mm_config.EnableSignUpWithEmail && numMethods > 0) {
+ let signInSection;
+ if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) {
signInSection = this.createSignInSection();
}
+ let mfaSection;
+ if (global.window.mm_config.EnableMultifactorAuthentication === 'true' && global.window.mm_license.IsLicensed === 'true') {
+ mfaSection = this.createMfaSection();
+ }
+
return (
<div>
<div className='modal-header'>
@@ -512,6 +725,8 @@ class SecurityTab extends React.Component {
<div className='divider-dark first'/>
{passwordSection}
<div className='divider-light'/>
+ {mfaSection}
+ <div className='divider-light'/>
{signInSection}
<div className='divider-dark'/>
<br></br>