Files
ThreadNet-Web/src/components/views/auth/LoginWithQR.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

244 lines
8.8 KiB
TypeScript
Raw Normal View History

/*
Copyright 2022 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 {
MSC3906Rendezvous,
MSC3906RendezvousPayload,
LegacyRendezvousFailureReason,
} from "matrix-js-sdk/src/rendezvous";
import { MSC3886SimpleHttpRendezvousTransport } from "matrix-js-sdk/src/rendezvous/transports";
2023-03-02 13:54:43 +00:00
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "matrix-js-sdk/src/rendezvous/channels";
import { logger } from "matrix-js-sdk/src/logger";
import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler";
import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth";
import LoginWithQRFlow from "./LoginWithQRFlow";
2024-04-30 18:18:55 +01:00
import { Click, Mode, Phase } from "./LoginWithQR-types";
interface IProps {
client: MatrixClient;
mode: Mode;
onFinished(...args: any): void;
}
interface IState {
phase: Phase;
rendezvous?: MSC3906Rendezvous;
confirmationDigits?: string;
failureReason?: FailureReason;
mediaPermissionError?: boolean;
}
export enum LoginWithQRFailureReason {
RateLimited = "rate_limited",
}
export type FailureReason = LegacyRendezvousFailureReason | LoginWithQRFailureReason;
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
// However, we want to keep this implementation around for some time.
// TODO: define an end-of-life date for this implementation.
/**
* A component that allows sign in and E2EE set up with a QR code.
*
* It implements `login.reciprocate` capabilities and showing QR codes.
*
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
*/
export default class LoginWithQR extends React.Component<IProps, IState> {
2023-02-13 11:39:16 +00:00
public constructor(props: IProps) {
super(props);
this.state = {
phase: Phase.Loading,
};
}
public componentDidMount(): void {
this.updateMode(this.props.mode).then(() => {});
}
public componentDidUpdate(prevProps: Readonly<IProps>): void {
if (prevProps.mode !== this.props.mode) {
this.updateMode(this.props.mode).then(() => {});
}
}
private async updateMode(mode: Mode): Promise<void> {
this.setState({ phase: Phase.Loading });
if (this.state.rendezvous) {
2022-11-30 15:18:10 +00:00
const rendezvous = this.state.rendezvous;
rendezvous.onFailure = undefined;
await rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled);
this.setState({ rendezvous: undefined });
}
if (mode === Mode.Show) {
await this.generateCode();
}
}
public componentWillUnmount(): void {
if (this.state.rendezvous) {
// eslint-disable-next-line react/no-direct-mutation-state
this.state.rendezvous.onFailure = undefined;
// calling cancel will call close() as well to clean up the resources
this.state.rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled).then(() => {});
}
}
private approveLogin = async (): Promise<void> => {
if (!this.state.rendezvous) {
throw new Error("Rendezvous not found");
}
this.setState({ phase: Phase.Loading });
try {
logger.info("Requesting login token");
const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, {
matrixClient: this.props.client,
title: _t("auth|qr_code_login|sign_in_new_device"),
})();
this.setState({ phase: Phase.WaitingForDevice });
const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken);
if (!newDeviceId) {
// user denied
return;
}
if (!this.props.client.getCrypto()) {
// no E2EE to set up
this.props.onFinished(true);
return;
}
this.setState({ phase: Phase.Verifying });
await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
// clean up our state:
try {
await this.state.rendezvous.close();
} finally {
this.setState({ rendezvous: undefined });
}
this.props.onFinished(true);
} catch (e) {
logger.error("Error whilst approving sign in", e);
if (e instanceof HTTPError && e.httpStatus === 429) {
// 429: rate limit
this.setState({ phase: Phase.Error, failureReason: LoginWithQRFailureReason.RateLimited });
return;
}
this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.Unknown });
}
};
private generateCode = async (): Promise<void> => {
let rendezvous: MSC3906Rendezvous;
try {
const fallbackRzServer = this.props.client.getClientWellKnown()?.["io.element.rendezvous"]?.server;
const transport = new MSC3886SimpleHttpRendezvousTransport<MSC3903ECDHPayload>({
onFailure: this.onFailure,
client: this.props.client,
fallbackRzServer,
});
2023-03-02 13:54:43 +00:00
const channel = new MSC3903ECDHv2RendezvousChannel<MSC3906RendezvousPayload>(
transport,
undefined,
this.onFailure,
);
rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure);
await rendezvous.generateCode();
this.setState({
phase: Phase.ShowingQR,
rendezvous,
failureReason: undefined,
});
} catch (e) {
logger.error("Error whilst generating QR code", e);
this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.HomeserverLacksSupport });
return;
}
try {
const confirmationDigits = await rendezvous.startAfterShowingCode();
this.setState({ phase: Phase.Connected, confirmationDigits });
} catch (e) {
logger.error("Error whilst doing QR login", e);
// only set to error phase if it hasn't already been set by onFailure or similar
if (this.state.phase !== Phase.Error) {
this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.Unknown });
}
}
};
private onFailure = (reason: LegacyRendezvousFailureReason): void => {
logger.info(`Rendezvous failed: ${reason}`);
this.setState({ phase: Phase.Error, failureReason: reason });
};
public reset(): void {
this.setState({
rendezvous: undefined,
confirmationDigits: undefined,
failureReason: undefined,
});
}
private onClick = async (type: Click): Promise<void> => {
switch (type) {
case Click.Cancel:
await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled);
this.reset();
this.props.onFinished(false);
break;
case Click.Approve:
await this.approveLogin();
break;
case Click.Decline:
await this.state.rendezvous?.declineLoginOnExistingDevice();
this.reset();
this.props.onFinished(false);
break;
case Click.TryAgain:
this.reset();
await this.updateMode(this.props.mode);
break;
case Click.Back:
await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled);
this.props.onFinished(false);
break;
}
};
public render(): React.ReactNode {
return (
<LoginWithQRFlow
onClick={this.onClick}
phase={this.state.phase}
code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined}
confirmationDigits={this.state.phase === Phase.Connected ? this.state.confirmationDigits : undefined}
failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined}
/>
);
}
}