diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index fe1f2830096..7a1c39d1f6f 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -79,3 +79,22 @@ limitations under the License. color: $button-danger-disabled-fg-color; background-color: $button-danger-disabled-bg-color; } + +.mx_AccessibleButton_kind_link { + color: $button-link-fg-color; + background-color: $button-link-bg-color; +} + +.mx_AccessibleButton_kind_link.mx_AccessibleButton_disabled { + opacity: 0.4; +} + +.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_link_sm { + padding: 5px 12px; + color: $button-link-fg-color; + background-color: $button-link-bg-color; +} + +.mx_AccessibleButton_kind_link_sm.mx_AccessibleButton_disabled { + opacity: 0.4; +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index f2bfe5bc8a4..fc3da7bb0a2 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -143,6 +143,8 @@ $button-danger-fg-color: #ffffff; $button-danger-bg-color: $notice-primary-color; $button-danger-disabled-fg-color: #ffffff; $button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color +$button-link-fg-color: $accent-color; +$button-link-bg-color: transparent; $room-warning-bg-color: $header-panel-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 447516a26bd..d8d4b0a11bb 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -244,6 +244,8 @@ $button-danger-fg-color: #ffffff; $button-danger-bg-color: $notice-primary-color; $button-danger-disabled-fg-color: #ffffff; $button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color +$button-link-fg-color: $accent-color; +$button-link-bg-color: transparent; // Toggle switch $togglesw-off-color: #c1c9d6; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 1b76c61071f..e3c4d392428 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -340,6 +340,25 @@ export function setLoggedIn(credentials) { return _doSetLoggedIn(credentials, true); } +/** + * Hydrates an existing session by using the credentials provided. This will + * not clear any local storage, unlike setLoggedIn(). + * + * Stops the existing Matrix client (without clearing its data) and starts a + * new one in its place. This additionally starts all other react-sdk services + * which use the new Matrix client. + * + * @param {MatrixClientCreds} credentials The credentials to use + * + * @returns {Promise} promise which resolves to the new MatrixClient once it has been started + */ +export function hydrateSession(credentials) { + stopMatrixClient(); + localStorage.removeItem("mx_soft_logout"); + _isLoggingOut = false; + return _doSetLoggedIn(credentials, false); +} + /** * fires on_logging_in, optionally clears localstorage, persists new credentials * to localstorage, starts the new client. @@ -541,6 +560,7 @@ async function startMatrixClient(startSyncing=true) { await MatrixClientPeg.start(); } else { console.warn("Caller requested only auxiliary services be started"); + await MatrixClientPeg.assign(); } // dispatch that we finished starting up to wire up any other bits diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 07499a3a875..ca75b68e57c 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -120,7 +120,7 @@ class MatrixClientPeg { this._createClient(creds); } - async start() { + async assign() { for (const dbType of ['indexeddb', 'memory']) { try { const promise = this.matrixClient.store.startup(); @@ -131,7 +131,7 @@ class MatrixClientPeg { if (dbType === 'indexeddb') { console.error('Error starting matrixclient store - falling back to memory store', err); this.matrixClient.store = new Matrix.MemoryStore({ - localStorage: global.localStorage, + localStorage: global.localStorage, }); } else { console.error('Failed to start memory store!', err); @@ -172,6 +172,12 @@ class MatrixClientPeg { MatrixActionCreators.start(this.matrixClient); MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient; + return opts; + } + + async start() { + const opts = await this.assign(); + console.log(`MatrixClientPeg: really starting MatrixClient`); await this.get().startClient(opts); console.log(`MatrixClientPeg: MatrixClient started`); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a6965e06cae..a93492cd416 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -450,6 +450,10 @@ export default React.createClass({ startAnyRegistrationFlow(payload); break; case 'start_registration': + if (Lifecycle.isSoftLogout()) { + this._onSoftLogout(); + break; + } // This starts the full registration flow if (payload.screenAfterLogin) { this._screenAfterLogin = payload.screenAfterLogin; @@ -457,6 +461,10 @@ export default React.createClass({ this._startRegistration(payload.params || {}); break; case 'start_login': + if (Lifecycle.isSoftLogout()) { + this._onSoftLogout(); + break; + } if (payload.screenAfterLogin) { this._screenAfterLogin = payload.screenAfterLogin; } diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 85654d512a5..0c589c5bb14 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -23,6 +23,21 @@ import Modal from '../../../Modal'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import SdkConfig from "../../../SdkConfig"; import MatrixClientPeg from "../../../MatrixClientPeg"; +import {sendLoginRequest} from "../../../Login"; + +const LOGIN_VIEW = { + LOADING: 1, + PASSWORD: 2, + CAS: 3, // SSO, but old + SSO: 4, + UNSUPPORTED: 5, +}; + +const FLOWS_TO_VIEWS = { + "m.login.password": LOGIN_VIEW.PASSWORD, + "m.login.cas": LOGIN_VIEW.CAS, + "m.login.sso": LOGIN_VIEW.SSO, +}; export default class SoftLogout extends React.Component { static propTypes = { @@ -48,9 +63,23 @@ export default class SoftLogout extends React.Component { domainName, userId, displayName, + loginView: LOGIN_VIEW.LOADING, + keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount) + + busy: false, + password: "", + errorText: "", }; } + componentDidMount(): void { + this._initLogin(); + + MatrixClientPeg.get().flagAllGroupSessionsForBackup().then(remaining => { + this.setState({keyBackupNeeded: remaining > 0}); + }); + } + onClearAll = () => { const ConfirmWipeDeviceDialog = sdk.getComponent('dialogs.ConfirmWipeDeviceDialog'); Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, { @@ -63,10 +92,129 @@ export default class SoftLogout extends React.Component { }); }; - onLogin = () => { - dis.dispatch({action: 'start_login'}); + async _initLogin() { + // Note: we don't use the existing Login class because it is heavily flow-based. We don't + // care about login flows here, unless it is the single flow we support. + const client = MatrixClientPeg.get(); + const loginViews = (await client.loginFlows()).flows.map(f => FLOWS_TO_VIEWS[f.type]); + + const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED; + this.setState({loginView: chosenView}); + } + + onPasswordChange = (ev) => { + this.setState({password: ev.target.value}); + }; + + onForgotPassword = () => { + dis.dispatch({action: 'start_password_recovery'}); + }; + + onPasswordLogin = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + this.setState({busy: true}); + + const hsUrl = MatrixClientPeg.get().getHomeserverUrl(); + const isUrl = MatrixClientPeg.get().getIdentityServerUrl(); + const loginType = "m.login.password"; + const loginParams = { + identifier: { + type: "m.id.user", + user: MatrixClientPeg.get().getUserId(), + }, + password: this.state.password, + device_id: MatrixClientPeg.get().getDeviceId(), + }; + + let credentials = null; + try { + credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams); + } catch (e) { + let errorText = _t("Failed to re-authenticate due to a homeserver problem"); + if (e.errcode === "M_FORBIDDEN" && (e.httpStatus === 401 || e.httpStatus === 403)) { + errorText = _t("Incorrect password"); + } + + this.setState({ + busy: false, + errorText: errorText, + }); + return; + } + + Lifecycle.hydrateSession(credentials).catch((e) => { + console.error(e); + this.setState({busy: false, errorText: _t("Failed to re-authenticate")}); + }); }; + _renderSignInSection() { + if (this.state.loginView === LOGIN_VIEW.LOADING) { + const Spinner = sdk.getComponent("elements.Spinner"); + return ; + } + + if (this.state.loginView === LOGIN_VIEW.PASSWORD) { + const Field = sdk.getComponent("elements.Field"); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + let error = null; + if (this.state.errorText) { + error = {this.state.errorText}; + } + + let introText = _t("Enter your password to sign in and regain access to your account."); + if (this.state.keyBackupNeeded) { + introText = _t( + "Regain access your account and recover encryption keys stored on this device. " + + "Without them, you won’t be able to read all of your secure messages on any device."); + } + + return ( +
+

{introText}

+ {error} + + + {_t("Sign In")} + + + {_t("Forgotten your password?")} + + + ); + } + + if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) { + // TODO: TravisR - https://github.com/vector-im/riot-web/issues/10238 + return

PLACEHOLDER

; + } + + // Default: assume unsupported + return ( +

+ {_t( + "Cannot re-authenticate with your account. Please contact your " + + "homeserver admin for more information.", + )} +

+ ); + } + render() { const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); @@ -107,14 +255,7 @@ export default class SoftLogout extends React.Component {

{_t("Sign in")}

- {_t( - "Sign in again to regain access to your account, or a different one.", - )} -
-
- - {_t("Sign in")} - + {this._renderSignInSection()}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a8febdf2e43..c83565ac29e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1586,12 +1586,17 @@ "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", "Create your account": "Create your account", + "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", + "Failed to re-authenticate": "Failed to re-authenticate", + "Enter your password to sign in and regain access to your account.": "Enter your password to sign in and regain access to your account.", + "Regain access your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Regain access your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.", + "Forgotten your password?": "Forgotten your password?", + "Cannot re-authenticate with your account. Please contact your homeserver admin for more information.": "Cannot re-authenticate with your account. Please contact your homeserver admin for more information.", "You're signed out": "You're signed out", "Your homeserver (%(domainName)s) admin has signed you out of your account %(displayName)s (%(userId)s).": "Your homeserver (%(domainName)s) admin has signed you out of your account %(displayName)s (%(userId)s).", "I don't want to sign in": "I don't want to sign in", "If this is a shared device, or you don't want to access your account again from it, clear all data stored locally on this device.": "If this is a shared device, or you don't want to access your account again from it, clear all data stored locally on this device.", "Clear all data": "Clear all data", - "Sign in again to regain access to your account, or a different one.": "Sign in again to regain access to your account, or a different one.", "Commands": "Commands", "Results from DuckDuckGo": "Results from DuckDuckGo", "Emoji": "Emoji",