From 0b1a0c77b7fe9a9eef7f134041cb438ac6ad47c6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:07:40 -0600 Subject: [PATCH] Make login pass around server config objects Very similar to password resets and registration, the components pass around a server config for usage by other components. Login is a bit more complicated and needs a few more changes to pull the logic out to a more generic layer. --- src/components/structures/auth/Login.js | 189 ++++++--------------- src/components/views/auth/PasswordLogin.js | 77 ++++----- 2 files changed, 86 insertions(+), 180 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 2940346a4f..46bf0c2c76 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -25,7 +25,7 @@ import sdk from '../../../index'; import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import { AutoDiscovery } from "matrix-js-sdk"; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -59,19 +59,14 @@ module.exports = React.createClass({ propTypes: { onLoggedIn: PropTypes.func.isRequired, - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about where to "sign in to". - defaultServerName: PropTypes.string, // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, + // went wrong. May be replaced with a different error within the + // Login component. + errorText: PropTypes.string, + + // If true, the component will consider itself busy. + busy: PropTypes.bool, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, // Secondary HS which we try to log into if the user is using // the default HS but login fails. Useful for migrating to a // different homeserver without confusing users. @@ -79,12 +74,13 @@ module.exports = React.createClass({ defaultDeviceDisplayName: PropTypes.string, - // login shouldn't know or care how registration is done. + // login shouldn't know or care how registration, password recovery, + // etc is done. onRegisterClick: PropTypes.func.isRequired, - - // login shouldn't care how password recovery is done. onForgotPasswordClick: PropTypes.func, onServerConfigChange: PropTypes.func.isRequired, + + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, }, getInitialState: function() { @@ -93,9 +89,6 @@ module.exports = React.createClass({ errorText: null, loginIncorrect: false, - enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl, - enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl, - // used for preserving form values when changing homeserver username: "", phoneCountry: null, @@ -105,10 +98,6 @@ module.exports = React.createClass({ phase: PHASE_LOGIN, // The current login flow, such as password, SSO, etc. currentFlow: "m.login.password", - - // .well-known discovery - discoveryError: "", - findingHomeserver: false, }; }, @@ -139,10 +128,17 @@ module.exports = React.createClass({ }); }, + isBusy: function() { + return this.state.busy || this.props.busy; + }, + + hasError: function() { + return this.state.errorText || this.props.errorText; + }, + onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { - // Prevent people from submitting their password when homeserver - // discovery went wrong - if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return; + // Prevent people from submitting their password when something isn't right. + if (this.isBusy() || this.hasError()) return; this.setState({ busy: true, @@ -164,7 +160,7 @@ module.exports = React.createClass({ const usingEmail = username.indexOf("@") > 0; if (error.httpStatus === 400 && usingEmail) { errorText = _t('This homeserver does not support login using email address.'); - } else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') { + } else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const errorTop = messageForResourceLimitError( error.data.limit_type, error.data.admin_contact, { @@ -194,11 +190,10 @@ module.exports = React.createClass({
{ _t('Incorrect username and/or password.') }
- { _t('Please note you are logging into the %(hs)s server, not matrix.org.', - { - hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''), - }) - } + {_t( + 'Please note you are logging into the %(hs)s server, not matrix.org.', + {hs: this.props.serverConfig.hsName}, + )}
); @@ -235,9 +230,9 @@ module.exports = React.createClass({ onUsernameBlur: function(username) { this.setState({ username: username, - discoveryError: null, + errorText: null, }); - if (username[0] === "@") { + if (username[0] === "@" && false) { // TODO: TravisR - Restore this const serverName = username.split(':').slice(1).join(':'); try { // we have to append 'https://' to make the URL constructor happy @@ -246,7 +241,7 @@ module.exports = React.createClass({ this._tryWellKnownDiscovery(url.hostname); } catch (e) { console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); - this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); + this.setState({errorText: _t("Failed to perform homeserver discovery")}); } } }, @@ -274,32 +269,19 @@ module.exports = React.createClass({ } }, - onServerConfigChange: function(config) { - const self = this; - const newState = { - errorText: null, // reset err messages - }; - if (config.hsUrl !== undefined) { - newState.enteredHsUrl = config.hsUrl; - } - if (config.isUrl !== undefined) { - newState.enteredIsUrl = config.isUrl; - } - - this.props.onServerConfigChange(config); - this.setState(newState, function() { - self._initLoginLogic(config.hsUrl || null, config.isUrl); - }); - }, - onRegisterClick: function(ev) { ev.preventDefault(); ev.stopPropagation(); this.props.onRegisterClick(); }, - onServerDetailsNextPhaseClick(ev) { + async onServerDetailsNextPhaseClick(ev) { ev.stopPropagation(); + // TODO: TravisR - Capture the user's input somehow else + if (this._serverConfigRef) { + // Just to make sure the user's input gets captured + await this._serverConfigRef.validateServer(); + } this.setState({ phase: PHASE_LOGIN, }); @@ -313,64 +295,13 @@ module.exports = React.createClass({ }); }, - _tryWellKnownDiscovery: async function(serverName) { - if (!serverName.trim()) { - // Nothing to discover - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - return; - } - - this.setState({findingHomeserver: true}); - try { - const discovery = await AutoDiscovery.findClientConfig(serverName); - - const state = discovery["m.homeserver"].state; - if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) { - this.setState({ - discoveryError: discovery["m.homeserver"].error, - findingHomeserver: false, - }); - } else if (state === AutoDiscovery.PROMPT) { - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - } else if (state === AutoDiscovery.SUCCESS) { - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - this.onServerConfigChange({ - hsUrl: discovery["m.homeserver"].base_url, - isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS - ? discovery["m.identity_server"].base_url - : "", - }); - } else { - console.warn("Unknown state for m.homeserver in discovery response: ", discovery); - this.setState({ - discoveryError: _t("Unknown failure discovering homeserver"), - findingHomeserver: false, - }); - } - } catch (e) { - console.error(e); - this.setState({ - findingHomeserver: false, - discoveryError: _t("Unknown error discovering homeserver"), - }); - } - }, - _initLoginLogic: function(hsUrl, isUrl) { const self = this; - hsUrl = hsUrl || this.state.enteredHsUrl; - isUrl = isUrl || this.state.enteredIsUrl; + hsUrl = hsUrl || this.props.serverConfig.hsUrl; + isUrl = isUrl || this.props.serverConfig.isUrl; - const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null; + // TODO: TravisR - Only use this if the homeserver is the default homeserver + const fallbackHsUrl = this.props.fallbackHsUrl; const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, @@ -378,8 +309,6 @@ module.exports = React.createClass({ this._loginLogic = loginLogic; this.setState({ - enteredHsUrl: hsUrl, - enteredIsUrl: isUrl, busy: true, loginIncorrect: false, }); @@ -445,8 +374,8 @@ module.exports = React.createClass({ if (err.cors === 'rejected') { if (window.location.protocol === 'https:' && - (this.state.enteredHsUrl.startsWith("http:") || - !this.state.enteredHsUrl.startsWith("http")) + (this.props.serverConfig.hsUrl.startsWith("http:") || + !this.props.serverConfig.hsUrl.startsWith("http")) ) { errorText = { _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + @@ -469,9 +398,9 @@ module.exports = React.createClass({ "is not blocking requests.", {}, { 'a': (sub) => { - return { sub }; + return + { sub } + ; }, }, ) } @@ -495,19 +424,17 @@ module.exports = React.createClass({ } const serverDetails = this._serverConfigRef = r} + serverConfig={this.props.serverConfig} + onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} />; let nextButton = null; if (PHASES_ENABLED) { + // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? nextButton = + onClick={this.onServerDetailsNextPhaseClick}> {_t("Next")} ; } @@ -547,13 +474,6 @@ module.exports = React.createClass({ onEditServerDetailsClick = this.onEditServerDetailsClick; } - // If the current HS URL is the default HS URL, then we can label it - // with the default HS name (if it exists). - let hsName; - if (this.state.enteredHsUrl === this.props.defaultHsUrl) { - hsName = this.props.defaultServerName; - } - return ( + serverConfig={this.props.serverConfig} + disableSubmit={this.isBusy()} + /> ); }, @@ -595,9 +514,9 @@ module.exports = React.createClass({ const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthBody = sdk.getComponent("auth.AuthBody"); - const loader = this.state.busy ?
: null; + const loader = this.isBusy() ?
: null; - const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; + const errorText = this.state.errorText || this.props.errorText; let errorTextSection; if (errorText) { diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index ed3afede2f..90c607442f 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd +Copyright 2017,2019 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,11 +21,29 @@ import classNames from 'classnames'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; /** * A pure UI component which displays a username/password form. */ -class PasswordLogin extends React.Component { +export default class PasswordLogin extends React.Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, // fn(username, password) + onError: PropTypes.func, + onForgotPasswordClick: PropTypes.func, // fn() + initialUsername: PropTypes.string, + initialPhoneCountry: PropTypes.string, + initialPhoneNumber: PropTypes.string, + initialPassword: PropTypes.string, + onUsernameChanged: PropTypes.func, + onPhoneCountryChanged: PropTypes.func, + onPhoneNumberChanged: PropTypes.func, + onPasswordChanged: PropTypes.func, + loginIncorrect: PropTypes.bool, + disableSubmit: PropTypes.bool, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + }; + static defaultProps = { onError: function() {}, onEditServerDetailsClick: null, @@ -40,13 +58,12 @@ class PasswordLogin extends React.Component { initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, - // This is optional and only set if we used a server name to determine - // the HS URL via `.well-known` discovery. The server name is used - // instead of the HS URL when talking about where to "sign in to". - hsName: null, - hsUrl: "", disableSubmit: false, - } + }; + + static LOGIN_FIELD_EMAIL = "login_field_email"; + static LOGIN_FIELD_MXID = "login_field_mxid"; + static LOGIN_FIELD_PHONE = "login_field_phone"; constructor(props) { super(props); @@ -258,20 +275,14 @@ class PasswordLogin extends React.Component {
; } - let signInToText = _t('Sign in to your Matrix account'); - if (this.props.hsName) { - signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: this.props.hsName, + let signInToText = _t('Sign in to your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + // TODO: TravisR - Use tooltip to underline + signInToText = _t('Sign in to your Matrix account on ', {}, { + 'underlinedServerName': () => {this.props.serverConfig.hsName}, }); - } else { - try { - const parsedHsUrl = new URL(this.props.hsUrl); - signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - // ignore - } } let editLink = null; @@ -353,27 +364,3 @@ class PasswordLogin extends React.Component { ); } } - -PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email"; -PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid"; -PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone"; - -PasswordLogin.propTypes = { - onSubmit: PropTypes.func.isRequired, // fn(username, password) - onError: PropTypes.func, - onForgotPasswordClick: PropTypes.func, // fn() - initialUsername: PropTypes.string, - initialPhoneCountry: PropTypes.string, - initialPhoneNumber: PropTypes.string, - initialPassword: PropTypes.string, - onUsernameChanged: PropTypes.func, - onPhoneCountryChanged: PropTypes.func, - onPhoneNumberChanged: PropTypes.func, - onPasswordChanged: PropTypes.func, - loginIncorrect: PropTypes.bool, - hsName: PropTypes.string, - hsUrl: PropTypes.string, - disableSubmit: PropTypes.bool, -}; - -module.exports = PasswordLogin;