summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-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
-rw-r--r--webapp/i18n/en.json6
-rw-r--r--webapp/root.jsx3
-rw-r--r--webapp/utils/client.jsx38
-rw-r--r--webapp/utils/constants.jsx3
12 files changed, 693 insertions, 334 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>
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index c8e40f0a3..6f23552b3 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -328,6 +328,8 @@
"admin.select_team.close": "Close",
"admin.select_team.select": "Select",
"admin.select_team.selectTeam": "Select Team",
+ "admin.service.mfaTitle": "Enable Multi-factor Authentication:",
+ "admin.service.mfaDesc": "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.",
"admin.service.attemptDescription": "Login attempts allowed before user is locked out and required to reset password via email.",
"admin.service.attemptExample": "Ex \"10\"",
"admin.service.attemptTitle": "Maximum Login Attempts:",
@@ -857,6 +859,10 @@
"login.session_expired": " Your session has expired. Please login again.",
"login.signTo": "Sign in to:",
"login.verified": " Email Verified",
+ "login_mfa.token": "MFA Token",
+ "login_mfa.enterToken": "To complete the sign in process, please enter a token from your smartphone's authenticator",
+ "login_mfa.submit": "Submit",
+ "login_mfa.tokenReq": "Please enter an MFA token",
"login_email.badTeam": "Bad team name",
"login_email.email": "Email",
"login_email.emailReq": "An email is required",
diff --git a/webapp/root.jsx b/webapp/root.jsx
index fca368bdb..adabe3fa3 100644
--- a/webapp/root.jsx
+++ b/webapp/root.jsx
@@ -12,7 +12,6 @@ import React from 'react';
import ReactDOM from 'react-dom';
import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router';
import Root from 'components/root.jsx';
-import Login from 'components/login.jsx';
import LoggedIn from 'components/logged_in.jsx';
import NotLoggedIn from 'components/not_logged_in.jsx';
import NeedsTeam from 'components/needs_team.jsx';
@@ -58,6 +57,8 @@ import OAuthToEmail from 'components/claim/components/oauth_to_email.jsx';
import LDAPToEmail from 'components/claim/components/ldap_to_email.jsx';
import EmailToLDAP from 'components/claim/components/email_to_ldap.jsx';
+import Login from 'components/login/login.jsx';
+
import * as I18n from 'i18n/i18n.jsx';
// This is for anything that needs to be done for ALL react components.
diff --git a/webapp/utils/client.jsx b/webapp/utils/client.jsx
index d42767d31..a596d1e14 100644
--- a/webapp/utils/client.jsx
+++ b/webapp/utils/client.jsx
@@ -337,13 +337,28 @@ export function logout(success, error) {
});
}
-export function loginByEmail(name, email, password, success, error) {
+export function checkMfa(method, team, loginId, success, error) {
+ $.ajax({
+ url: '/api/v1/users/mfa',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({method, team_name: team, login_id: loginId}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('checkMfa', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function loginByEmail(name, email, password, token, success, error) {
$.ajax({
url: '/api/v1/users/login',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- data: JSON.stringify({name, email, password}),
+ data: JSON.stringify({name, email, password, token}),
success: function onSuccess(data, textStatus, xhr) {
track('api', 'api_users_login_success', data.team_id, 'email', data.email);
sessionStorage.removeItem(data.id + '_last_error');
@@ -381,13 +396,13 @@ export function loginByUsername(name, username, password, success, error) {
});
}
-export function loginByLdap(teamName, id, password, success, error) {
+export function loginByLdap(teamName, id, password, token, success, error) {
$.ajax({
url: '/api/v1/users/login_ldap',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- data: JSON.stringify({teamName, id, password}),
+ data: JSON.stringify({teamName, id, password, token}),
success: function onSuccess(data, textStatus, xhr) {
track('api', 'api_users_loginLdap_success', data.team_id, 'id', id);
sessionStorage.removeItem(data.id + '_last_error');
@@ -1712,3 +1727,18 @@ export function resendVerification(success, error, teamName, email) {
}
});
}
+
+export function updateMfa(data, success, error) {
+ $.ajax({
+ url: '/api/v1/users/update_mfa',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('updateMfa', xhr, status, err);
+ error(e);
+ }
+ });
+}
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index f0e8c260e..421a46860 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -194,8 +194,9 @@ export default {
OFFTOPIC_CHANNEL: 'off-topic',
GITLAB_SERVICE: 'gitlab',
GOOGLE_SERVICE: 'google',
- LDAP_SERVICE: 'ldap',
EMAIL_SERVICE: 'email',
+ LDAP_SERVICE: 'ldap',
+ USERNAME_SERVICE: 'username',
SIGNIN_CHANGE: 'signin_change',
SIGNIN_VERIFIED: 'verified',
SESSION_EXPIRED: 'expired',