Allow user to control if they are signed out of all devices when changing password (#8259)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
ee2ee3c08c
commit
bb4064ff43
5 changed files with 157 additions and 76 deletions
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import Modal from "../../../Modal";
|
||||
|
@ -37,6 +38,7 @@ import AuthHeader from "../../views/auth/AuthHeader";
|
|||
import AuthBody from "../../views/auth/AuthBody";
|
||||
import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
import StyledCheckbox from '../../views/elements/StyledCheckbox';
|
||||
|
||||
enum Phase {
|
||||
// Show the forgot password inputs
|
||||
|
@ -72,6 +74,9 @@ interface IState {
|
|||
serverDeadError: string;
|
||||
|
||||
currentHttpRequest?: Promise<any>;
|
||||
|
||||
serverSupportsControlOfDevicesLogout: boolean;
|
||||
logoutDevices: boolean;
|
||||
}
|
||||
|
||||
enum ForgotPasswordField {
|
||||
|
@ -97,11 +102,14 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
serverSupportsControlOfDevicesLogout: false,
|
||||
logoutDevices: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.reset = null;
|
||||
this.checkServerLiveliness(this.props.serverConfig);
|
||||
this.checkServerCapabilities(this.props.serverConfig);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
|
@ -112,6 +120,9 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
|
||||
// Do a liveliness check on the new URLs
|
||||
this.checkServerLiveliness(newProps.serverConfig);
|
||||
|
||||
// Do capabilities check on new URLs
|
||||
this.checkServerCapabilities(newProps.serverConfig);
|
||||
}
|
||||
|
||||
private async checkServerLiveliness(serverConfig): Promise<void> {
|
||||
|
@ -129,12 +140,25 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
public submitPasswordReset(email: string, password: string): void {
|
||||
private async checkServerCapabilities(serverConfig: ValidatedServerConfig): Promise<void> {
|
||||
const tempClient = createClient({
|
||||
baseUrl: serverConfig.hsUrl,
|
||||
});
|
||||
|
||||
const serverSupportsControlOfDevicesLogout = await tempClient.doesServerSupportLogoutDevices();
|
||||
|
||||
this.setState({
|
||||
logoutDevices: !serverSupportsControlOfDevicesLogout,
|
||||
serverSupportsControlOfDevicesLogout,
|
||||
});
|
||||
}
|
||||
|
||||
public submitPasswordReset(email: string, password: string, logoutDevices = true): void {
|
||||
this.setState({
|
||||
phase: Phase.SendingEmail,
|
||||
});
|
||||
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
|
||||
this.reset.resetPassword(email, password).then(() => {
|
||||
this.reset.resetPassword(email, password, logoutDevices).then(() => {
|
||||
this.setState({
|
||||
phase: Phase.EmailSent,
|
||||
});
|
||||
|
@ -174,24 +198,35 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
return;
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description:
|
||||
<div>
|
||||
{ _t(
|
||||
"Changing your password will reset any end-to-end encryption keys " +
|
||||
"on all of your sessions, making encrypted chat history unreadable. Set up " +
|
||||
"Key Backup or export your room keys from another session before resetting your " +
|
||||
"password.",
|
||||
) }
|
||||
</div>,
|
||||
button: _t('Continue'),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(this.state.email, this.state.password);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (this.state.logoutDevices) {
|
||||
const { finished } = Modal.createTrackedDialog<[boolean]>('Forgot Password Warning', '', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description:
|
||||
<div>
|
||||
<p>{ !this.state.serverSupportsControlOfDevicesLogout ?
|
||||
_t(
|
||||
"Resetting your password on this homeserver will cause all of your devices to be " +
|
||||
"signed out. This will delete the message encryption keys stored on them, " +
|
||||
"making encrypted chat history unreadable.",
|
||||
) :
|
||||
_t(
|
||||
"Signing out your devices will delete the message encryption keys stored on them, " +
|
||||
"making encrypted chat history unreadable.",
|
||||
)
|
||||
}</p>
|
||||
<p>{ _t(
|
||||
"If you want to retain access to your chat history in encrypted rooms, set up Key Backup " +
|
||||
"or export your message keys from one of your other devices before proceeding.",
|
||||
) }</p>
|
||||
</div>,
|
||||
button: _t('Continue'),
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
this.submitPasswordReset(this.state.email, this.state.password, this.state.logoutDevices);
|
||||
};
|
||||
|
||||
private async verifyFieldsBeforeSubmit() {
|
||||
|
@ -316,6 +351,13 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
{ this.state.serverSupportsControlOfDevicesLogout ?
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<StyledCheckbox onChange={() => this.setState({ logoutDevices: !this.state.logoutDevices })} checked={this.state.logoutDevices}>
|
||||
{ _t("Sign out all devices") }
|
||||
</StyledCheckbox>
|
||||
</div> : null
|
||||
}
|
||||
<span>{ _t(
|
||||
'A verification email will be sent to your inbox to confirm ' +
|
||||
'setting your new password.',
|
||||
|
@ -355,11 +397,14 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
renderDone() {
|
||||
return <div>
|
||||
<p>{ _t("Your password has been reset.") }</p>
|
||||
<p>{ _t(
|
||||
"You have been logged out of all sessions and will no longer receive " +
|
||||
"push notifications. To re-enable notifications, sign in again on each " +
|
||||
"device.",
|
||||
) }</p>
|
||||
{ this.state.logoutDevices ?
|
||||
<p>{ _t(
|
||||
"You have been logged out of all devices and will no longer receive " +
|
||||
"push notifications. To re-enable notifications, sign in again on each " +
|
||||
"device.",
|
||||
) }</p>
|
||||
: null
|
||||
}
|
||||
<input
|
||||
className="mx_Login_submit"
|
||||
type="button"
|
||||
|
|
|
@ -41,7 +41,11 @@ enum Phase {
|
|||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished?: ({ didSetEmail: boolean }?) => void;
|
||||
onFinished?: (outcome: {
|
||||
didSetEmail?: boolean;
|
||||
/** Was one or more other devices logged out whilst changing the password */
|
||||
didLogoutOutOtherDevices: boolean;
|
||||
}) => void;
|
||||
onError?: (error: {error: string}) => void;
|
||||
rowClassName?: string;
|
||||
buttonClassName?: string;
|
||||
|
@ -82,48 +86,58 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
};
|
||||
}
|
||||
|
||||
private onChangePassword(oldPassword: string, newPassword: string): void {
|
||||
private async onChangePassword(oldPassword: string, newPassword: string): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
if (!this.props.confirm) {
|
||||
this.changePassword(cli, oldPassword, newPassword);
|
||||
return;
|
||||
// if the server supports it then don't sign user out of all devices
|
||||
const serverSupportsControlOfDevicesLogout = await cli.doesServerSupportLogoutDevices();
|
||||
const userHasOtherDevices = (await cli.getDevices()).devices.length > 1;
|
||||
|
||||
if (userHasOtherDevices && !serverSupportsControlOfDevicesLogout && this.props.confirm) {
|
||||
// warn about logging out all devices
|
||||
const { finished } = Modal.createTrackedDialog<[boolean]>('Change Password', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
<p>{ _t(
|
||||
'Changing your password on this homeserver will cause all of your other devices to be ' +
|
||||
'signed out. This will delete the message encryption keys stored on them, and may make ' +
|
||||
'encrypted chat history unreadable.',
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
'If you want to retain access to your chat history in encrypted rooms you should first ' +
|
||||
'export your room keys and re-import them afterwards.',
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
'You can also ask your homeserver admin to upgrade the server to change this behaviour.',
|
||||
) }</p>
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
extraButtons: [
|
||||
<button
|
||||
key="exportRoomKeys"
|
||||
className="mx_Dialog_primary"
|
||||
onClick={this.onExportE2eKeysClicked}
|
||||
>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>,
|
||||
],
|
||||
});
|
||||
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t(
|
||||
'Changing password will currently reset any end-to-end encryption keys on all sessions, ' +
|
||||
'making encrypted chat history unreadable, unless you first export your room keys ' +
|
||||
'and re-import them afterwards. ' +
|
||||
'In future this will be improved.',
|
||||
) }
|
||||
{ ' ' }
|
||||
<a href="https://github.com/vector-im/element-web/issues/2671" target="_blank" rel="noreferrer noopener">
|
||||
https://github.com/vector-im/element-web/issues/2671
|
||||
</a>
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
extraButtons: [
|
||||
<button
|
||||
key="exportRoomKeys"
|
||||
className="mx_Dialog_primary"
|
||||
onClick={this.onExportE2eKeysClicked}
|
||||
>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>,
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.changePassword(cli, oldPassword, newPassword);
|
||||
}
|
||||
},
|
||||
});
|
||||
this.changePassword(cli, oldPassword, newPassword, serverSupportsControlOfDevicesLogout, userHasOtherDevices);
|
||||
}
|
||||
|
||||
private changePassword(cli: MatrixClient, oldPassword: string, newPassword: string): void {
|
||||
private changePassword(
|
||||
cli: MatrixClient,
|
||||
oldPassword: string,
|
||||
newPassword: string,
|
||||
serverSupportsControlOfDevicesLogout: boolean,
|
||||
userHasOtherDevices: boolean,
|
||||
): void {
|
||||
const authDict = {
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
|
@ -140,15 +154,21 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
phase: Phase.Uploading,
|
||||
});
|
||||
|
||||
cli.setPassword(authDict, newPassword).then(() => {
|
||||
const logoutDevices = serverSupportsControlOfDevicesLogout ? false : undefined;
|
||||
|
||||
// undefined or true mean all devices signed out
|
||||
const didLogoutOutOtherDevices = !serverSupportsControlOfDevicesLogout && userHasOtherDevices;
|
||||
|
||||
cli.setPassword(authDict, newPassword, logoutDevices).then(() => {
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this.optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
didSetEmail: confirmed,
|
||||
didLogoutOutOtherDevices,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.props.onFinished();
|
||||
this.props.onFinished({ didLogoutOutOtherDevices });
|
||||
}
|
||||
}, (err) => {
|
||||
this.props.onError(err);
|
||||
|
@ -279,7 +299,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
if (err) {
|
||||
this.props.onError(err);
|
||||
} else {
|
||||
this.onChangePassword(oldPassword, newPassword);
|
||||
return this.onChangePassword(oldPassword, newPassword);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -260,14 +260,17 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
});
|
||||
};
|
||||
|
||||
private onPasswordChanged = (): void => {
|
||||
private onPasswordChanged = ({ didLogoutOutOtherDevices }: { didLogoutOutOtherDevices: boolean }): void => {
|
||||
let description = _t("Your password was successfully changed.");
|
||||
if (didLogoutOutOtherDevices) {
|
||||
description += " " + _t(
|
||||
"You will not receive push notifications on other devices until you sign back in to them.",
|
||||
);
|
||||
}
|
||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||
Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
|
||||
title: _t("Success"),
|
||||
description: _t(
|
||||
"Your password was successfully changed. You will not receive " +
|
||||
"push notifications on other sessions until you log back in to them",
|
||||
) + ".",
|
||||
description,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue