* Use `CryptoApi.getKeyBackupInfo` instead of deprecated `MatrixClient.getKeyBackupVersion` * Review changes --------- Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
260 lines
9 KiB
TypeScript
260 lines
9 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2020-2022 The Matrix.org Foundation C.I.C.
|
|
Copyright 2018, 2019 New Vector Ltd
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import React, { lazy } from "react";
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
|
|
|
import Modal from "../../../Modal";
|
|
import dis from "../../../dispatcher/dispatcher";
|
|
import { _t } from "../../../languageHandler";
|
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
import RestoreKeyBackupDialog from "./security/RestoreKeyBackupDialog";
|
|
import QuestionDialog from "./QuestionDialog";
|
|
import BaseDialog from "./BaseDialog";
|
|
import Spinner from "../elements/Spinner";
|
|
import DialogButtons from "../elements/DialogButtons";
|
|
|
|
interface IProps {
|
|
onFinished: (success: boolean) => void;
|
|
}
|
|
|
|
enum BackupStatus {
|
|
/** we're trying to figure out if there is an active backup */
|
|
LOADING,
|
|
|
|
/** crypto is disabled in this client (so no need to back up) */
|
|
NO_CRYPTO,
|
|
|
|
/** Key backup is active and working */
|
|
BACKUP_ACTIVE,
|
|
|
|
/** there is a backup on the server but we are not backing up to it */
|
|
SERVER_BACKUP_BUT_DISABLED,
|
|
|
|
/** backup is not set up locally and there is no backup on the server */
|
|
NO_BACKUP,
|
|
|
|
/** there was an error fetching the state */
|
|
ERROR,
|
|
}
|
|
|
|
interface IState {
|
|
backupStatus: BackupStatus;
|
|
}
|
|
|
|
/**
|
|
* Checks if the `LogoutDialog` should be shown instead of the simple logout flow.
|
|
* The `LogoutDialog` will check the crypto recovery status of the account and
|
|
* help the user setup recovery properly if needed.
|
|
*/
|
|
export async function shouldShowLogoutDialog(cli: MatrixClient): Promise<boolean> {
|
|
const crypto = cli?.getCrypto();
|
|
if (!crypto) return false;
|
|
|
|
// If any room is encrypted, we need to show the advanced logout flow
|
|
const allRooms = cli!.getRooms();
|
|
for (const room of allRooms) {
|
|
const isE2e = await crypto.isEncryptionEnabledInRoom(room.roomId);
|
|
if (isE2e) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export default class LogoutDialog extends React.Component<IProps, IState> {
|
|
public static defaultProps = {
|
|
onFinished: function () {},
|
|
};
|
|
|
|
public constructor(props: IProps) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
backupStatus: BackupStatus.LOADING,
|
|
};
|
|
}
|
|
|
|
public componentDidMount(): void {
|
|
this.startLoadBackupStatus();
|
|
}
|
|
|
|
/** kick off the asynchronous calls to populate `state.backupStatus` in the background */
|
|
private startLoadBackupStatus(): void {
|
|
this.loadBackupStatus().catch((e) => {
|
|
logger.log("Unable to fetch key backup status", e);
|
|
this.setState({
|
|
backupStatus: BackupStatus.ERROR,
|
|
});
|
|
});
|
|
}
|
|
|
|
private async loadBackupStatus(): Promise<void> {
|
|
const client = MatrixClientPeg.safeGet();
|
|
const crypto = client.getCrypto();
|
|
if (!crypto) {
|
|
this.setState({ backupStatus: BackupStatus.NO_CRYPTO });
|
|
return;
|
|
}
|
|
|
|
if ((await crypto.getActiveSessionBackupVersion()) !== null) {
|
|
this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE });
|
|
return;
|
|
}
|
|
|
|
// backup is not active. see if there is a backup version on the server we ought to back up to.
|
|
const backupInfo = await crypto.getKeyBackupInfo();
|
|
this.setState({ backupStatus: backupInfo ? BackupStatus.SERVER_BACKUP_BUT_DISABLED : BackupStatus.NO_BACKUP });
|
|
}
|
|
|
|
private onExportE2eKeysClicked = (): void => {
|
|
Modal.createDialog(
|
|
lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")),
|
|
{
|
|
matrixClient: MatrixClientPeg.safeGet(),
|
|
},
|
|
);
|
|
};
|
|
|
|
private onFinished = (confirmed?: boolean): void => {
|
|
if (confirmed) {
|
|
dis.dispatch({ action: "logout" });
|
|
}
|
|
// close dialog
|
|
this.props.onFinished(!!confirmed);
|
|
};
|
|
|
|
private onSetRecoveryMethodClick = (): void => {
|
|
if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) {
|
|
// A key backup exists for this account, but the creating device is not
|
|
// verified, so restore the backup which will give us the keys from it and
|
|
// allow us to trust it (ie. upload keys to it)
|
|
Modal.createDialog(
|
|
RestoreKeyBackupDialog,
|
|
undefined,
|
|
undefined,
|
|
/* priority = */ false,
|
|
/* static = */ true,
|
|
);
|
|
} else {
|
|
Modal.createDialog(
|
|
lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")),
|
|
undefined,
|
|
undefined,
|
|
/* priority = */ false,
|
|
/* static = */ true,
|
|
);
|
|
}
|
|
|
|
// close dialog
|
|
this.props.onFinished(true);
|
|
};
|
|
|
|
private onLogoutConfirm = (): void => {
|
|
dis.dispatch({ action: "logout" });
|
|
|
|
// close dialog
|
|
this.props.onFinished(true);
|
|
};
|
|
|
|
/**
|
|
* Show a dialog prompting the user to set up key backup.
|
|
*
|
|
* Either there is no backup at all ({@link BackupStatus.NO_BACKUP}), there is a backup on the server but
|
|
* we are not connected to it ({@link BackupStatus.SERVER_BACKUP_BUT_DISABLED}), or we were unable to pull the
|
|
* backup data ({@link BackupStatus.ERROR}). In all three cases, we should prompt the user to set up key backup.
|
|
*/
|
|
private renderSetupBackupDialog(): React.ReactNode {
|
|
const description = (
|
|
<div>
|
|
<p>{_t("auth|logout_dialog|setup_secure_backup_description_1")}</p>
|
|
<p>{_t("auth|logout_dialog|setup_secure_backup_description_2")}</p>
|
|
<p>{_t("encryption|setup_secure_backup|explainer")}</p>
|
|
</div>
|
|
);
|
|
|
|
let setupButtonCaption;
|
|
if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) {
|
|
setupButtonCaption = _t("settings|security|key_backup_connect");
|
|
} else {
|
|
// if there's an error fetching the backup info, we'll just assume there's
|
|
// no backup for the purpose of the button caption
|
|
setupButtonCaption = _t("auth|logout_dialog|use_key_backup");
|
|
}
|
|
|
|
const dialogContent = (
|
|
<div>
|
|
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
|
{description}
|
|
</div>
|
|
<DialogButtons
|
|
primaryButton={setupButtonCaption}
|
|
hasCancel={false}
|
|
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
|
focus={true}
|
|
>
|
|
<button onClick={this.onLogoutConfirm}>{_t("auth|logout_dialog|skip_key_backup")}</button>
|
|
</DialogButtons>
|
|
<details>
|
|
<summary className="mx_LogoutDialog_ExportKeyAdvanced">{_t("common|advanced")}</summary>
|
|
<p>
|
|
<button onClick={this.onExportE2eKeysClicked}>{_t("auth|logout_dialog|megolm_export")}</button>
|
|
</p>
|
|
</details>
|
|
</div>
|
|
);
|
|
// Not quite a standard question dialog as the primary button cancels
|
|
// the action and does something else instead, whilst non-default button
|
|
// confirms the action.
|
|
return (
|
|
<BaseDialog
|
|
title={_t("auth|logout_dialog|setup_key_backup_title")}
|
|
contentId="mx_Dialog_content"
|
|
hasCancel={true}
|
|
onFinished={this.onFinished}
|
|
>
|
|
{dialogContent}
|
|
</BaseDialog>
|
|
);
|
|
}
|
|
|
|
public render(): React.ReactNode {
|
|
switch (this.state.backupStatus) {
|
|
case BackupStatus.LOADING:
|
|
// while we're deciding if we have backups, show a spinner
|
|
return (
|
|
<BaseDialog
|
|
title={_t("action|sign_out")}
|
|
contentId="mx_Dialog_content"
|
|
hasCancel={true}
|
|
onFinished={this.onFinished}
|
|
>
|
|
<Spinner />
|
|
</BaseDialog>
|
|
);
|
|
|
|
case BackupStatus.NO_CRYPTO:
|
|
case BackupStatus.BACKUP_ACTIVE:
|
|
return (
|
|
<QuestionDialog
|
|
hasCancelButton={true}
|
|
title={_t("action|sign_out")}
|
|
description={_t("auth|logout_dialog|description")}
|
|
button={_t("action|sign_out")}
|
|
onFinished={this.onFinished}
|
|
/>
|
|
);
|
|
|
|
case BackupStatus.NO_BACKUP:
|
|
case BackupStatus.SERVER_BACKUP_BUT_DISABLED:
|
|
case BackupStatus.ERROR:
|
|
return this.renderSetupBackupDialog();
|
|
}
|
|
}
|
|
}
|