Implement basic soft logout handling
Fixes https://github.com/vector-im/riot-web/issues/10235 CSS and copy are left as an exercise for a later iteration. Login page handling is left for https://github.com/vector-im/riot-web/issues/10236 This implementation reuses as much of the Lifecycle flow as it can without causing problems. Most importantly, it requires https://github.com/matrix-org/matrix-js-sdk/pull/975 to be able to detect a soft logout and react to it. When it comes time to starting/stopping the Lifecycle, additional parameters are provided so that the auxiliary services can (re)start themselves without the client starting to sync.
This commit is contained in:
parent
668d24111c
commit
42e6287bdb
12 changed files with 286 additions and 8 deletions
|
@ -36,6 +36,7 @@ export default class BasePlatform {
|
|||
|
||||
_onAction(payload: Object) {
|
||||
switch (payload.action) {
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out':
|
||||
this.setNotificationCount(0);
|
||||
break;
|
||||
|
|
|
@ -352,11 +352,14 @@ export function setLoggedIn(credentials) {
|
|||
async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
credentials.guest = Boolean(credentials.guest);
|
||||
|
||||
const softLogout = isSoftLogout();
|
||||
|
||||
console.log(
|
||||
"setLoggedIn: mxid: " + credentials.userId +
|
||||
" deviceId: " + credentials.deviceId +
|
||||
" guest: " + credentials.guest +
|
||||
" hs: " + credentials.homeserverUrl,
|
||||
" hs: " + credentials.homeserverUrl +
|
||||
" softLogout: " + softLogout,
|
||||
);
|
||||
|
||||
// This is dispatched to indicate that the user is still in the process of logging in
|
||||
|
@ -414,7 +417,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
|||
|
||||
dis.dispatch({ action: 'on_logged_in' });
|
||||
|
||||
await startMatrixClient();
|
||||
await startMatrixClient(/*startSyncing=*/!softLogout);
|
||||
return MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
|
@ -487,6 +490,25 @@ export function logout() {
|
|||
).done();
|
||||
}
|
||||
|
||||
export function softLogout() {
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
|
||||
// Track that we've detected and trapped a soft logout. This helps prevent other
|
||||
// parts of the app from starting if there's no point (ie: don't sync if we've
|
||||
// been soft logged out, despite having credentials and data for a MatrixClient).
|
||||
localStorage.setItem("mx_soft_logout", "true");
|
||||
|
||||
_isLoggingOut = true; // to avoid repeated flags
|
||||
stopMatrixClient(/*unsetClient=*/false);
|
||||
dis.dispatch({action: 'on_client_not_viable'}); // generic version of on_logged_out
|
||||
|
||||
// DO NOT CALL LOGOUT. A soft logout preserves data, logout does not.
|
||||
}
|
||||
|
||||
export function isSoftLogout() {
|
||||
return localStorage.getItem("mx_soft_logout") === "true";
|
||||
}
|
||||
|
||||
export function isLoggingOut() {
|
||||
return _isLoggingOut;
|
||||
}
|
||||
|
@ -494,8 +516,10 @@ export function isLoggingOut() {
|
|||
/**
|
||||
* Starts the matrix client and all other react-sdk services that
|
||||
* listen for events while a session is logged in.
|
||||
* @param {boolean} startSyncing True (default) to actually start
|
||||
* syncing the client.
|
||||
*/
|
||||
async function startMatrixClient() {
|
||||
async function startMatrixClient(startSyncing=true) {
|
||||
console.log(`Lifecycle: Starting MatrixClient`);
|
||||
|
||||
// dispatch this before starting the matrix client: it's used
|
||||
|
@ -513,11 +537,19 @@ async function startMatrixClient() {
|
|||
DMRoomMap.makeShared().start();
|
||||
ActiveWidgetStore.start();
|
||||
|
||||
if (startSyncing) {
|
||||
await MatrixClientPeg.start();
|
||||
} else {
|
||||
console.warn("Caller requested only auxiliary services be started");
|
||||
}
|
||||
|
||||
// dispatch that we finished starting up to wire up any other bits
|
||||
// of the matrix client that cannot be set prior to starting up.
|
||||
dis.dispatch({action: 'client_started'});
|
||||
|
||||
if (isSoftLogout()) {
|
||||
softLogout();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -551,8 +583,10 @@ function _clearStorage() {
|
|||
|
||||
/**
|
||||
* Stop all the background processes related to the current client.
|
||||
* @param {boolean} unsetClient True (default) to abandon the client
|
||||
* on MatrixClientPeg after stopping.
|
||||
*/
|
||||
export function stopMatrixClient() {
|
||||
export function stopMatrixClient(unsetClient=true) {
|
||||
Notifier.stop();
|
||||
UserActivity.sharedInstance().stop();
|
||||
TypingStore.sharedInstance().reset();
|
||||
|
@ -563,6 +597,9 @@ export function stopMatrixClient() {
|
|||
if (cli) {
|
||||
cli.stopClient();
|
||||
cli.removeAllListeners();
|
||||
|
||||
if (unsetClient) {
|
||||
MatrixClientPeg.unset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,6 +90,10 @@ const VIEWS = {
|
|||
|
||||
// we are logged in with an active matrix client.
|
||||
LOGGED_IN: 7,
|
||||
|
||||
// We are logged out (invalid token) but have our local state again. The user
|
||||
// should log back in to rehydrate the client.
|
||||
SOFT_LOGOUT: 8,
|
||||
};
|
||||
|
||||
// Actions that are redirected through the onboarding process prior to being
|
||||
|
@ -432,6 +436,7 @@ export default React.createClass({
|
|||
|
||||
switch (payload.action) {
|
||||
case 'logout':
|
||||
console.log(payload);
|
||||
Lifecycle.logout();
|
||||
break;
|
||||
case 'require_registration':
|
||||
|
@ -615,7 +620,12 @@ export default React.createClass({
|
|||
});
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
if (!Lifecycle.isSoftLogout()) {
|
||||
this._onLoggedIn();
|
||||
}
|
||||
break;
|
||||
case 'on_client_not_viable':
|
||||
this._onSoftLogout();
|
||||
break;
|
||||
case 'on_logged_out':
|
||||
this._onLoggedOut();
|
||||
|
@ -1258,6 +1268,22 @@ export default React.createClass({
|
|||
this._setPageSubtitle();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the session is softly logged out
|
||||
*/
|
||||
_onSoftLogout: function() {
|
||||
this.notifyNewScreen('soft_logout');
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.SOFT_LOGOUT,
|
||||
ready: false,
|
||||
collapseLhs: false,
|
||||
collapsedRhs: false,
|
||||
currentRoomId: null,
|
||||
page_type: PageTypes.RoomDirectory,
|
||||
});
|
||||
this._setPageSubtitle();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called just before the matrix client is started
|
||||
* (useful for setting listeners)
|
||||
|
@ -1337,8 +1363,16 @@ export default React.createClass({
|
|||
call: call,
|
||||
}, true);
|
||||
});
|
||||
cli.on('Session.logged_out', function(call) {
|
||||
cli.on('Session.logged_out', function(errObj) {
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
|
||||
if (errObj.httpStatus === 401 && errObj.data && errObj.data['soft_logout']) {
|
||||
console.warn("Soft logout issued by server - avoiding data deletion");
|
||||
Lifecycle.softLogout();
|
||||
dis.dispatch({actions: 'soft_logout'});
|
||||
return;
|
||||
}
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Signed out', '', ErrorDialog, {
|
||||
title: _t('Signed Out'),
|
||||
|
@ -1908,6 +1942,13 @@ export default React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.SOFT_LOGOUT) {
|
||||
const SoftLogout = sdk.getComponent('structures.auth.SoftLogout');
|
||||
return (
|
||||
<SoftLogout />
|
||||
);
|
||||
}
|
||||
|
||||
console.error(`Unknown view ${this.state.view}`);
|
||||
},
|
||||
});
|
||||
|
|
123
src/components/structures/auth/SoftLogout.js
Normal file
123
src/components/structures/auth/SoftLogout.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import Modal from '../../../Modal';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
export default class SoftLogout extends React.Component {
|
||||
static propTypes = {
|
||||
// Nothing.
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const defaultServerConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
|
||||
const hsUrl = MatrixClientPeg.get().getHomeserverUrl();
|
||||
const domainName = hsUrl === defaultServerConfig.hsUrl
|
||||
? defaultServerConfig.hsName
|
||||
: MatrixClientPeg.get().getHomeServerName();
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const user = MatrixClientPeg.get().getUser(userId);
|
||||
|
||||
const displayName = user ? user.displayName : userId.substring(1).split(':')[0];
|
||||
|
||||
this.state = {
|
||||
domainName,
|
||||
userId,
|
||||
displayName,
|
||||
};
|
||||
}
|
||||
|
||||
onClearAll = () => {
|
||||
const ConfirmWipeDeviceDialog = sdk.getComponent('dialogs.ConfirmWipeDeviceDialog');
|
||||
Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, {
|
||||
onFinished: (wipeData) => {
|
||||
if (!wipeData) return;
|
||||
|
||||
console.log("Clearing data from soft-logged-out device");
|
||||
Lifecycle.logout();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onLogin = () => {
|
||||
dis.dispatch({action: 'start_login'});
|
||||
};
|
||||
|
||||
render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h2>
|
||||
{_t("You're signed out")}
|
||||
</h2>
|
||||
<div>
|
||||
{_t(
|
||||
"Your homeserver (%(domainName)s) admin has signed you out of your " +
|
||||
"account %(displayName)s (%(userId)s).",
|
||||
{
|
||||
domainName: this.state.domainName,
|
||||
displayName: this.state.displayName,
|
||||
userId: this.state.userId,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3>{_t("I don't want to sign in")}</h3>
|
||||
<div>
|
||||
{_t(
|
||||
"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.",
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<AccessibleButton onClick={this.onClearAll} kind="primary">
|
||||
{_t("Clear all data")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<h3>{_t("Sign in")}</h3>
|
||||
<div>
|
||||
{_t(
|
||||
"Sign in again to regain access to your account, or a different one.",
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<AccessibleButton onClick={this.onLogin} kind="primary">
|
||||
{_t("Sign in")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
);
|
||||
}
|
||||
}
|
60
src/components/views/dialogs/ConfirmWipeDeviceDialog.js
Normal file
60
src/components/views/dialogs/ConfirmWipeDeviceDialog.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import sdk from "../../../index";
|
||||
|
||||
export default class ConfirmWipeDeviceDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onConfirm = () => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onDecline = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_ConfirmWipeDeviceDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Clear all data on this device?")}>
|
||||
<div className='mx_ConfirmWipeDeviceDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"Deleting all data from this device is permanent. Encrypted messages will be lost " +
|
||||
"unless their keys have been backed up.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Delete everything")}
|
||||
onPrimaryButtonClick={this._onConfirm}
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this._onDecline}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -147,6 +147,7 @@
|
|||
"Changes your display nickname": "Changes your display nickname",
|
||||
"Changes your display nickname in the current room only": "Changes your display nickname in the current room only",
|
||||
"Changes your avatar in this current room only": "Changes your avatar in this current room only",
|
||||
"Changes your avatar in all rooms": "Changes your avatar in all rooms",
|
||||
"Changes colour scheme of current room": "Changes colour scheme of current room",
|
||||
"Gets or sets the room topic": "Gets or sets the room topic",
|
||||
"This room has no topic.": "This room has no topic.",
|
||||
|
@ -1127,6 +1128,9 @@
|
|||
"Start Chatting": "Start Chatting",
|
||||
"Confirm Removal": "Confirm Removal",
|
||||
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.",
|
||||
"Clear all data on this device?": "Clear all data on this device?",
|
||||
"Deleting all data from this device is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Deleting all data from this device is permanent. Encrypted messages will be lost unless their keys have been backed up.",
|
||||
"Delete everything": "Delete everything",
|
||||
"Community IDs cannot be empty.": "Community IDs cannot be empty.",
|
||||
"Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community IDs may only contain characters a-z, 0-9, or '=_-./'",
|
||||
"Something went wrong whilst creating your community": "Something went wrong whilst creating your community",
|
||||
|
@ -1581,6 +1585,12 @@
|
|||
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
|
||||
"Registration Successful": "Registration Successful",
|
||||
"Create your account": "Create your account",
|
||||
"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",
|
||||
|
|
|
@ -122,6 +122,7 @@ class CustomRoomTagStore extends EventEmitter {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out': {
|
||||
// we assume to always have a tags object in the state
|
||||
this._state = {tags: {}};
|
||||
|
|
|
@ -63,6 +63,7 @@ class LifecycleStore extends Store {
|
|||
dis.dispatch(deferredAction);
|
||||
break;
|
||||
}
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out':
|
||||
this.reset();
|
||||
break;
|
||||
|
|
|
@ -261,6 +261,7 @@ class RoomListStore extends Store {
|
|||
// console.log("!! Optimistic tag failure: ", payload);
|
||||
// }
|
||||
// break;
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out': {
|
||||
// Reset state without pushing an update to the view, which generally assumes that
|
||||
// the matrix client isn't `null` and so causing a re-render will cause NPEs.
|
||||
|
|
|
@ -103,6 +103,7 @@ class RoomViewStore extends Store {
|
|||
case 'join_room_error':
|
||||
this._joinRoomError(payload);
|
||||
break;
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out':
|
||||
this.reset();
|
||||
break;
|
||||
|
|
|
@ -68,6 +68,7 @@ class SessionStore extends Store {
|
|||
cachedPassword: null,
|
||||
});
|
||||
break;
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out':
|
||||
this._setState({
|
||||
cachedPassword: null,
|
||||
|
|
|
@ -166,6 +166,7 @@ class TagOrderStore extends Store {
|
|||
});
|
||||
Analytics.trackEvent('FilterStore', 'deselect_tags');
|
||||
break;
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out': {
|
||||
// Reset state without pushing an update to the view, which generally assumes that
|
||||
// the matrix client isn't `null` and so causing a re-render will cause NPEs.
|
||||
|
|
Loading…
Reference in a new issue