diff options
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/admin_console/service_settings.jsx | 55 | ||||
-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.jsx | 92 | ||||
-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.jsx | 2 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_security.jsx | 223 |
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> |