From e2fdeec71aac438a0b671c0b045f14017595d85e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 4 Jun 2019 23:41:59 -0600 Subject: [PATCH 1/7] Fail more softly on homeserver liveliness errors This performs liveliness checks on the auth pages to try and show a friendlier error. Earlier checks in the app startup are expected to not block the app from loading on such failures. See https://github.com/vector-im/riot-web/issues/9828 --- .../structures/auth/ForgotPassword.js | 57 ++++++++++- src/components/structures/auth/Login.js | 81 ++++++++++++--- .../structures/auth/Registration.js | 34 ++++++- .../views/auth/ModularServerConfig.js | 5 +- src/components/views/auth/RegistrationForm.js | 6 +- src/components/views/auth/ServerConfig.js | 5 +- src/i18n/strings/en_EN.json | 4 +- src/utils/AutoDiscoveryUtils.js | 98 ++++++++++++++++--- 8 files changed, 257 insertions(+), 33 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 2ea39ad657b..e235446b160 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -22,7 +22,7 @@ import sdk from '../../../index'; import Modal from "../../../Modal"; import SdkConfig from "../../../SdkConfig"; import PasswordReset from "../../../PasswordReset"; -import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -53,9 +53,40 @@ module.exports = React.createClass({ password: "", password2: "", errorText: null, + + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: true, + serverDeadError: "", }; }, + componentWillMount: function() { + this._checkServerLiveliness(this.props.serverConfig); + }, + + componentWillReceiveProps: async function(newProps) { + if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && + newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; + + // Do a liveliness check on the new URLs + this._checkServerLiveliness(newProps.serverConfig); + }, + + _checkServerLiveliness: async function(serverConfig) { + try { + await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( + serverConfig.hsUrl, + serverConfig.isUrl, + ); + this.setState({serverIsAlive: true}); + } catch (e) { + this.setState(AutoDiscoveryUtils.authComponentStateForError(e)); + } + }, + submitPasswordReset: function(email, password) { this.setState({ phase: PHASE_SENDING_EMAIL, @@ -89,6 +120,8 @@ module.exports = React.createClass({ onSubmitForm: function(ev) { ev.preventDefault(); + if (!this.state.serverIsAlive) return; + if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); } else if (!this.state.password || !this.state.password2) { @@ -173,11 +206,21 @@ module.exports = React.createClass({ const Field = sdk.getComponent('elements.Field'); let errorText = null; - const err = this.state.errorText || this.props.defaultServerDiscoveryError; + const err = this.state.errorText; if (err) { errorText =
{ err }
; } + let serverDeadSection; + if (!this.state.serverIsAlive) { + // TODO: TravisR - Design from Nad + serverDeadSection = ( +
+ {this.state.serverDeadError} +
+ ); + } + let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { serverName: this.props.serverConfig.hsName, }); @@ -207,11 +250,12 @@ module.exports = React.createClass({ } return
+ {errorText} + {serverDeadSection}

{yourMatrixAccountText} {editLink}

- {errorText}
- + {_t('Sign in instead')} diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index b556057cdbe..c38d59caebd 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -94,6 +94,13 @@ module.exports = React.createClass({ phase: PHASE_LOGIN, // The current login flow, such as password, SSO, etc. currentFlow: "m.login.password", + + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: true, + serverDeadError: "", }; }, @@ -233,7 +240,7 @@ module.exports = React.createClass({ username: username, busy: doWellknownLookup, // unset later by the result of onServerConfigChange errorText: null, - canTryLogin: true, + canTryLogin: this.state.serverIsAlive, }); if (doWellknownLookup) { const serverName = username.split(':').slice(1).join(':'); @@ -247,7 +254,19 @@ module.exports = React.createClass({ if (e.translatedMessage) { message = e.translatedMessage; } - this.setState({errorText: message, busy: false, canTryLogin: false}); + + let errorText = message; + let discoveryState = {}; + if (AutoDiscoveryUtils.isLivelinessError(e)) { + errorText = this.state.errorText; + discoveryState = this._stateForDiscoveryError(e); + } + + this.setState({ + busy: false, + errorText, + ...discoveryState, + }); } } }, @@ -272,7 +291,7 @@ module.exports = React.createClass({ } else { this.setState({ errorText: null, - canTryLogin: true, + canTryLogin: this.state.serverIsAlive, }); } }, @@ -297,13 +316,25 @@ module.exports = React.createClass({ }); }, - _initLoginLogic: function(hsUrl, isUrl) { - const self = this; + _stateForDiscoveryError: function(err) { + return { + canTryLogin: false, + ...AutoDiscoveryUtils.authComponentStateForError(err), + }; + }, + + _initLoginLogic: async function(hsUrl, isUrl) { hsUrl = hsUrl || this.props.serverConfig.hsUrl; isUrl = isUrl || this.props.serverConfig.isUrl; - // TODO: TravisR - Only use this if the homeserver is the default homeserver - const fallbackHsUrl = this.props.fallbackHsUrl; + let isDefaultServer = false; + if (this.props.serverConfig.isDefault + && hsUrl === this.props.serverConfig.hsUrl + && isUrl === this.props.serverConfig.isUrl) { + isDefaultServer = true; + } + + const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl : null; const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, @@ -315,6 +346,19 @@ module.exports = React.createClass({ loginIncorrect: false, }); + // Do a quick liveliness check on the URLs + try { + await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + this.setState({serverIsAlive: true, errorText: "", canTryLogin: true}); + } catch (e) { + const discoveryState = this._stateForDiscoveryError(e); + this.setState({ + busy: false, + ...discoveryState, + }); + return; // Server is dead - do not continue. + } + loginLogic.getFlows().then((flows) => { // look for a flow where we understand all of the steps. for (let i = 0; i < flows.length; i++ ) { @@ -339,14 +383,14 @@ module.exports = React.createClass({ "supported by this client.", ), }); - }, function(err) { - self.setState({ - errorText: self._errorTextFromError(err), + }, (err) => { + this.setState({ + errorText: this._errorTextFromError(err), loginIncorrect: false, canTryLogin: false, }); - }).finally(function() { - self.setState({ + }).finally(() => { + this.setState({ busy: false, }); }).done(); @@ -485,7 +529,7 @@ module.exports = React.createClass({ onForgotPasswordClick={this.props.onForgotPasswordClick} loginIncorrect={this.state.loginIncorrect} serverConfig={this.props.serverConfig} - disableSubmit={this.isBusy()} + disableSubmit={this.isBusy() || !this.state.serverIsAlive} /> ); }, @@ -522,6 +566,16 @@ module.exports = React.createClass({ ); } + let serverDeadSection; + if (!this.state.serverIsAlive) { + // TODO: TravisR - Design from Nad + serverDeadSection = ( +
+ {this.state.serverDeadError} +
+ ); + } + return ( @@ -531,6 +585,7 @@ module.exports = React.createClass({ {loader} { errorTextSection } + { serverDeadSection } { this.renderServerComponent() } { this.renderLoginComponentForStep() }
diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6e4f076091c..bf46c7520d4 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -26,7 +26,7 @@ import { _t, _td } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import * as ServerType from '../../views/auth/ServerTypeSelector'; -import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -79,6 +79,13 @@ module.exports = React.createClass({ // Phase of the overall registration dialog. phase: PHASE_REGISTRATION, flows: null, + + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: true, + serverDeadError: "", }; }, @@ -152,6 +159,19 @@ module.exports = React.createClass({ errorText: null, }); if (!serverConfig) serverConfig = this.props.serverConfig; + + // Do a liveliness check on the URLs + try { + await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( + serverConfig.hsUrl, + serverConfig.isUrl, + ); + this.setState({serverIsAlive: true}); + } catch (e) { + this.setState(AutoDiscoveryUtils.authComponentStateForError(e)); + return; // Server is dead - do not continue. + } + const {hsUrl, isUrl} = serverConfig; this._matrixClient = Matrix.createClient({ baseUrl: hsUrl, @@ -447,6 +467,7 @@ module.exports = React.createClass({ onEditServerDetailsClick={onEditServerDetailsClick} flows={this.state.flows} serverConfig={this.props.serverConfig} + canSubmit={this.state.serverIsAlive} />; } }, @@ -462,6 +483,16 @@ module.exports = React.createClass({ errorText =
{ err }
; } + let serverDeadSection; + if (!this.state.serverIsAlive) { + // TODO: TravisR - Design from Nad + serverDeadSection = ( +
+ {this.state.serverDeadError} +
+ ); + } + const signIn =
{ _t('Sign in instead') } ; @@ -480,6 +511,7 @@ module.exports = React.createClass({

{ _t('Create your account') }

{ errorText } + { serverDeadSection } { this.renderServerComponent() } { this.renderRegisterComponent() } { goBack } diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index 5a3bc23596f..b5af58adf19 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -108,6 +108,8 @@ export default class ModularServerConfig extends React.PureComponent { busy: false, errorText: message, }); + + return null; } } @@ -132,7 +134,8 @@ export default class ModularServerConfig extends React.PureComponent { onSubmit = async (ev) => { ev.preventDefault(); ev.stopPropagation(); - await this.validateServer(); + const result = await this.validateServer(); + if (!result) return; // Do not continue. if (this.props.onAfterSubmit) { this.props.onAfterSubmit(); diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index b1af6ea42cf..ccbfc507c60 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -53,11 +53,13 @@ module.exports = React.createClass({ onEditServerDetailsClick: PropTypes.func, flows: PropTypes.arrayOf(PropTypes.object).isRequired, serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + canSubmit: PropTypes.bool, }, getDefaultProps: function() { return { onValidationChange: console.error, + canSubmit: true, }; }, @@ -80,6 +82,8 @@ module.exports = React.createClass({ onSubmit: async function(ev) { ev.preventDefault(); + if (!this.props.canSubmit) return; + const allFieldsValid = await this.verifyFieldsBeforeSubmit(); if (!allFieldsValid) { return; @@ -540,7 +544,7 @@ module.exports = React.createClass({ } const registerButton = ( - + ); return ( diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index 3967f49f189..8d2e2e7bbae 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -109,6 +109,8 @@ export default class ServerConfig extends React.PureComponent { busy: false, errorText: message, }); + + return null; } } @@ -137,7 +139,8 @@ export default class ServerConfig extends React.PureComponent { onSubmit = async (ev) => { ev.preventDefault(); ev.stopPropagation(); - await this.validateServer(); + const result = await this.validateServer(); + if (!result) return; // Do not continue. if (this.props.onAfterSubmit) { this.props.onAfterSubmit(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7673b5f6ab8..2b5efc38eeb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -249,8 +249,11 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", + "Server failed liveliness check": "Server failed liveliness check", + "Server failed syntax check": "Server failed syntax check", "No homeserver URL provided": "No homeserver URL provided", "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", + "Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -304,7 +307,6 @@ "Custom user status messages": "Custom user status messages", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", - "Custom Notification Sounds": "Custom Notification Sounds", "Edit messages after they have been sent (refresh to apply changes)": "Edit messages after they have been sent (refresh to apply changes)", "React to messages with emoji (refresh to apply changes)": "React to messages with emoji (refresh to apply changes)", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 08500393441..405215c2374 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -15,10 +15,15 @@ limitations under the License. */ import {AutoDiscovery} from "matrix-js-sdk"; -import {_td, newTranslatableError} from "../languageHandler"; +import {_t, _td, newTranslatableError} from "../languageHandler"; import {makeType} from "./TypeUtils"; import SdkConfig from "../SdkConfig"; +const LIVLINESS_DISCOVERY_ERRORS = [ + AutoDiscovery.ERROR_INVALID_HOMESERVER, + AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, +]; + export class ValidatedServerConfig { hsUrl: string; hsName: string; @@ -31,7 +36,52 @@ export class ValidatedServerConfig { } export default class AutoDiscoveryUtils { - static async validateServerConfigWithStaticUrls(homeserverUrl: string, identityUrl: string): ValidatedServerConfig { + /** + * Checks if a given error or error message is considered an error + * relating to the liveliness of the server. Must be an error returned + * from this AutoDiscoveryUtils class. + * @param {string|Error} error The error to check + * @returns {boolean} True if the error is a liveliness error. + */ + static isLivelinessError(error: string|Error): boolean { + if (!error) return false; + return !!LIVLINESS_DISCOVERY_ERRORS.find(e => e === error || e === error.message); + } + + /** + * Gets the common state for auth components (login, registration, forgot + * password) for a given validation error. + * @param {Error} err The error encountered. + * @returns {{serverDeadError: (string|*), serverIsAlive: boolean}} The state + * for the component, given the error. + */ + static authComponentStateForError(err: Error): {serverIsAlive: boolean, serverDeadError: string} { + if (AutoDiscoveryUtils.isLivelinessError(err)) { + // TODO: TravisR - Copy from Nad + return { + serverIsAlive: false, + serverDeadError: _t("Server failed liveliness check"), + }; + } else { + // TODO: TravisR - Copy from Nad + return { + serverIsAlive: false, + serverDeadError: _t("Server failed syntax check"), + }; + } + } + + /** + * Validates a server configuration, using a pair of URLs as input. + * @param {string} homeserverUrl The homeserver URL. + * @param {string} identityUrl The identity server URL. + * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will + * not be raised. + * @returns {Promise} Resolves to the validated configuration. + */ + static async validateServerConfigWithStaticUrls( + homeserverUrl: string, identityUrl: string, syntaxOnly = false): ValidatedServerConfig { + if (!homeserverUrl) { throw newTranslatableError(_td("No homeserver URL provided")); } @@ -50,15 +100,33 @@ export default class AutoDiscoveryUtils { const url = new URL(homeserverUrl); const serverName = url.hostname; - return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result, syntaxOnly); } - static async validateServerName(serverName: string): ValidatedServerConfig { + /** + * Validates a server configuration, using a homeserver domain name as input. + * @param {string} serverName The homeserver domain name (eg: "matrix.org") to validate. + * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will + * not be raised. + * @returns {Promise} Resolves to the validated configuration. + */ + static async validateServerName(serverName: string, syntaxOnly=false): ValidatedServerConfig { const result = await AutoDiscovery.findClientConfig(serverName); return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); } - static buildValidatedConfigFromDiscovery(serverName: string, discoveryResult): ValidatedServerConfig { + /** + * Validates a server configuration, using a pre-calculated AutoDiscovery result as + * input. + * @param {string} serverName The domain name the AutoDiscovery result is for. + * @param {*} discoveryResult The AutoDiscovery result. + * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will + * not be raised. + * @returns {Promise} Resolves to the validated configuration. + */ + static buildValidatedConfigFromDiscovery( + serverName: string, discoveryResult, syntaxOnly=false): ValidatedServerConfig { + if (!discoveryResult || !discoveryResult["m.homeserver"]) { // This shouldn't happen without major misconfiguration, so we'll log a bit of information // in the log so we can find this bit of codee but otherwise tell teh user "it broke". @@ -68,19 +136,27 @@ export default class AutoDiscoveryUtils { const hsResult = discoveryResult['m.homeserver']; if (hsResult.state !== AutoDiscovery.SUCCESS) { - if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error) !== -1) { - throw newTranslatableError(hsResult.error); - } - throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + console.error("Error processing homeserver config:", hsResult); + if (!syntaxOnly || !AutoDiscoveryUtils.isLivelinessError(hsResult.error)) { + if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error) !== -1) { + throw newTranslatableError(hsResult.error); + } + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } // else the error is not related to syntax - continue anyways. } const isResult = discoveryResult['m.identity_server']; - let preferredIdentityUrl = "https://vector.im"; + let preferredIdentityUrl = "https://vector.im"; // We already know this is an IS, so don't validate it. if (isResult && isResult.state === AutoDiscovery.SUCCESS) { preferredIdentityUrl = isResult["base_url"]; } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { console.error("Error determining preferred identity server URL:", isResult); - throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + if (!syntaxOnly || !AutoDiscoveryUtils.isLivelinessError(isResult.error)) { + if (AutoDiscovery.ALL_ERRORS.indexOf(isResult.error) !== -1) { + throw newTranslatableError(isResult.error); + } + throw newTranslatableError(_td("Unexpected error resolving identity server configuration")); + } // else the error is not related to syntax - continue anyways. } const preferredHomeserverUrl = hsResult["base_url"]; From 10f4d6b4193468257425ea86a1197cc8ac0790de Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 4 Jun 2019 23:52:40 -0600 Subject: [PATCH 2/7] Appease the linter --- src/utils/AutoDiscoveryUtils.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 405215c2374..26596bdbb8c 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -81,7 +81,6 @@ export default class AutoDiscoveryUtils { */ static async validateServerConfigWithStaticUrls( homeserverUrl: string, identityUrl: string, syntaxOnly = false): ValidatedServerConfig { - if (!homeserverUrl) { throw newTranslatableError(_td("No homeserver URL provided")); } @@ -126,7 +125,6 @@ export default class AutoDiscoveryUtils { */ static buildValidatedConfigFromDiscovery( serverName: string, discoveryResult, syntaxOnly=false): ValidatedServerConfig { - if (!discoveryResult || !discoveryResult["m.homeserver"]) { // This shouldn't happen without major misconfiguration, so we'll log a bit of information // in the log so we can find this bit of codee but otherwise tell teh user "it broke". From ace1bde23854d4ed64c9522c290bbe4bbf3ace89 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Jun 2019 11:32:02 -0600 Subject: [PATCH 3/7] Implement design and copy for errors --- res/css/structures/_GenericErrorPage.scss | 2 +- res/css/structures/auth/_Login.scss | 5 +++ src/components/structures/GenericErrorPage.js | 8 +--- .../structures/auth/ForgotPassword.js | 3 +- src/components/structures/auth/Login.js | 3 +- .../structures/auth/Registration.js | 3 +- src/i18n/strings/en_EN.json | 8 ++-- src/utils/AutoDiscoveryUtils.js | 39 +++++++++++++------ 8 files changed, 42 insertions(+), 29 deletions(-) diff --git a/res/css/structures/_GenericErrorPage.scss b/res/css/structures/_GenericErrorPage.scss index 44ea73444e4..2b9e9f5e7d3 100644 --- a/res/css/structures/_GenericErrorPage.scss +++ b/res/css/structures/_GenericErrorPage.scss @@ -12,7 +12,7 @@ right: 0; margin: auto; width: 500px; - height: 200px; + height: 125px; border: 1px solid #f22; padding: 10px 10px 20px; background-color: #fcc; diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 4eff5c33e4e..9ba46c09ab2 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -62,6 +62,11 @@ limitations under the License. margin-bottom: 12px; } +.mx_Login_error.mx_Login_serverError { + text-align: left; + font-weight: normal; +} + .mx_Login_type_container { display: flex; align-items: center; diff --git a/src/components/structures/GenericErrorPage.js b/src/components/structures/GenericErrorPage.js index 3d8e68cea78..ab7d4f93111 100644 --- a/src/components/structures/GenericErrorPage.js +++ b/src/components/structures/GenericErrorPage.js @@ -16,22 +16,18 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t} from "../../languageHandler"; export default class GenericErrorPage extends React.PureComponent { static propTypes = { + title: PropTypes.object.isRequired, // jsx for title message: PropTypes.object.isRequired, // jsx to display }; render() { return
-

{_t("Error loading Riot")}

+

{this.props.title}

{this.props.message}

-

{_t( - "If this is unexpected, please contact your system administrator " + - "or technical support representative.", - )}

; } diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index e235446b160..2daa9e2a41d 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -213,9 +213,8 @@ module.exports = React.createClass({ let serverDeadSection; if (!this.state.serverIsAlive) { - // TODO: TravisR - Design from Nad serverDeadSection = ( -
+
{this.state.serverDeadError}
); diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index c38d59caebd..3594a005132 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -568,9 +568,8 @@ module.exports = React.createClass({ let serverDeadSection; if (!this.state.serverIsAlive) { - // TODO: TravisR - Design from Nad serverDeadSection = ( -
+
{this.state.serverDeadError}
); diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index bf46c7520d4..cee809de133 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -485,9 +485,8 @@ module.exports = React.createClass({ let serverDeadSection; if (!this.state.serverIsAlive) { - // TODO: TravisR - Design from Nad serverDeadSection = ( -
+
{this.state.serverDeadError}
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2b5efc38eeb..1e883075332 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -249,8 +249,10 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", - "Server failed liveliness check": "Server failed liveliness check", - "Server failed syntax check": "Server failed syntax check", + "Cannot reach homeserver": "Cannot reach homeserver", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin", + "Your Riot is misconfigured": "Your Riot is misconfigured", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Ask your Riot admin to check your config for incorrect or duplicate entries.", "No homeserver URL provided": "No homeserver URL provided", "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", "Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration", @@ -1387,8 +1389,6 @@ "You must register to use this functionality": "You must register to use this functionality", "You must join the room to see its files": "You must join the room to see its files", "There are no visible files in this room": "There are no visible files in this room", - "Error loading Riot": "Error loading Riot", - "If this is unexpected, please contact your system administrator or technical support representative.": "If this is unexpected, please contact your system administrator or technical support representative.", "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n": "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n", "Add rooms to the community summary": "Add rooms to the community summary", "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 26596bdbb8c..8b13fff3684 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import {AutoDiscovery} from "matrix-js-sdk"; import {_t, _td, newTranslatableError} from "../languageHandler"; import {makeType} from "./TypeUtils"; @@ -56,19 +57,33 @@ export default class AutoDiscoveryUtils { * for the component, given the error. */ static authComponentStateForError(err: Error): {serverIsAlive: boolean, serverDeadError: string} { - if (AutoDiscoveryUtils.isLivelinessError(err)) { - // TODO: TravisR - Copy from Nad - return { - serverIsAlive: false, - serverDeadError: _t("Server failed liveliness check"), - }; - } else { - // TODO: TravisR - Copy from Nad - return { - serverIsAlive: false, - serverDeadError: _t("Server failed syntax check"), - }; + let title = _t("Cannot reach homeserver"); + let body = _t("Ensure you have a stable internet connection, or get in touch with the server admin"); + if (!AutoDiscoveryUtils.isLivelinessError(err)) { + title = _t("Your Riot is misconfigured"); + body = _t( + "Ask your Riot admin to check your config for incorrect or duplicate entries.", + {}, { + a: (sub) => { + return {sub}; + } + }, + ); } + + return { + serverIsAlive: false, + serverDeadError: ( +
+ {title} +
{body}
+
+ ), + }; } /** From c15ee1a829911bec7cd5e8cfb91abae236896b52 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Jun 2019 12:15:37 -0600 Subject: [PATCH 4/7] Appease the linter --- src/utils/AutoDiscoveryUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 8b13fff3684..eed0ffc08c6 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -70,7 +70,7 @@ export default class AutoDiscoveryUtils { target="_blank" rel="noopener" >{sub}; - } + }, }, ); } From d59ad605a64b9066dc9f511893ee9c52649f6dbb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 6 Jun 2019 12:18:41 -0600 Subject: [PATCH 5/7] Allow the login form to be submitted, and clarify other things --- .../structures/auth/ForgotPassword.js | 2 +- src/components/structures/auth/Login.js | 23 +++++++------------ src/utils/AutoDiscoveryUtils.js | 6 ++++- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 2daa9e2a41d..8f660f92982 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -67,7 +67,7 @@ module.exports = React.createClass({ this._checkServerLiveliness(this.props.serverConfig); }, - componentWillReceiveProps: async function(newProps) { + componentWillReceiveProps: function(newProps) { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 3594a005132..9abf188ac32 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -145,7 +145,7 @@ module.exports = React.createClass({ onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { // Prevent people from submitting their password when something isn't right. - if (this.isBusy() || !this.state.canTryLogin) return; + if (this.isBusy()) return; this.setState({ busy: true, @@ -156,6 +156,7 @@ module.exports = React.createClass({ this._loginLogic.loginViaPassword( username, phoneCountry, phoneNumber, password, ).then((data) => { + this.setState({serverIsAlive: true}); // it must be, we logged in. this.props.onLoggedIn(data); }, (error) => { if (this._unmounted) { @@ -240,7 +241,7 @@ module.exports = React.createClass({ username: username, busy: doWellknownLookup, // unset later by the result of onServerConfigChange errorText: null, - canTryLogin: this.state.serverIsAlive, + canTryLogin: true, }); if (doWellknownLookup) { const serverName = username.split(':').slice(1).join(':'); @@ -259,7 +260,7 @@ module.exports = React.createClass({ let discoveryState = {}; if (AutoDiscoveryUtils.isLivelinessError(e)) { errorText = this.state.errorText; - discoveryState = this._stateForDiscoveryError(e); + discoveryState = AutoDiscoveryUtils.authComponentStateForError(e); } this.setState({ @@ -291,7 +292,7 @@ module.exports = React.createClass({ } else { this.setState({ errorText: null, - canTryLogin: this.state.serverIsAlive, + canTryLogin: true, }); } }, @@ -316,13 +317,6 @@ module.exports = React.createClass({ }); }, - _stateForDiscoveryError: function(err) { - return { - canTryLogin: false, - ...AutoDiscoveryUtils.authComponentStateForError(err), - }; - }, - _initLoginLogic: async function(hsUrl, isUrl) { hsUrl = hsUrl || this.props.serverConfig.hsUrl; isUrl = isUrl || this.props.serverConfig.isUrl; @@ -349,12 +343,11 @@ module.exports = React.createClass({ // Do a quick liveliness check on the URLs try { await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); - this.setState({serverIsAlive: true, errorText: "", canTryLogin: true}); + this.setState({serverIsAlive: true, errorText: ""}); } catch (e) { - const discoveryState = this._stateForDiscoveryError(e); this.setState({ busy: false, - ...discoveryState, + ...AutoDiscoveryUtils.authComponentStateForError(e), }); return; // Server is dead - do not continue. } @@ -529,7 +522,7 @@ module.exports = React.createClass({ onForgotPasswordClick={this.props.onForgotPasswordClick} loginIncorrect={this.state.loginIncorrect} serverConfig={this.props.serverConfig} - disableSubmit={this.isBusy() || !this.state.serverIsAlive} + disableSubmit={this.isBusy()} /> ); }, diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index eed0ffc08c6..55cf84b9ad4 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -158,8 +158,12 @@ export default class AutoDiscoveryUtils { } // else the error is not related to syntax - continue anyways. } + // Note: In the cases where we rely on this pre-populated "https://vector.im" (namely + // lack of identity server provided by the discovery method), we intentionally do not + // validate it. We already know the IS is an IS, and this helps some off-the-grid usage + // of Riot. + let preferredIdentityUrl = "https://vector.im"; const isResult = discoveryResult['m.identity_server']; - let preferredIdentityUrl = "https://vector.im"; // We already know this is an IS, so don't validate it. if (isResult && isResult.state === AutoDiscovery.SUCCESS) { preferredIdentityUrl = isResult["base_url"]; } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { From 120123bcb1e5ed1cec8701d3294318e41ff16b48 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Jun 2019 07:27:15 -0600 Subject: [PATCH 6/7] Allow submit on forgot password page --- src/components/structures/auth/ForgotPassword.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 8f660f92982..62a67c7ec55 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -82,8 +82,10 @@ module.exports = React.createClass({ serverConfig.isUrl, ); this.setState({serverIsAlive: true}); + return true; } catch (e) { this.setState(AutoDiscoveryUtils.authComponentStateForError(e)); + return false; } }, @@ -117,10 +119,11 @@ module.exports = React.createClass({ }); }, - onSubmitForm: function(ev) { + onSubmitForm: async function(ev) { ev.preventDefault(); - if (!this.state.serverIsAlive) return; + const shouldBlockSubmit = await this._checkServerLiveliness(this.props.serverConfig); + if (shouldBlockSubmit) return; if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); @@ -293,7 +296,6 @@ module.exports = React.createClass({ className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} - disabled={!this.state.serverIsAlive} /> From 59ba5fe62a7e1b6caaecf746e58d0a37d01a92ee Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Jun 2019 07:36:46 -0600 Subject: [PATCH 7/7] Don't block submit if the server is dead But still check so we can clear any errors, maybe --- src/components/structures/auth/ForgotPassword.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 62a67c7ec55..3380ea6dacf 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -82,10 +82,8 @@ module.exports = React.createClass({ serverConfig.isUrl, ); this.setState({serverIsAlive: true}); - return true; } catch (e) { this.setState(AutoDiscoveryUtils.authComponentStateForError(e)); - return false; } }, @@ -122,8 +120,7 @@ module.exports = React.createClass({ onSubmitForm: async function(ev) { ev.preventDefault(); - const shouldBlockSubmit = await this._checkServerLiveliness(this.props.serverConfig); - if (shouldBlockSubmit) return; + await this._checkServerLiveliness(this.props.serverConfig); if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.'));