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.
This commit is contained in:
Travis Ralston 2019-05-02 23:07:40 -06:00
parent b6e027f5cb
commit 0b1a0c77b7
2 changed files with 86 additions and 180 deletions

View file

@ -25,7 +25,7 @@ import sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import { AutoDiscovery } from "matrix-js-sdk"; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -59,19 +59,14 @@ module.exports = React.createClass({
propTypes: { propTypes: {
onLoggedIn: PropTypes.func.isRequired, 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 // An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl. // went wrong. May be replaced with a different error within the
defaultServerDiscoveryError: PropTypes.string, // 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 // Secondary HS which we try to log into if the user is using
// the default HS but login fails. Useful for migrating to a // the default HS but login fails. Useful for migrating to a
// different homeserver without confusing users. // different homeserver without confusing users.
@ -79,12 +74,13 @@ module.exports = React.createClass({
defaultDeviceDisplayName: PropTypes.string, 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, onRegisterClick: PropTypes.func.isRequired,
// login shouldn't care how password recovery is done.
onForgotPasswordClick: PropTypes.func, onForgotPasswordClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired, onServerConfigChange: PropTypes.func.isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
}, },
getInitialState: function() { getInitialState: function() {
@ -93,9 +89,6 @@ module.exports = React.createClass({
errorText: null, errorText: null,
loginIncorrect: false, loginIncorrect: false,
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
// used for preserving form values when changing homeserver // used for preserving form values when changing homeserver
username: "", username: "",
phoneCountry: null, phoneCountry: null,
@ -105,10 +98,6 @@ module.exports = React.createClass({
phase: PHASE_LOGIN, phase: PHASE_LOGIN,
// The current login flow, such as password, SSO, etc. // The current login flow, such as password, SSO, etc.
currentFlow: "m.login.password", 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) { onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
// Prevent people from submitting their password when homeserver // Prevent people from submitting their password when something isn't right.
// discovery went wrong if (this.isBusy() || this.hasError()) return;
if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
this.setState({ this.setState({
busy: true, busy: true,
@ -164,7 +160,7 @@ module.exports = React.createClass({
const usingEmail = username.indexOf("@") > 0; const usingEmail = username.indexOf("@") > 0;
if (error.httpStatus === 400 && usingEmail) { if (error.httpStatus === 400 && usingEmail) {
errorText = _t('This homeserver does not support login using email address.'); 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( const errorTop = messageForResourceLimitError(
error.data.limit_type, error.data.limit_type,
error.data.admin_contact, { error.data.admin_contact, {
@ -194,11 +190,10 @@ module.exports = React.createClass({
<div> <div>
<div>{ _t('Incorrect username and/or password.') }</div> <div>{ _t('Incorrect username and/or password.') }</div>
<div className="mx_Login_smallError"> <div className="mx_Login_smallError">
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.', {_t(
{ 'Please note you are logging into the %(hs)s server, not matrix.org.',
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''), {hs: this.props.serverConfig.hsName},
}) )}
}
</div> </div>
</div> </div>
); );
@ -235,9 +230,9 @@ module.exports = React.createClass({
onUsernameBlur: function(username) { onUsernameBlur: function(username) {
this.setState({ this.setState({
username: username, username: username,
discoveryError: null, errorText: null,
}); });
if (username[0] === "@") { if (username[0] === "@" && false) { // TODO: TravisR - Restore this
const serverName = username.split(':').slice(1).join(':'); const serverName = username.split(':').slice(1).join(':');
try { try {
// we have to append 'https://' to make the URL constructor happy // we have to append 'https://' to make the URL constructor happy
@ -246,7 +241,7 @@ module.exports = React.createClass({
this._tryWellKnownDiscovery(url.hostname); this._tryWellKnownDiscovery(url.hostname);
} catch (e) { } catch (e) {
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", 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) { onRegisterClick: function(ev) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.props.onRegisterClick(); this.props.onRegisterClick();
}, },
onServerDetailsNextPhaseClick(ev) { async onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation(); 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({ this.setState({
phase: PHASE_LOGIN, 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) { _initLoginLogic: function(hsUrl, isUrl) {
const self = this; const self = this;
hsUrl = hsUrl || this.state.enteredHsUrl; hsUrl = hsUrl || this.props.serverConfig.hsUrl;
isUrl = isUrl || this.state.enteredIsUrl; 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, { const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
@ -378,8 +309,6 @@ module.exports = React.createClass({
this._loginLogic = loginLogic; this._loginLogic = loginLogic;
this.setState({ this.setState({
enteredHsUrl: hsUrl,
enteredIsUrl: isUrl,
busy: true, busy: true,
loginIncorrect: false, loginIncorrect: false,
}); });
@ -445,8 +374,8 @@ module.exports = React.createClass({
if (err.cors === 'rejected') { if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' && if (window.location.protocol === 'https:' &&
(this.state.enteredHsUrl.startsWith("http:") || (this.props.serverConfig.hsUrl.startsWith("http:") ||
!this.state.enteredHsUrl.startsWith("http")) !this.props.serverConfig.hsUrl.startsWith("http"))
) { ) {
errorText = <span> errorText = <span>
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + { _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.", {}, "is not blocking requests.", {},
{ {
'a': (sub) => { 'a': (sub) => {
return <a target="_blank" rel="noopener" return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
href={this.state.enteredHsUrl} { sub }
>{ sub }</a>; </a>;
}, },
}, },
) } ) }
@ -495,19 +424,17 @@ module.exports = React.createClass({
} }
const serverDetails = <ServerConfig const serverDetails = <ServerConfig
customHsUrl={this.state.enteredHsUrl} ref={r => this._serverConfigRef = r}
customIsUrl={this.state.enteredIsUrl} serverConfig={this.props.serverConfig}
defaultHsUrl={this.props.defaultHsUrl} onServerConfigChange={this.props.onServerConfigChange}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={250} delayTimeMs={250}
/>; />;
let nextButton = null; let nextButton = null;
if (PHASES_ENABLED) { if (PHASES_ENABLED) {
// TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button?
nextButton = <AccessibleButton className="mx_Login_submit" nextButton = <AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick} onClick={this.onServerDetailsNextPhaseClick}>
>
{_t("Next")} {_t("Next")}
</AccessibleButton>; </AccessibleButton>;
} }
@ -547,13 +474,6 @@ module.exports = React.createClass({
onEditServerDetailsClick = this.onEditServerDetailsClick; 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 ( return (
<PasswordLogin <PasswordLogin
onSubmit={this.onPasswordLogin} onSubmit={this.onPasswordLogin}
@ -569,9 +489,8 @@ module.exports = React.createClass({
onPhoneNumberBlur={this.onPhoneNumberBlur} onPhoneNumberBlur={this.onPhoneNumberBlur}
onForgotPasswordClick={this.props.onForgotPasswordClick} onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect} loginIncorrect={this.state.loginIncorrect}
hsName={hsName} serverConfig={this.props.serverConfig}
hsUrl={this.state.enteredHsUrl} disableSubmit={this.isBusy()}
disableSubmit={this.state.findingHomeserver}
/> />
); );
}, },
@ -595,9 +514,9 @@ module.exports = React.createClass({
const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthPage = sdk.getComponent("auth.AuthPage");
const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody"); const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null; const loader = this.isBusy() ? <div className="mx_Login_loader"><Loader /></div> : null;
const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; const errorText = this.state.errorText || this.props.errorText;
let errorTextSection; let errorTextSection;
if (errorText) { if (errorText) {

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
/** /**
* A pure UI component which displays a username/password form. * 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 = { static defaultProps = {
onError: function() {}, onError: function() {},
onEditServerDetailsClick: null, onEditServerDetailsClick: null,
@ -40,13 +58,12 @@ class PasswordLogin extends React.Component {
initialPhoneNumber: "", initialPhoneNumber: "",
initialPassword: "", initialPassword: "",
loginIncorrect: false, 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, 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) { constructor(props) {
super(props); super(props);
@ -258,20 +275,14 @@ class PasswordLogin extends React.Component {
</span>; </span>;
} }
let signInToText = _t('Sign in to your Matrix account'); let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
if (this.props.hsName) { serverName: this.props.serverConfig.hsName,
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.hsName,
}); });
} else { if (this.props.serverConfig.hsNameIsDifferent) {
try { // TODO: TravisR - Use tooltip to underline
const parsedHsUrl = new URL(this.props.hsUrl); signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
signInToText = _t('Sign in to your Matrix account on %(serverName)s', { 'underlinedServerName': () => <u>{this.props.serverConfig.hsName}</u>,
serverName: parsedHsUrl.hostname,
}); });
} catch (e) {
// ignore
}
} }
let editLink = null; 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;