diff options
-rw-r--r-- | webapp/components/login/login_controller.jsx | 1 | ||||
-rw-r--r-- | webapp/components/signup/components/signup_email.jsx | 507 | ||||
-rw-r--r-- | webapp/components/signup/components/signup_ldap.jsx | 233 | ||||
-rw-r--r-- | webapp/components/signup/signup_controller.jsx | 333 | ||||
-rw-r--r-- | webapp/components/signup_user_complete.jsx | 840 | ||||
-rw-r--r-- | webapp/i18n/en.json | 6 | ||||
-rw-r--r-- | webapp/routes/route_root.jsx | 14 | ||||
-rw-r--r-- | webapp/sass/routes/_signup.scss | 29 |
8 files changed, 1113 insertions, 850 deletions
diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx index f62464f37..5a5d0388a 100644 --- a/webapp/components/login/login_controller.jsx +++ b/webapp/components/login/login_controller.jsx @@ -520,6 +520,7 @@ export default class LoginController extends React.Component { key='saml' href={'/login/sso/saml' + this.props.location.search} > + <span className='icon fa fa-lock fa--margin-top'/> <span> {window.mm_config.SamlLoginButtonText} </span> diff --git a/webapp/components/signup/components/signup_email.jsx b/webapp/components/signup/components/signup_email.jsx new file mode 100644 index 000000000..2d4b3f277 --- /dev/null +++ b/webapp/components/signup/components/signup_email.jsx @@ -0,0 +1,507 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LoadingScreen from 'components/loading_screen.jsx'; + +import * as GlobalActions from 'actions/global_actions.jsx'; +import {track} from 'actions/analytics_actions.jsx'; + +import BrowserStore from 'stores/browser_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; +import Client from 'client/web_client.jsx'; +import Constants from 'utils/constants.jsx'; + +import React from 'react'; +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import {browserHistory, Link} from 'react-router/es6'; + +import logoImage from 'images/logo.png'; + +export default class SignupEmail extends React.Component { + static get propTypes() { + return { + location: React.PropTypes.object + }; + } + + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.getInviteInfo = this.getInviteInfo.bind(this); + this.renderEmailSignup = this.renderEmailSignup.bind(this); + this.isUserValid = this.isUserValid.bind(this); + + this.state = this.getInviteInfo(); + } + + getInviteInfo() { + let data = this.props.location.query.d; + let hash = this.props.location.query.h; + const inviteId = this.props.location.query.id; + let email = ''; + let teamDisplayName = ''; + let teamName = ''; + let teamId = ''; + let loading = true; + let serverError = ''; + let noOpenServerError = false; + + if (hash && hash.length > 0) { + const parsedData = JSON.parse(data); + email = parsedData.email; + teamDisplayName = parsedData.display_name; + teamName = parsedData.name; + teamId = parsedData.id; + loading = false; + } else if (inviteId && inviteId.length > 0) { + loading = true; + Client.getInviteInfo( + inviteId, + (inviteData) => { + if (!inviteData) { + return; + } + + serverError = ''; + teamDisplayName = inviteData.display_name; + teamName = inviteData.name; + teamId = inviteData.id; + }, + () => { + noOpenServerError = true; + serverError = ( + <FormattedMessage + id='signup_user_completed.invalid_invite' + defaultMessage='The invite link was invalid. Please speak with your Administrator to receive an invitation.' + /> + ); + } + ); + + loading = false; + data = null; + hash = null; + } else { + loading = false; + } + + return { + data, + hash, + email, + teamDisplayName, + teamName, + teamId, + inviteId, + loading, + serverError, + noOpenServerError + }; + } + + finishSignup() { + GlobalActions.emitInitialLoad( + () => { + const query = this.props.location.query; + GlobalActions.loadDefaultLocale(); + if (query.redirect_to) { + browserHistory.push(query.redirect_to); + } else { + browserHistory.push('/select_team'); + } + } + ); + } + + handleSignupSuccess(user, data) { + track('signup', 'signup_user_02_complete'); + Client.loginById( + data.id, + user.password, + '', + () => { + if (this.state.hash > 0) { + BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true})); + } + + GlobalActions.emitInitialLoad( + () => { + const query = this.props.location.query; + if (query.redirect_to) { + browserHistory.push(query.redirect_to); + } else { + browserHistory.push('/select_team'); + } + } + ); + }, + (err) => { + if (err.id === 'api.user.login.not_verified.app_error') { + browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName)); + } else { + this.setState({serverError: err.message}); + } + } + ); + } + + isUserValid() { + const providedEmail = this.refs.email.value.trim(); + if (!providedEmail) { + this.setState({ + nameError: '', + emailError: (<FormattedMessage id='signup_user_completed.required'/>), + passwordError: '', + serverError: '' + }); + return false; + } + + if (!Utils.isEmail(providedEmail)) { + this.setState({ + nameError: '', + emailError: (<FormattedMessage id='signup_user_completed.validEmail'/>), + passwordError: '', + serverError: '' + }); + return false; + } + + const providedUsername = this.refs.name.value.trim().toLowerCase(); + if (!providedUsername) { + this.setState({ + nameError: (<FormattedMessage id='signup_user_completed.required'/>), + emailError: '', + passwordError: '', + serverError: '' + }); + return false; + } + + const usernameError = Utils.isValidUsername(providedUsername); + if (usernameError === 'Cannot use a reserved word as a username.') { + this.setState({ + nameError: (<FormattedMessage id='signup_user_completed.reserved'/>), + emailError: '', + passwordError: '', + serverError: '' + }); + return false; + } else if (usernameError) { + this.setState({ + nameError: ( + <FormattedMessage + id='signup_user_completed.usernameLength' + values={{ + min: Constants.MIN_USERNAME_LENGTH, + max: Constants.MAX_USERNAME_LENGTH + }} + /> + ), + emailError: '', + passwordError: '', + serverError: '' + }); + return false; + } + + const providedPassword = this.refs.password.value; + const pwdError = Utils.isValidPassword(providedPassword); + if (pwdError) { + this.setState({ + nameError: '', + emailError: '', + passwordError: pwdError, + serverError: '' + }); + return false; + } + + return true; + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.isUserValid()) { + this.setState({ + nameError: '', + emailError: '', + passwordError: '', + serverError: '' + }); + + const user = { + email: this.refs.email.value.trim(), + username: this.refs.name.value.trim().toLowerCase(), + password: this.refs.password.value, + allow_marketing: true + }; + + Client.createUserWithInvite(user, + this.state.data, + this.state.hash, + this.state.inviteId, + this.handleSignupSuccess.bind(this, user), + (err) => { + this.setState({serverError: err.message}); + } + ); + } + } + + renderEmailSignup() { + let emailError = null; + let emailHelpText = ( + <span className='help-block'> + <FormattedMessage + id='signup_user_completed.emailHelp' + defaultMessage='Valid email required for sign-up' + /> + </span> + ); + let emailDivStyle = 'form-group'; + if (this.state.emailError) { + emailError = (<label className='control-label'>{this.state.emailError}</label>); + emailHelpText = ''; + emailDivStyle += ' has-error'; + } + + let nameError = null; + let nameHelpText = ( + <span className='help-block'> + <FormattedMessage + id='signup_user_completed.userHelp' + defaultMessage="Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'" + values={{ + min: Constants.MIN_USERNAME_LENGTH, + max: Constants.MAX_USERNAME_LENGTH + }} + /> + </span> + ); + let nameDivStyle = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameHelpText = ''; + nameDivStyle += ' has-error'; + } + + let passwordError = null; + let passwordDivStyle = 'form-group'; + if (this.state.passwordError) { + passwordError = <label className='control-label'>{this.state.passwordError}</label>; + passwordDivStyle += ' has-error'; + } + + let yourEmailIs = null; + if (this.state.email) { + yourEmailIs = ( + <FormattedHTMLMessage + id='signup_user_completed.emailIs' + defaultMessage="Your email address is <strong>{email}</strong>. You'll use this address to sign in to {siteName}." + values={{ + email: this.state.email, + siteName: global.window.mm_config.SiteName + }} + /> + ); + } + + let emailContainerStyle = 'margin--extra'; + if (this.state.email) { + emailContainerStyle = 'hidden'; + } + + return ( + <form> + <div className='inner__content'> + <div className={emailContainerStyle}> + <h5><strong> + <FormattedMessage + id='signup_user_completed.whatis' + defaultMessage="What's your email address?" + /> + </strong></h5> + <div className={emailDivStyle}> + <input + type='email' + ref='email' + className='form-control' + defaultValue={this.state.email} + placeholder='' + maxLength='128' + autoFocus={true} + spellCheck='false' + autoCapitalize='off' + /> + {emailError} + {emailHelpText} + </div> + </div> + {yourEmailIs} + <div className='margin--extra'> + <h5><strong> + <FormattedMessage + id='signup_user_completed.chooseUser' + defaultMessage='Choose your username' + /> + </strong></h5> + <div className={nameDivStyle}> + <input + type='text' + ref='name' + className='form-control' + placeholder='' + maxLength={Constants.MAX_USERNAME_LENGTH} + spellCheck='false' + autoCapitalize='off' + /> + {nameError} + {nameHelpText} + </div> + </div> + <div className='margin--extra'> + <h5><strong> + <FormattedMessage + id='signup_user_completed.choosePwd' + defaultMessage='Choose your password' + /> + </strong></h5> + <div className={passwordDivStyle}> + <input + type='password' + ref='password' + className='form-control' + placeholder='' + maxLength='128' + spellCheck='false' + /> + {passwordError} + </div> + </div> + <p className='margin--extra'> + <button + type='submit' + onClick={this.handleSubmit} + className='btn-primary btn' + > + <FormattedMessage + id='signup_user_completed.create' + defaultMessage='Create Account' + /> + </button> + </p> + </div> + </form> + ); + } + + render() { + track('signup', 'signup_user_01_welcome'); + + let serverError = null; + if (this.state.serverError) { + serverError = ( + <div className={'form-group has-error'}> + <label className='control-label'>{this.state.serverError}</label> + </div> + ); + } + + if (this.state.loading) { + return (<LoadingScreen/>); + } + + let emailSignup; + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { + emailSignup = this.renderEmailSignup(); + } else { + return null; + } + + let terms = null; + if (!this.state.noOpenServerError && emailSignup) { + terms = ( + <p> + <FormattedHTMLMessage + id='create_team.agreement' + defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}." + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </p> + ); + } + + if (this.state.noOpenServerError) { + emailSignup = null; + } + + let description = null; + if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true' && global.window.mm_config.EnableCustomBrand === 'true') { + description = global.window.mm_config.CustomDescriptionText; + } else { + description = ( + <FormattedMessage + id='web.root.signup_info' + defaultMessage='All team communication in one place, searchable and accessible anywhere' + /> + ); + } + + return ( + <div> + <div className='signup-header'> + <Link to='/signup_user_complete'> + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </Link> + </div> + <div className='col-sm-12'> + <div className='signup-team__container padding--less'> + <img + className='signup-team-logo' + src={logoImage} + /> + <h1>{global.window.mm_config.SiteName}</h1> + <h4 className='color--light'> + {description} + </h4> + <h4 className='color--light'> + <FormattedMessage + id='signup_user_completed.lets' + defaultMessage="Let's create your account" + /> + </h4> + <span className='color--light'> + <FormattedMessage + id='signup_user_completed.haveAccount' + defaultMessage='Already have an account?' + /> + {' '} + <Link + to={'/login'} + query={this.props.location.query} + > + <FormattedMessage + id='signup_user_completed.signIn' + defaultMessage='Click here to sign in.' + /> + </Link> + </span> + {emailSignup} + {serverError} + {terms} + </div> + </div> + </div> + ); + } +} diff --git a/webapp/components/signup/components/signup_ldap.jsx b/webapp/components/signup/components/signup_ldap.jsx new file mode 100644 index 000000000..92089f2f7 --- /dev/null +++ b/webapp/components/signup/components/signup_ldap.jsx @@ -0,0 +1,233 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import FormError from 'components/form_error.jsx'; + +import * as GlobalActions from 'actions/global_actions.jsx'; +import {track} from 'actions/analytics_actions.jsx'; + +import * as Utils from 'utils/utils.jsx'; +import Client from 'client/web_client.jsx'; + +import React from 'react'; +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import {browserHistory, Link} from 'react-router/es6'; + +import logoImage from 'images/logo.png'; + +export default class SignupLdap extends React.Component { + static get propTypes() { + return { + location: React.PropTypes.object + }; + } + + constructor(props) { + super(props); + + this.handleLdapSignup = this.handleLdapSignup.bind(this); + this.handleLdapSignupSuccess = this.handleLdapSignupSuccess.bind(this); + + this.state = ({ + ldapError: '' + }); + } + + handleLdapSignup(e) { + e.preventDefault(); + + this.setState({ldapError: ''}); + + Client.webLoginByLdap( + this.refs.id.value.trim(), + this.refs.password.value, + null, + this.handleLdapSignupSuccess, + (err) => { + this.setState({ + ldapError: err.message + }); + } + ); + } + + handleLdapSignupSuccess() { + if (this.props.location.query.id || this.props.location.query.h) { + Client.addUserToTeamFromInvite( + this.props.location.query.d, + this.props.location.query.h, + this.props.location.query.id, + () => { + this.finishSignup(); + }, + () => { + // there's not really a good way to deal with this, so just let the user log in like normal + this.finishSignup(); + } + ); + } else { + this.finishSignup(); + } + } + + finishSignup() { + GlobalActions.emitInitialLoad( + () => { + GlobalActions.loadDefaultLocale(); + browserHistory.push('/select_team'); + } + ); + } + + render() { + track('signup', 'signup_user_01_welcome'); + + let ldapIdPlaceholder; + if (global.window.mm_config.LdapLoginFieldName) { + ldapIdPlaceholder = global.window.mm_config.LdapLoginFieldName; + } else { + ldapIdPlaceholder = Utils.localizeMessage('login.ldap_username', 'LDAP Username'); + } + + let errorClass = ''; + if (this.state.ldapError) { + errorClass += ' has-error'; + } + + let ldapSignup; + if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) { + ldapSignup = ( + <div className='inner__content'> + <h5> + <strong> + <FormattedMessage + id='signup.ldap' + defaultMessage='LDAP Credentials' + /> + </strong> + </h5> + <form + onSubmit={this.handleLdapSignup} + > + <div className='signup__email-container'> + <FormError + error={this.state.ldapError} + margin={true} + /> + <div className={'form-group' + errorClass}> + <input + className='form-control' + name='ldapId' + ref='id' + placeholder={ldapIdPlaceholder} + spellCheck='false' + autoCapitalize='off' + /> + </div> + <div className={'form-group' + errorClass}> + <input + type='password' + className='form-control' + name='password' + ref='password' + placeholder={Utils.localizeMessage('login.password', 'Password')} + spellCheck='false' + /> + </div> + <div className='form-group'> + <button + type='submit' + className='btn btn-primary' + disabled={!this.state.ldapId || !this.state.ldapPassword} + > + <FormattedMessage + id='login.signIn' + defaultMessage='Sign in' + /> + </button> + </div> + </div> + </form> + </div> + ); + } else { + return null; + } + + let terms = null; + if (ldapSignup) { + terms = ( + <p> + <FormattedHTMLMessage + id='create_team.agreement' + defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}." + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </p> + ); + } + + let description = null; + if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true' && global.window.mm_config.EnableCustomBrand === 'true') { + description = global.window.mm_config.CustomDescriptionText; + } else { + description = ( + <FormattedMessage + id='web.root.signup_info' + defaultMessage='All team communication in one place, searchable and accessible anywhere' + /> + ); + } + + return ( + <div> + <div className='signup-header'> + <Link to='/signup_user_complete'> + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </Link> + </div> + <div className='col-sm-12'> + <div className='signup-team__container padding--less'> + <img + className='signup-team-logo' + src={logoImage} + /> + <h1>{global.window.mm_config.SiteName}</h1> + <h4 className='color--light'> + {description} + </h4> + <h4 className='color--light'> + <FormattedMessage + id='signup_user_completed.lets' + defaultMessage="Let's create your account" + /> + </h4> + <span className='color--light'> + <FormattedMessage + id='signup_user_completed.haveAccount' + defaultMessage='Already have an account?' + /> + {' '} + <Link + to={'/login'} + query={this.props.location.query} + > + <FormattedMessage + id='signup_user_completed.signIn' + defaultMessage='Click here to sign in.' + /> + </Link> + </span> + {ldapSignup} + {terms} + </div> + </div> + </div> + ); + } +} diff --git a/webapp/components/signup/signup_controller.jsx b/webapp/components/signup/signup_controller.jsx new file mode 100644 index 000000000..a0587bba9 --- /dev/null +++ b/webapp/components/signup/signup_controller.jsx @@ -0,0 +1,333 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import FormError from 'components/form_error.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import BrowserStore from 'stores/browser_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import Client from 'client/web_client.jsx'; +import * as GlobalActions from 'actions/global_actions.jsx'; + +import logoImage from 'images/logo.png'; +import ErrorBar from 'components/error_bar.jsx'; + +import {FormattedMessage} from 'react-intl'; +import {browserHistory, Link} from 'react-router/es6'; + +export default class SignupController extends React.Component { + constructor(props) { + super(props); + + this.renderSignupControls = this.renderSignupControls.bind(this); + + let loading = false; + let serverError = ''; + let noOpenServerError = false; + let usedBefore = false; + + if (props.location.query) { + loading = true; + const hash = props.location.query.h; + + if (hash && hash.length > 0 && !UserStore.getCurrentUser()) { + usedBefore = BrowserStore.getGlobalItem(hash); + loading = false; + } else if (global.window.mm_config.EnableOpenServer !== 'true' && !UserStore.getNoAccounts()) { + noOpenServerError = true; + loading = false; + serverError = ( + <FormattedMessage + id='signup_user_completed.no_open_server' + defaultMessage='This server does not allow open signups. Please speak with your Administrator to receive an invitation.' + /> + ); + } + } + + this.state = { + loading, + serverError, + noOpenServerError, + usedBefore + }; + } + + componentDidMount() { + AsyncClient.checkVersion(); + + if (this.props.location.query) { + const hash = this.props.location.query.h; + const data = this.props.location.query.d; + const inviteId = this.props.location.query.id; + + if ((inviteId && inviteId.length > 0) || (hash && hash.length > 0)) { + if (UserStore.getCurrentUser()) { + Client.addUserToTeamFromInvite( + data, + hash, + inviteId, + (team) => { + GlobalActions.emitInitialLoad( + () => { + browserHistory.push('/' + team.name + '/channels/town-square'); + } + ); + }, + (err) => { + this.setState({ // eslint-disable-line react/no-did-mount-set-state + serverError: err.message + }); + } + ); + } else if (!this.state.usedBefore) { + Client.getInviteInfo( + inviteId, + (inviteData) => { + if (!inviteData) { + return; + } + + this.setState({ // eslint-disable-line react/no-did-mount-set-state + serverError: '', + loading: false + }); + }, + () => { + this.setState({ // eslint-disable-line react/no-did-mount-set-state + noOpenServerError: true, + loading: false, + serverError: ( + <FormattedMessage + id='signup_user_completed.invalid_invite' + defaultMessage='The invite link was invalid. Please speak with your Administrator to receive an invitation.' + /> + ) + }); + } + ); + } + } else if (UserStore.getCurrentUser()) { + browserHistory.push('/select_team'); + } else { + this.setState({ // eslint-disable-line react/no-did-mount-set-state + loading: false + }); + } + } + } + + renderSignupControls() { + let signupControls = []; + + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { + signupControls.push( + <Link + className='btn btn-custom-login btn--full email' + key='email' + to={'/signup_email' + window.location.search} + > + + <span className='icon fa fa-envelope'/> + <span> + <FormattedMessage + id='signup.email' + defaultMessage='Email and Password' + /> + </span> + </Link> + ); + } + + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { + signupControls.push( + <a + className='btn btn-custom-login btn--full gitlab' + key='gitlab' + href={Client.getOAuthRoute() + '/gitlab/signup' + window.location.search} + > + <span className='icon'/> + <span> + <FormattedMessage + id='signup.gitlab' + defaultMessage='GitLab Single-Sign-On' + /> + </span> + </a> + ); + } + + if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { + signupControls.push( + <a + className='btn btn-custom-login btn--full google' + key='google' + href={Client.getOAuthRoute() + '/google/signup' + window.location.search} + > + <span className='icon'/> + <span> + <FormattedMessage + id='signup.google' + defaultMessage='Google Account' + /> + </span> + </a> + ); + } + + if (global.window.mm_config.EnableSignUpWithOffice365 === 'true') { + signupControls.push( + <a + className='btn btn-custom-login btn--full office365' + key='office365' + href={Client.getOAuthRoute() + '/office365/signup' + window.location.search} + > + <span className='icon'/> + <span> + <FormattedMessage + id='signup.office365' + defaultMessage='Office 365' + /> + </span> + </a> + ); + } + + if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableLdap === 'true') { + signupControls.push( + <Link + className='btn btn-custom-login btn--full ldap' + key='ldap' + to={'/signup_ldap'} + > + <span className='icon fa fa-folder-open fa--margin-top'/> + <span> + <FormattedMessage + id='signup.ldap' + defaultMessage='LDAP Credentials' + /> + </span> + </Link> + ); + } + + if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableSaml === 'true') { + let query = ''; + if (window.location.search) { + query = '&action=signup'; + } else { + query = '?action=signup'; + } + + signupControls.push( + <a + className='btn btn-custom-login btn--full saml' + key='saml' + href={'/login/sso/saml' + window.location.search + query} + > + <span className='icon fa fa-lock fa--margin-top'/> + <span> + {global.window.mm_config.SamlLoginButtonText} + </span> + </a> + ); + } + + if (signupControls.length === 0) { + const signupDisabledError = ( + <FormattedMessage + id='signup_user_completed.none' + defaultMessage='No user creation method has been enabled. Please contact an administrator for access.' + /> + ); + signupControls = ( + <FormError + error={signupDisabledError} + margin={true} + /> + ); + } + + return signupControls; + } + + render() { + if (this.state.loading) { + return (<LoadingScreen/>); + } + + if (this.state.usedBefore) { + return ( + <div> + <FormattedMessage + id='signup_user_completed.expired' + defaultMessage="You've already completed the signup process for this invitation or this invitation has expired." + /> + </div> + ); + } + + let signupControls = this.renderSignupControls(); + + let serverError = null; + if (this.state.serverError) { + serverError = ( + <div className={'form-group has-error'}> + <label className='control-label'>{this.state.serverError}</label> + </div> + ); + } + + if (this.state.noOpenServerError || this.state.usedBefore) { + signupControls = null; + } + + return ( + <div> + <ErrorBar/> + <div className='signup-header'> + <Link to='/'> + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </Link> + </div> + <div className='col-sm-12'> + <div className='signup-team__container'> + <img + className='signup-team-logo' + src={logoImage} + /> + <div className='signup__content'> + <h1>{global.window.mm_config.SiteName}</h1> + <h4 className='color--light'> + <FormattedMessage + id='web.root.signup_info' + /> + </h4> + <div className='margin--extra'> + <h5><strong> + <FormattedMessage + id='signup.title' + defaultMessage='Create an account with:' + /> + </strong></h5> + </div> + {signupControls} + {serverError} + </div> + </div> + </div> + </div> + ); + } +} + +SignupController.propTypes = { + location: React.PropTypes.object +};
\ No newline at end of file diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx deleted file mode 100644 index 9bb208c84..000000000 --- a/webapp/components/signup_user_complete.jsx +++ /dev/null @@ -1,840 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import FormError from 'components/form_error.jsx'; -import LoadingScreen from 'components/loading_screen.jsx'; - -import * as GlobalActions from 'actions/global_actions.jsx'; -import {track} from 'actions/analytics_actions.jsx'; - -import BrowserStore from 'stores/browser_store.jsx'; -import UserStore from 'stores/user_store.jsx'; - -import * as Utils from 'utils/utils.jsx'; -import Client from 'client/web_client.jsx'; -import Constants from 'utils/constants.jsx'; - -import React from 'react'; -import ReactDOM from 'react-dom'; -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; -import {browserHistory, Link} from 'react-router/es6'; - -import logoImage from 'images/logo.png'; - -export default class SignupUserComplete extends React.Component { - static get propTypes() { - return { - location: React.PropTypes.object - }; - } - - constructor(props) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - this.handleLdapSignup = this.handleLdapSignup.bind(this); - - this.handleLdapIdChange = this.handleLdapIdChange.bind(this); - this.handleLdapPasswordChange = this.handleLdapPasswordChange.bind(this); - - this.state = { - data: '', - hash: '', - usedBefore: false, - email: '', - teamDisplayName: '', - signupDisabledError: '', - teamName: '', - teamId: '', - openServer: false, - loading: true, - inviteId: '', - ldapId: '', - ldapPassword: '' - }; - } - - componentWillMount() { - let data = this.props.location.query.d; - let hash = this.props.location.query.h; - const inviteId = this.props.location.query.id; - let usedBefore = false; - let email = ''; - let teamDisplayName = ''; - let teamName = ''; - let teamId = ''; - let openServer = false; - let loading = true; - - if ((inviteId && inviteId.length > 0) || (hash && hash.length > 0)) { - // if we are already logged in then attempt to just join the team - if (UserStore.getCurrentUser()) { - Client.addUserToTeamFromInvite( - data, - hash, - inviteId, - (team) => { - loading = true; - GlobalActions.emitInitialLoad( - () => { - browserHistory.push('/' + team.name + '/channels/town-square'); - } - ); - }, - (err) => { - this.setState({ - noOpenServerError: true, - serverError: err.message, - loading: false - }); - } - ); - } else if (hash) { - // If we have a hash in the url then we are attempting to access a private team - const parsedData = JSON.parse(data); - usedBefore = BrowserStore.getGlobalItem(hash); - email = parsedData.email; - teamDisplayName = parsedData.display_name; - teamName = parsedData.name; - teamId = parsedData.id; - loading = false; - } else { - loading = true; - Client.getInviteInfo( - inviteId, - (inviteData) => { - if (!inviteData) { - return; - } - - this.setState({ - serverError: null, - teamDisplayName: inviteData.display_name, - teamName: inviteData.name, - teamId: inviteData.id - }); - }, - () => { - this.setState({ - noOpenServerError: true, - serverError: - <FormattedMessage - id='signup_user_completed.invalid_invite' - defaultMessage='The invite link was invalid. Please speak with your Administrator to receive an invitation.' - /> - }); - } - ); - - loading = false; - data = ''; - hash = ''; - } - } else if (global.window.mm_config.EnableOpenServer === 'true' || UserStore.getNoAccounts()) { - // If this is the first account then let them create an account anyway. - // The server will verify it's the first account before allowing creation. - // Of if the server is open then we don't care. - openServer = true; - loading = false; - } else { - loading = false; - this.setState({ - noOpenServerError: true, - serverError: - <FormattedMessage - id='signup_user_completed.no_open_server' - defaultMessage='This server does not allow open signups. Please speak with your Administrator to receive an invitation.' - /> - }); - } - - this.setState({ - data, - hash, - usedBefore, - email, - teamDisplayName, - teamName, - teamId, - openServer, - inviteId, - loading - }); - - this.setState({ - signupDisabledError: ( - <FormattedMessage - id='signup_user_completed.none' - defaultMessage='No user creation method has been enabled. Please contact an administrator for access.' - /> - ) - }); - } - - handleLdapSignup(e) { - e.preventDefault(); - - this.setState({ldapError: ''}); - - Client.webLoginByLdap( - this.state.ldapId, - this.state.ldapPassword, - null, - () => { - if (this.props.location.query.id || this.props.location.query.h) { - Client.addUserToTeamFromInvite( - this.props.location.query.d, - this.props.location.query.h, - this.props.location.query.id, - () => { - this.finishSignup(); - }, - () => { - // there's not really a good way to deal with this, so just let the user log in like normal - this.finishSignup(); - } - ); - - return; - } - - this.finishSignup(); - }, - (err) => { - if (err.id === 'ent.ldap.do_login.user_not_registered.app_error' || err.id === 'ent.ldap.do_login.user_filtered.app_error') { - this.setState({ - ldapError: ( - <FormattedMessage - id='login.userNotFound' - defaultMessage="We couldn't find an account matching your login credentials." - /> - ) - }); - } else if (err.id === 'ent.ldap.do_login.invalid_password.app_error') { - this.setState({ - ldapError: ( - <FormattedMessage - id='login.invalidPassword' - defaultMessage='Your password is incorrect.' - /> - ) - }); - } else { - this.setState({ldapError: err.message}); - } - } - ); - } - - finishSignup() { - GlobalActions.emitInitialLoad( - () => { - const query = this.props.location.query; - GlobalActions.loadDefaultLocale(); - if (query.redirect_to) { - browserHistory.push(query.redirect_to); - } else { - browserHistory.push('/select_team'); - } - } - ); - } - - handleUserCreated(user, data) { - track('signup', 'signup_user_02_complete'); - Client.loginById( - data.id, - user.password, - '', - () => { - if (this.state.hash > 0) { - BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true})); - } - - GlobalActions.emitInitialLoad( - () => { - const query = this.props.location.query; - if (query.redirect_to) { - browserHistory.push(query.redirect_to); - } else { - browserHistory.push('/select_team'); - } - } - ); - }, - (err) => { - if (err.id === 'api.user.login.not_verified.app_error') { - browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName)); - } else { - this.setState({serverError: err.message}); - } - } - ); - } - - handleSubmit(e) { - e.preventDefault(); - - const providedEmail = ReactDOM.findDOMNode(this.refs.email).value.trim(); - if (!providedEmail) { - this.setState({ - nameError: '', - emailError: (<FormattedMessage id='signup_user_completed.required'/>), - passwordError: '', - serverError: '' - }); - return; - } - - if (!Utils.isEmail(providedEmail)) { - this.setState({ - nameError: '', - emailError: (<FormattedMessage id='signup_user_completed.validEmail'/>), - passwordError: '', - serverError: '' - }); - return; - } - - const providedUsername = ReactDOM.findDOMNode(this.refs.name).value.trim().toLowerCase(); - if (!providedUsername) { - this.setState({ - nameError: (<FormattedMessage id='signup_user_completed.required'/>), - emailError: '', - passwordError: '', - serverError: '' - }); - return; - } - - const usernameError = Utils.isValidUsername(providedUsername); - if (usernameError === 'Cannot use a reserved word as a username.') { - this.setState({ - nameError: (<FormattedMessage id='signup_user_completed.reserved'/>), - emailError: '', - passwordError: '', - serverError: '' - }); - return; - } else if (usernameError) { - this.setState({ - nameError: ( - <FormattedMessage - id='signup_user_completed.usernameLength' - values={{ - min: Constants.MIN_USERNAME_LENGTH, - max: Constants.MAX_USERNAME_LENGTH - }} - /> - ), - emailError: '', - passwordError: '', - serverError: '' - }); - return; - } - - const providedPassword = ReactDOM.findDOMNode(this.refs.password).value; - const pwdError = Utils.isValidPassword(providedPassword); - if (pwdError != null) { - this.setState({ - nameError: '', - emailError: '', - passwordError: pwdError, - serverError: '' - }); - } - - this.setState({ - nameError: '', - emailError: '', - passwordError: '', - serverError: '' - }); - - const user = { - email: providedEmail, - username: providedUsername, - password: providedPassword, - allow_marketing: true - }; - - Client.createUserWithInvite(user, - this.state.data, - this.state.hash, - this.state.inviteId, - this.handleUserCreated.bind(this, user), - (err) => { - this.setState({serverError: err.message}); - } - ); - } - - handleLdapIdChange(e) { - e.preventDefault(); - - this.setState({ - ldapId: e.target.value - }); - } - - handleLdapPasswordChange(e) { - e.preventDefault(); - - this.setState({ - ldapPassword: e.target.value - }); - } - - renderLdapLogin() { - let ldapIdPlaceholder; - if (global.window.mm_config.LdapLoginFieldName) { - ldapIdPlaceholder = global.window.mm_config.LdapLoginFieldName; - } else { - ldapIdPlaceholder = Utils.localizeMessage('login.ldap_username', 'LDAP Username'); - } - - let errorClass = ''; - if (this.state.ldapError) { - errorClass += ' has-error'; - } - - return ( - <form - onSubmit={this.handleLdapSignup} - > - <div className='signup__email-container'> - <FormError - error={this.state.ldapError} - margin={true} - /> - <div className={'form-group' + errorClass}> - <input - className='form-control' - name='ldapId' - value={this.state.ldapId} - onChange={this.handleLdapIdChange} - placeholder={ldapIdPlaceholder} - spellCheck='false' - autoCapitalize='off' - /> - </div> - <div className={'form-group' + errorClass}> - <input - type='password' - className='form-control' - name='password' - value={this.state.ldapPassword} - onChange={this.handleLdapPasswordChange} - placeholder={Utils.localizeMessage('login.password', 'Password')} - spellCheck='false' - /> - </div> - <div className='form-group'> - <button - type='submit' - className='btn btn-primary' - disabled={!this.state.ldapId || !this.state.ldapPassword} - > - <FormattedMessage - id='login.signIn' - defaultMessage='Sign in' - /> - </button> - </div> - </div> - </form> - ); - } - - render() { - track('signup', 'signup_user_01_welcome'); - - // If we have been used then just display a message - if (this.state.usedBefore) { - return ( - <div> - <FormattedMessage - id='signup_user_completed.expired' - defaultMessage="You've already completed the signup process for this invitation or this invitation has expired." - /> - </div> - ); - } - - if (this.state.loading) { - return (<LoadingScreen/>); - } - - // set up error labels - var emailError = null; - var emailHelpText = ( - <span className='help-block'> - <FormattedMessage - id='signup_user_completed.emailHelp' - defaultMessage='Valid email required for sign-up' - /> - </span> - ); - var emailDivStyle = 'form-group'; - if (this.state.emailError) { - emailError = (<label className='control-label'>{this.state.emailError}</label>); - emailHelpText = ''; - emailDivStyle += ' has-error'; - } - - var nameError = null; - var nameHelpText = ( - <span className='help-block'> - <FormattedMessage - id='signup_user_completed.userHelp' - defaultMessage="Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'" - values={{ - min: Constants.MIN_USERNAME_LENGTH, - max: Constants.MAX_USERNAME_LENGTH - }} - /> - </span> - ); - var nameDivStyle = 'form-group'; - if (this.state.nameError) { - nameError = <label className='control-label'>{this.state.nameError}</label>; - nameHelpText = ''; - nameDivStyle += ' has-error'; - } - - var passwordError = null; - var passwordDivStyle = 'form-group'; - if (this.state.passwordError) { - passwordError = <label className='control-label'>{this.state.passwordError}</label>; - passwordDivStyle += ' has-error'; - } - - var serverError = null; - if (this.state.serverError) { - serverError = ( - <div className={'form-group has-error'}> - <label className='control-label'>{this.state.serverError}</label> - </div> - ); - } - - // set up the email entry and hide it if an email was provided - var yourEmailIs = ''; - if (this.state.email) { - yourEmailIs = ( - <FormattedHTMLMessage - id='signup_user_completed.emailIs' - defaultMessage="Your email address is <strong>{email}</strong>. You'll use this address to sign in to {siteName}." - values={{ - email: this.state.email, - siteName: global.window.mm_config.SiteName - }} - /> - ); - } - - var emailContainerStyle = 'margin--extra'; - if (this.state.email) { - emailContainerStyle = 'hidden'; - } - - var email = ( - <div className={emailContainerStyle}> - <h5><strong> - <FormattedMessage - id='signup_user_completed.whatis' - defaultMessage="What's your email address?" - /> - </strong></h5> - <div className={emailDivStyle}> - <input - type='email' - ref='email' - className='form-control' - defaultValue={this.state.email} - placeholder='' - maxLength='128' - autoFocus={true} - spellCheck='false' - autoCapitalize='off' - /> - {emailError} - {emailHelpText} - </div> - </div> - ); - - let signupMessage = []; - if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { - signupMessage.push( - <a - className='btn btn-custom-login gitlab' - key='gitlab' - href={Client.getOAuthRoute() + '/gitlab/signup' + window.location.search} - > - <span className='icon'/> - <span> - <FormattedMessage - id='signup_user_completed.gitlab' - defaultMessage='with GitLab' - /> - </span> - </a> - ); - } - - if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { - signupMessage.push( - <a - className='btn btn-custom-login google' - key='google' - href={Client.getOAuthRoute() + '/google/signup' + window.location.search} - > - <span className='icon'/> - <span> - <FormattedMessage - id='signup_user_completed.google' - defaultMessage='with Google' - /> - </span> - </a> - ); - } - - if (global.window.mm_config.EnableSignUpWithOffice365 === 'true') { - signupMessage.push( - <a - className='btn btn-custom-login office365' - key='office365' - href={Client.getOAuthRoute() + '/office365/signup' + window.location.search} - > - <span className='icon'/> - <span> - <FormattedMessage - id='signup_user_completed.office365' - defaultMessage='with Office 365' - /> - </span> - </a> - ); - } - - if (global.window.mm_config.EnableSaml === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.SAML === 'true') { - signupMessage.push( - <a - className='btn btn-custom-login saml' - key='saml' - href={`/login/sso/saml${window.location.search}${window.location.search ? '&' : '?'}action=signup`} - > - <span> - {global.window.mm_config.SamlLoginButtonText} - </span> - </a> - ); - } - - let ldapSignup; - if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) { - ldapSignup = ( - <div className='inner__content'> - <h5> - <strong> - <FormattedMessage - id='signup_user_completed.withLdap' - defaultMessage='With your LDAP credentials' - /> - </strong> - </h5> - {this.renderLdapLogin()} - </div> - ); - } - - let emailSignup; - if (global.window.mm_config.EnableSignUpWithEmail === 'true') { - emailSignup = ( - <form> - <div className='inner__content'> - {email} - {yourEmailIs} - <div className='margin--extra'> - <h5><strong> - <FormattedMessage - id='signup_user_completed.chooseUser' - defaultMessage='Choose your username' - /> - </strong></h5> - <div className={nameDivStyle}> - <input - type='text' - ref='name' - className='form-control' - placeholder='' - maxLength={Constants.MAX_USERNAME_LENGTH} - spellCheck='false' - autoCapitalize='off' - /> - {nameError} - {nameHelpText} - </div> - </div> - <div className='margin--extra'> - <h5><strong> - <FormattedMessage - id='signup_user_completed.choosePwd' - defaultMessage='Choose your password' - /> - </strong></h5> - <div className={passwordDivStyle}> - <input - type='password' - ref='password' - className='form-control' - placeholder='' - maxLength='128' - spellCheck='false' - /> - {passwordError} - </div> - </div> - <p className='margin--extra'> - <button - type='submit' - onClick={this.handleSubmit} - className='btn-primary btn' - > - <FormattedMessage - id='signup_user_completed.create' - defaultMessage='Create Account' - /> - </button> - </p> - </div> - </form> - ); - } - - if (signupMessage.length > 0 && (emailSignup || ldapSignup)) { - signupMessage = ( - <div> - {signupMessage} - <div className='or__container'> - <FormattedMessage - id='signup_user_completed.or' - defaultMessage='or' - /> - </div> - </div> - ); - } - - if (ldapSignup && emailSignup) { - ldapSignup = ( - <div> - {ldapSignup} - <div className='or__container'> - <FormattedMessage - id='signup_user_completed.or' - defaultMessage='or' - /> - </div> - </div> - ); - } - - let terms = null; - if (!this.state.noOpenServerError && (emailSignup || ldapSignup)) { - terms = ( - <p> - <FormattedHTMLMessage - id='create_team.agreement' - defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}." - values={{ - siteName: global.window.mm_config.SiteName - }} - /> - </p> - ); - } - - if (signupMessage.length === 0 && !emailSignup && !ldapSignup) { - emailSignup = ( - <FormError - error={this.state.signupDisabledError} - margin={true} - /> - ); - } - - if (this.state.noOpenServerError) { - signupMessage = null; - emailSignup = null; - ldapSignup = null; - } - - let description = null; - if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true' && global.window.mm_config.EnableCustomBrand === 'true') { - description = global.window.mm_config.CustomDescriptionText; - } else { - description = ( - <FormattedMessage - id='web.root.signup_info' - defaultMessage='All team communication in one place, searchable and accessible anywhere' - /> - ); - } - - return ( - <div> - <div className='signup-header'> - <Link to='/'> - <span className='fa fa-chevron-left'/> - <FormattedMessage - id='web.header.back' - /> - </Link> - </div> - <div className='col-sm-12'> - <div className='signup-team__container padding--less'> - <img - className='signup-team-logo' - src={logoImage} - /> - <h1>{global.window.mm_config.SiteName}</h1> - <h4 className='color--light'> - {description} - </h4> - <h4 className='color--light'> - <FormattedMessage - id='signup_user_completed.lets' - defaultMessage="Let's create your account" - /> - </h4> - <span className='color--light'> - <FormattedMessage - id='signup_user_completed.haveAccount' - defaultMessage='Already have an account?' - /> - {' '} - <Link - to={'/login'} - query={this.props.location.query} - > - <FormattedMessage - id='signup_user_completed.signIn' - defaultMessage='Click here to sign in.' - /> - </Link> - </span> - {signupMessage} - {ldapSignup} - {emailSignup} - {serverError} - {terms} - </div> - </div> - </div> - ); - } -} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index b26e33cd6..5c636716b 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1597,6 +1597,12 @@ "sidebar_right_menu.switch_team": "Team Selection", "sidebar_right_menu.teamLink": "Get Team Invite Link", "sidebar_right_menu.teamSettings": "Team Settings", + "signup.title": "Create an account with:", + "signup.ldap": "LDAP Credentials", + "signup.google": "Google Account", + "signup.office365": "Office 365", + "signup.gitlab": "GitLab Single-Sign-On", + "signup.email": "Email and Password", "signup_team.choose": "Your teams: ", "signup_team.createTeam": "Or Create a Team", "signup_team.disabled": "Team creation has been disabled. Please contact an administrator for access.", diff --git a/webapp/routes/route_root.jsx b/webapp/routes/route_root.jsx index 52eb5c757..9d64c6012 100644 --- a/webapp/routes/route_root.jsx +++ b/webapp/routes/route_root.jsx @@ -66,7 +66,19 @@ export default { { path: 'signup_user_complete', getComponents: (location, callback) => { - System.import('components/signup_user_complete.jsx').then(RouteUtils.importComponentSuccess(callback)); + System.import('components/signup/signup_controller.jsx').then(RouteUtils.importComponentSuccess(callback)); + } + }, + { + path: 'signup_email', + getComponents: (location, callback) => { + System.import('components/signup/components/signup_email.jsx').then(RouteUtils.importComponentSuccess(callback)); + } + }, + { + path: 'signup_ldap', + getComponents: (location, callback) => { + System.import('components/signup/components/signup_ldap.jsx').then(RouteUtils.importComponentSuccess(callback)); } }, { diff --git a/webapp/sass/routes/_signup.scss b/webapp/sass/routes/_signup.scss index 8315f8890..106cdc372 100644 --- a/webapp/sass/routes/_signup.scss +++ b/webapp/sass/routes/_signup.scss @@ -203,6 +203,11 @@ .fa { font-size: 17px; margin-right: 8px; + + &.fa--margin-top { + position: relative; + top: 2px; + } } .icon { @@ -210,6 +215,7 @@ display: inline-block; height: 18px; margin-right: 8px; + text-align: center; width: 18px; } @@ -218,10 +224,11 @@ color: $white; display: block; height: 40px; - line-height: 34px; + line-height: 36px; margin: 1em 0; min-width: 200px; - padding: 0 1em; + padding: 0 1em 0 2em; + text-align: left; width: 200px; &.gitlab { @@ -268,16 +275,15 @@ } .icon { - margin-left:-10px; background-image: url('../images/office365Logo.png'); } } &.ldap { - background: #dd4b39; + background: #3AA1CF; &:hover { - background: darken(#dd4b39, 10%); + background: darken(#3AA1CF, 10%); } span { @@ -298,10 +304,10 @@ } &.saml { - background: #dd4b39; + background: #34a28b; &:hover { - background: darken(#dd4b39, 10%); + background: darken(#34a28b, 10%); } span { @@ -309,11 +315,16 @@ } } - &.btn-full { + &.btn--full { + max-width: 350px; padding-left: 35px; text-align: left; width: 100%; } + + &.btn--large { + width: 300px; + } } &.btn-default { @@ -502,4 +513,4 @@ .verify_panel { margin: 60px auto auto; max-width: 380px; -} +}
\ No newline at end of file |