diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index fe1f283009..7a1c39d1f6 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 f2bfe5bc8a..fc3da7bb0a 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 447516a26b..d8d4b0a11b 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 1b76c61071..e3c4d39242 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 07499a3a87..ca75b68e57 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 a6965e06ca..a93492cd41 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 85654d512a..57deee7175 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,18 @@ export default class SoftLogout extends React.Component { domainName, userId, displayName, + loginView: LOGIN_VIEW.LOADING, + + busy: false, + password: "", + errorText: "", }; } + componentDidMount(): void { + this._initLogin(); + } + onClearAll = () => { const ConfirmWipeDeviceDialog = sdk.getComponent('dialogs.ConfirmWipeDeviceDialog'); Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, { @@ -63,10 +87,122 @@ 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}; + } + + return ( +
+

{_t("Enter your password to sign in and regain access to your account.")}

+ {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 +243,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 5d100bcfb0..1dc20f036d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1587,12 +1587,15 @@ "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": "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.", + "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",