login_controller.jsx 25.9 KB
Newer Older
1
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
// See LICENSE.txt for license information.
3

4 5 6
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
7
import {Link} from 'react-router-dom';
8

9
import {Client4} from 'mattermost-redux/client';
JoramWilander's avatar
JoramWilander committed
10

11
import * as GlobalActions from 'actions/global_actions.jsx';
12
import {addUserToTeamFromInvite} from 'actions/team_actions.jsx';
13
import {checkMfa, webLogin} from 'actions/user_actions.jsx';
enahum's avatar
enahum committed
14
import BrowserStore from 'stores/browser_store.jsx';
JoramWilander's avatar
JoramWilander committed
15
import UserStore from 'stores/user_store.jsx';
16
import TeamStore from 'stores/team_store.jsx';
17 18

import {browserHistory} from 'utils/browser_history';
19
import Constants from 'utils/constants.jsx';
20
import messageHtmlToComponent from 'utils/message_html_to_component';
21
import * as TextFormatting from 'utils/text_formatting.jsx';
JoramWilander's avatar
JoramWilander committed
22
import * as Utils from 'utils/utils.jsx';
23

24
import logoImage from 'images/logo.png';
25 26

import SiteNameAndDescription from 'components/common/site_name_and_description';
27 28
import AnnouncementBar from 'components/announcement_bar';
import FormError from 'components/form_error.jsx';
29

30
import LoginMfa from '../login_mfa.jsx';
31
export default class LoginController extends React.Component {
32 33
    static get propTypes() {
        return {
34 35 36 37 38 39 40 41 42 43 44 45 46
            location: PropTypes.object.isRequired,
            isLicensed: PropTypes.bool.isRequired,

            customBrandText: PropTypes.string,
            customDescriptionText: PropTypes.string,
            enableCustomBrand: PropTypes.bool.isRequired,
            enableLdap: PropTypes.bool.isRequired,
            enableOpenServer: PropTypes.bool.isRequired,
            enableSaml: PropTypes.bool.isRequired,
            enableSignInWithEmail: PropTypes.bool.isRequired,
            enableSignInWithUsername: PropTypes.bool.isRequired,
            enableSignUpWithEmail: PropTypes.bool.isRequired,
            enableSignUpWithGitLab: PropTypes.bool.isRequired,
47
            enableSignUpWithPhabricator: PropTypes.bool.isRequired,
48 49 50 51 52
            enableSignUpWithGoogle: PropTypes.bool.isRequired,
            enableSignUpWithOffice365: PropTypes.bool.isRequired,
            experimentalPrimaryTeam: PropTypes.string,
            ldapLoginFieldName: PropTypes.string,
            samlLoginButtonText: PropTypes.string,
53
            siteName: PropTypes.string,
54 55 56
        };
    }

57 58 59
    constructor(props) {
        super(props);

JoramWilander's avatar
JoramWilander committed
60 61
        this.preSubmit = this.preSubmit.bind(this);
        this.submit = this.submit.bind(this);
62
        this.finishSignin = this.finishSignin.bind(this);
63

64 65 66
        this.handleLoginIdChange = this.handleLoginIdChange.bind(this);
        this.handlePasswordChange = this.handlePasswordChange.bind(this);

67
        let loginId = '';
68 69
        if ((new URLSearchParams(this.props.location.search)).get('extra') === Constants.SIGNIN_VERIFIED && (new URLSearchParams(this.props.location.search)).get('email')) {
            loginId = (new URLSearchParams(this.props.location.search)).get('email');
70 71
        }

72
        this.state = {
73 74 75 76
            ldapEnabled: this.props.isLicensed && this.props.enableLdap,
            usernameSigninEnabled: this.props.enableSignInWithUsername,
            emailSigninEnabled: this.props.enableSignInWithEmail,
            samlEnabled: this.props.isLicensed && this.props.enableSaml,
77
            loginId,
78
            password: '',
79
            showMfa: false,
80
            loading: false,
81
        };
82
    }
83

84
    componentDidMount() {
85
        document.title = this.props.siteName;
enahum's avatar
enahum committed
86
        BrowserStore.removeGlobalItem('team');
87
        if (UserStore.getCurrentUser()) {
enahum's avatar
enahum committed
88
            GlobalActions.redirectUserToDefaultTeam();
89
        }
90

91
        if ((new URLSearchParams(this.props.location.search)).get('extra') === Constants.SIGNIN_VERIFIED && (new URLSearchParams(this.props.location.search)).get('email')) {
92 93
            this.refs.password.focus();
        }
94
    }
95 96 97 98

    preSubmit(e) {
        e.preventDefault();

99 100 101 102 103 104
        const {location} = this.props;
        const newQuery = location.search.replace(/(extra=password_change)&?/i, '');
        if (newQuery !== location.search) {
            browserHistory.replace(`${location.pathname}${newQuery}${location.hash}`);
        }

105 106 107 108 109 110 111
        // password managers don't always call onInput handlers for form fields so it's possible
        // for the state to get out of sync with what the user sees in the browser
        let loginId = this.refs.loginId.value;
        if (loginId !== this.state.loginId) {
            this.setState({loginId});
        }

112
        const password = this.refs.password.value;
113 114 115 116
        if (password !== this.state.password) {
            this.setState({password});
        }

117
        // don't trim the password since we support spaces in passwords
118
        loginId = loginId.trim().toLowerCase();
119 120

        if (!loginId) {
121 122 123 124 125 126 127 128 129 130 131 132
            // it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements
            let msgId = 'login.no';
            if (this.state.emailSigninEnabled) {
                msgId += 'Email';
            }
            if (this.state.usernameSigninEnabled) {
                msgId += 'Username';
            }
            if (this.state.ldapEnabled) {
                msgId += 'LdapUsername';
            }

133 134 135
            this.setState({
                serverError: (
                    <FormattedMessage
136
                        id={msgId}
137
                        values={{
138
                            ldapUsername: this.props.ldapLoginFieldName || Utils.localizeMessage('login.ldapUsernameLower', 'AD/LDAP username'),
139 140
                        }}
                    />
141
                ),
142 143 144 145 146 147 148 149 150 151 152
            });
            return;
        }

        if (!password) {
            this.setState({
                serverError: (
                    <FormattedMessage
                        id='login.noPassword'
                        defaultMessage='Please enter your password'
                    />
153
                ),
154 155 156
            });
            return;
        }
157

158 159
        checkMfa(
            loginId,
160 161
            (requiresMfa) => {
                if (requiresMfa) {
162 163 164
                    this.setState({showMfa: true});
                } else {
                    this.submit(loginId, password, '');
165
                }
166 167 168 169 170
            },
            (err) => {
                this.setState({serverError: err.message});
            }
        );
171 172 173
    }

    submit(loginId, password, token) {
174
        this.setState({serverError: null, loading: true});
175

176
        webLogin(
177 178 179 180
            loginId,
            password,
            token,
            () => {
181
                // check for query params brought over from signup_user_complete
182
                const params = new URLSearchParams(this.props.location.search);
183
                const inviteToken = params.get('t') || '';
184 185
                const inviteId = params.get('id') || '';

186
                if (inviteId || inviteToken) {
187
                    addUserToTeamFromInvite(
188
                        inviteToken,
189
                        inviteId,
190 191
                        (team) => {
                            this.finishSignin(team);
192 193 194 195 196 197 198 199 200 201
                        },
                        () => {
                            // there's not really a good way to deal with this, so just let the user log in like normal
                            this.finishSignin();
                        }
                    );

                    return;
                }

202 203 204 205 206 207
                this.finishSignin();
            },
            (err) => {
                if (err.id === 'api.user.login.not_verified.app_error') {
                    browserHistory.push('/should_verify_email?&email=' + encodeURIComponent(loginId));
                } else if (err.id === 'store.sql_user.get_for_login.app_error' ||
208
                    err.id === 'ent.ldap.do_login.user_not_registered.app_error') {
209
                    this.setState({
210
                        showMfa: false,
211
                        loading: false,
212 213 214
                        serverError: (
                            <FormattedMessage
                                id='login.userNotFound'
215 216
                                defaultMessage="We couldn't find an account matching your login credentials."
                            />
217
                        ),
218 219 220
                    });
                } else if (err.id === 'api.user.check_user_password.invalid.app_error' || err.id === 'ent.ldap.do_login.invalid_password.app_error') {
                    this.setState({
221
                        showMfa: false,
222
                        loading: false,
223 224 225 226
                        serverError: (
                            <FormattedMessage
                                id='login.invalidPassword'
                                defaultMessage='Your password is incorrect.'
227
                            />
228
                        ),
229 230
                    });
                } else {
231
                    this.setState({showMfa: false, serverError: err.message, loading: false});
JoramWilander's avatar
JoramWilander committed
232 233 234 235
                }
            }
        );
    }
236

237
    finishSignin(team) {
238
        const experimentalPrimaryTeam = this.props.experimentalPrimaryTeam;
239
        const primaryTeam = TeamStore.getByName(experimentalPrimaryTeam);
240 241 242
        const query = new URLSearchParams(this.props.location.search);
        const redirectTo = query.get('redirect_to');

243
        GlobalActions.loadCurrentLocale();
244 245
        if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
            browserHistory.push(redirectTo);
246 247
        } else if (team) {
            browserHistory.push(`/${team.name}`);
248
        } else if (primaryTeam) {
249
            browserHistory.push(`/${primaryTeam.name}/channels/${Constants.DEFAULT_CHANNEL}`);
250 251 252
        } else {
            GlobalActions.redirectUserToDefaultTeam();
        }
253 254
    }

255 256
    handleLoginIdChange(e) {
        this.setState({
257
            loginId: e.target.value,
258 259 260 261 262
        });
    }

    handlePasswordChange(e) {
        this.setState({
263
            password: e.target.value,
264
        });
JoramWilander's avatar
JoramWilander committed
265
    }
266

267
    createCustomLogin() {
268
        if (this.props.enableCustomBrand) {
269
            const text = this.props.customBrandText || '';
270
            const formattedText = TextFormatting.formatText(text);
271 272 273 274

            return (
                <div>
                    <img
275
                        src={Client4.getBrandImageUrl(0)}
276
                    />
277
                    <div>
278
                        {messageHtmlToComponent(formattedText, false, {mentions: false})}
279
                    </div>
280 281 282 283 284 285
                </div>
            );
        }

        return null;
    }
286

287
    createLoginPlaceholder() {
288 289 290
        const ldapEnabled = this.state.ldapEnabled;
        const usernameSigninEnabled = this.state.usernameSigninEnabled;
        const emailSigninEnabled = this.state.emailSigninEnabled;
291

292 293 294 295 296 297 298 299 300 301
        const loginPlaceholders = [];
        if (emailSigninEnabled) {
            loginPlaceholders.push(Utils.localizeMessage('login.email', 'Email'));
        }

        if (usernameSigninEnabled) {
            loginPlaceholders.push(Utils.localizeMessage('login.username', 'Username'));
        }

        if (ldapEnabled) {
302 303
            if (this.props.ldapLoginFieldName) {
                loginPlaceholders.push(this.props.ldapLoginFieldName);
304
            } else {
305
                loginPlaceholders.push(Utils.localizeMessage('login.ldapUsername', 'AD/LDAP Username'));
306 307 308 309 310 311 312 313 314 315 316 317 318 319
            }
        }

        if (loginPlaceholders.length >= 2) {
            return loginPlaceholders.slice(0, loginPlaceholders.length - 1).join(', ') +
                Utils.localizeMessage('login.placeholderOr', ' or ') +
                loginPlaceholders[loginPlaceholders.length - 1];
        } else if (loginPlaceholders.length === 1) {
            return loginPlaceholders[0];
        }

        return '';
    }

320
    checkSignUpEnabled() {
321 322
        return this.props.enableSignUpWithEmail ||
            this.props.enableSignUpWithGitLab ||
323
            this.props.enableSignUpWithPhabricator ||
324 325 326 327
            this.props.enableSignUpWithOffice365 ||
            this.props.enableSignUpWithGoogle ||
            this.props.enableLdap ||
            this.props.enableSaml;
328 329
    }

330
    createLoginOptions() {
331
        const extraParam = (new URLSearchParams(this.props.location.search)).get('extra');
332 333 334 335 336
        let extraBox = '';
        if (extraParam) {
            if (extraParam === Constants.SIGNIN_CHANGE) {
                extraBox = (
                    <div className='alert alert-success'>
337 338 339 340
                        <i
                            className='fa fa-check'
                            title={Utils.localizeMessage('generic_icons.success', 'Success Icon')}
                        />
341 342 343 344 345 346 347 348 349
                        <FormattedMessage
                            id='login.changed'
                            defaultMessage=' Sign-in method changed successfully'
                        />
                    </div>
                );
            } else if (extraParam === Constants.SIGNIN_VERIFIED) {
                extraBox = (
                    <div className='alert alert-success'>
350 351 352 353
                        <i
                            className='fa fa-check'
                            title={Utils.localizeMessage('generic_icons.success', 'Success Icon')}
                        />
354 355 356 357 358 359 360 361 362
                        <FormattedMessage
                            id='login.verified'
                            defaultMessage=' Email Verified'
                        />
                    </div>
                );
            } else if (extraParam === Constants.SESSION_EXPIRED) {
                extraBox = (
                    <div className='alert alert-warning'>
363 364 365 366
                        <i
                            className='fa fa-exclamation-triangle'
                            title={Utils.localizeMessage('generic_icons.warning', 'Warning Icon')}
                        />
367 368 369 370 371 372
                        <FormattedMessage
                            id='login.session_expired'
                            defaultMessage=' Your session has expired. Please login again.'
                        />
                    </div>
                );
373 374 375
            } else if (extraParam === Constants.PASSWORD_CHANGE) {
                extraBox = (
                    <div className='alert alert-success'>
376 377 378 379
                        <i
                            className='fa fa-check'
                            title={Utils.localizeMessage('generic_icons.success', 'Success Icon')}
                        />
380 381 382 383 384 385
                        <FormattedMessage
                            id='login.passwordChanged'
                            defaultMessage=' Password updated successfully'
                        />
                    </div>
                );
386 387 388
            }
        }

389 390
        const loginControls = [];

391
        const ldapEnabled = this.state.ldapEnabled;
392
        const gitlabSigninEnabled = this.props.enableSignUpWithGitLab;
393
        const phabricatorSigninEnabled = this.props.enableSignUpWithPhabricator;
394 395
        const googleSigninEnabled = this.props.enableSignUpWithGoogle;
        const office365SigninEnabled = this.props.enableSignUpWithOffice365;
enahum's avatar
enahum committed
396
        const samlSigninEnabled = this.state.samlEnabled;
397 398
        const usernameSigninEnabled = this.state.usernameSigninEnabled;
        const emailSigninEnabled = this.state.emailSigninEnabled;
JoramWilander's avatar
JoramWilander committed
399

400 401 402 403
        if (emailSigninEnabled || usernameSigninEnabled || ldapEnabled) {
            let errorClass = '';
            if (this.state.serverError) {
                errorClass = ' has-error';
JoramWilander's avatar
JoramWilander committed
404 405
            }

406 407
            let loginButton = (
                <FormattedMessage
408 409
                    id='login.signIn'
                    defaultMessage='Sign in'
410 411
                />
            );
412 413 414 415

            if (this.state.loading) {
                loginButton =
                (<span>
416 417 418 419
                    <span
                        className='fa fa-refresh icon--rotate'
                        title={Utils.localizeMessage('generic_icons.loading', 'Loading Icon')}
                    />
420 421 422 423 424 425 426
                    <FormattedMessage
                        id='login.signInLoading'
                        defaultMessage='Signing in...'
                    />
                </span>);
            }

427 428 429 430 431
            loginControls.push(
                <form
                    key='loginBoxes'
                    onSubmit={this.preSubmit}
                >
432
                    <div className='signup__email-container'>
433 434 435 436
                        <FormError
                            error={this.state.serverError}
                            margin={true}
                        />
437 438
                        <div className={'form-group' + errorClass}>
                            <input
439
                                id='loginId'
440
                                className='form-control'
441
                                ref='loginId'
442 443 444
                                name='loginId'
                                value={this.state.loginId}
                                onChange={this.handleLoginIdChange}
445
                                placeholder={this.createLoginPlaceholder()}
446
                                spellCheck='false'
447
                                autoCapitalize='off'
448
                                autoFocus='true'
JoramWilander's avatar
JoramWilander committed
449 450
                            />
                        </div>
451 452
                        <div className={'form-group' + errorClass}>
                            <input
453
                                id='loginPassword'
454 455
                                type='password'
                                className='form-control'
456
                                ref='password'
457 458 459 460 461
                                name='password'
                                value={this.state.password}
                                onChange={this.handlePasswordChange}
                                placeholder={Utils.localizeMessage('login.password', 'Password')}
                                spellCheck='false'
JoramWilander's avatar
JoramWilander committed
462 463
                            />
                        </div>
464 465
                        <div className='form-group'>
                            <button
466
                                id='loginButton'
467 468 469
                                type='submit'
                                className='btn btn-primary'
                            >
470
                                { loginButton }
471 472
                            </button>
                        </div>
JoramWilander's avatar
JoramWilander committed
473
                    </div>
474 475
                </form>
            );
476 477
        }

478
        if (this.props.enableOpenServer && this.checkSignUpEnabled()) {
479
            loginControls.push(
Asaad Mahmood's avatar
Asaad Mahmood committed
480 481 482 483
                <div
                    className='form-group'
                    key='signup'
                >
484
                    <span>
485
                        <FormattedMessage
486 487
                            id='login.noAccount'
                            defaultMessage="Don't have an account? "
488
                        />
489
                        <Link
490
                            id='signup'
491
                            to={'/signup_user_complete' + this.props.location.search}
492 493 494 495 496 497 498 499 500 501 502
                            className='signup-team-login'
                        >
                            <FormattedMessage
                                id='login.create'
                                defaultMessage='Create one now'
                            />
                        </Link>
                    </span>
                </div>
            );
        }
503

JoramWilander's avatar
JoramWilander committed
504
        if (usernameSigninEnabled || emailSigninEnabled) {
505 506 507 508 509
            loginControls.push(
                <div
                    key='forgotPassword'
                    className='form-group'
                >
510
                    <Link to={'/reset_password'}>
JoramWilander's avatar
JoramWilander committed
511 512 513 514 515 516 517 518 519
                        <FormattedMessage
                            id='login.forgot'
                            defaultMessage='I forgot my password'
                        />
                    </Link>
                </div>
            );
        }

520
        if ((emailSigninEnabled || usernameSigninEnabled || ldapEnabled) && (gitlabSigninEnabled || phabricatorSigninEnabled || googleSigninEnabled || samlSigninEnabled || office365SigninEnabled)) {
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
            loginControls.push(
                <div
                    key='divider'
                    className='or__container'
                >
                    <FormattedMessage
                        id='login.or'
                        defaultMessage='or'
                    />
                </div>
            );

            loginControls.push(
                <h5 key='oauthHeader'>
                    <FormattedMessage
                        id='login.signInWith'
                        defaultMessage='Sign in with:'
                    />
                </h5>
            );
        }

        if (gitlabSigninEnabled) {
            loginControls.push(
                <a
                    className='btn btn-custom-login gitlab'
                    key='gitlab'
548
                    href={Client4.getOAuthRoute() + '/gitlab/login' + this.props.location.search}
549 550
                >
                    <span>
Asaad Mahmood's avatar
Asaad Mahmood committed
551 552 553 554 555 556 557
                        <span className='icon'/>
                        <span>
                            <FormattedMessage
                                id='login.gitlab'
                                defaultMessage='GitLab'
                            />
                        </span>
558 559 560 561 562
                    </span>
                </a>
            );
        }

563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
        if (phabricatorSigninEnabled) {
            loginControls.push(
                <a
                    className='btn btn-custom-login phabricator'
                    key='phabricator'
                    href={Client4.getOAuthRoute() + '/phabricator/login' + this.props.location.search}
                >
                    <span>
                        <span className='icon'/>
                        <span>
                            <FormattedMessage
                                id='login.phabricator'
                                defaultMessage='Phabricator'
                            />
                        </span>
                    </span>
                </a>
            );
        }

583 584
        if (googleSigninEnabled) {
            loginControls.push(
585
                <a
586 587
                    className='btn btn-custom-login google'
                    key='google'
588
                    href={Client4.getOAuthRoute() + '/google/login' + this.props.location.search}
589 590
                >
                    <span>
Asaad Mahmood's avatar
Asaad Mahmood committed
591 592 593 594 595 596 597
                        <span className='icon'/>
                        <span>
                            <FormattedMessage
                                id='login.google'
                                defaultMessage='Google Apps'
                            />
                        </span>
598
                    </span>
599 600 601 602 603 604 605 606 607
                </a>
            );
        }

        if (office365SigninEnabled) {
            loginControls.push(
                <a
                    className='btn btn-custom-login office365'
                    key='office365'
608
                    href={Client4.getOAuthRoute() + '/office365/login' + this.props.location.search}
609 610
                >
                    <span>
Asaad Mahmood's avatar
Asaad Mahmood committed
611 612 613 614 615 616 617
                        <span className='icon'/>
                        <span>
                            <FormattedMessage
                                id='login.office365'
                                defaultMessage='Office 365'
                            />
                        </span>
618 619
                    </span>
                </a>
620 621 622
            );
        }

enahum's avatar
enahum committed
623 624 625 626
        if (samlSigninEnabled) {
            loginControls.push(
                <a
                    className='btn btn-custom-login saml'
627
                    key='saml'
628
                    href={Client4.getUrl() + '/login/sso/saml' + this.props.location.search}
enahum's avatar
enahum committed
629 630
                >
                    <span>
631 632 633 634
                        <span
                            className='icon fa fa-lock fa--margin-top'
                            title='Saml icon'
                        />
Asaad Mahmood's avatar
Asaad Mahmood committed
635
                        <span>
636
                            {this.props.samlLoginButtonText}
Asaad Mahmood's avatar
Asaad Mahmood committed
637
                        </span>
enahum's avatar
enahum committed
638 639 640 641 642
                    </span>
                </a>
            );
        }

643 644 645 646 647 648
        if (loginControls.length === 0) {
            loginControls.push(
                <FormError
                    error={
                        <FormattedMessage
                            id='login.noMethods'
lfbrock's avatar
lfbrock committed
649
                            defaultMessage='No sign-in methods are enabled. Please contact your System Administrator.'
650 651 652 653 654 655 656
                        />
                    }
                    margin={true}
                />
            );
        }

JoramWilander's avatar
JoramWilander committed
657 658 659
        return (
            <div>
                {extraBox}
660
                {loginControls}
JoramWilander's avatar
JoramWilander committed
661 662 663
            </div>
        );
    }
664

JoramWilander's avatar
JoramWilander committed
665
    render() {
666 667 668 669 670
        const {
            customDescriptionText,
            siteName,
        } = this.props;

JoramWilander's avatar
JoramWilander committed
671
        let content;
672 673
        let customContent;
        let customClass;
JoramWilander's avatar
JoramWilander committed
674 675 676 677 678 679
        if (this.state.showMfa) {
            content = (
                <LoginMfa
                    loginId={this.state.loginId}
                    password={this.state.password}
                    submit={this.submit}
680 681
                />
            );
JoramWilander's avatar
JoramWilander committed
682
        } else {
683
            content = this.createLoginOptions();
684 685 686 687
            customContent = this.createCustomLogin();
            if (customContent) {
                customClass = 'branded';
            }
688 689 690 691
        }

        return (
            <div>
692
                <AnnouncementBar/>
693
                <div className='col-sm-12'>
694 695 696 697
                    <div className={'signup-team__container ' + customClass}>
                        <div className='signup__markdown'>
                            {customContent}
                        </div>
698 699 700 701
                        <img
                            className='signup-team-logo'
                            src={logoImage}
                        />
702
                        <div className='signup__content'>
703 704 705 706
                            <SiteNameAndDescription
                                customDescriptionText={customDescriptionText}
                                siteName={siteName}
                            />
707 708
                            {content}
                        </div>
709 710 711 712 713
                    </div>
                </div>
            </div>
        );
    }
714
}