Files
ThreadNet-Web/src/components/views/settings/SetIdServer.tsx
T

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

423 lines
16 KiB
TypeScript
Raw Normal View History

2019-08-12 11:51:44 +01:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
2021-04-06 12:26:50 +01:00
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
2019-08-12 11:51:44 +01:00
2024-09-09 14:57:16 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
2019-08-12 11:51:44 +01:00
*/
import React, { ReactNode } from "react";
2021-10-22 17:23:32 -05:00
import { logger } from "matrix-js-sdk/src/logger";
import { IThreepid } from "matrix-js-sdk/src/matrix";
2021-10-22 17:23:32 -05:00
2021-06-29 13:11:58 +01:00
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
2019-08-12 14:36:48 +01:00
import Modal from "../../../Modal";
2020-05-13 20:41:41 -06:00
import dis from "../../../dispatcher/dispatcher";
import { getThreepidsWithBindStatus } from "../../../boundThreepids";
import IdentityAuthClient from "../../../IdentityAuthClient";
import { abbreviateUrl, parseUrl, unabbreviateUrl } from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from "../../../utils/IdentityServerUtils";
2021-06-29 13:11:58 +01:00
import { timeout } from "../../../utils/promise";
2021-04-26 15:21:49 +01:00
import { ActionPayload } from "../../../dispatcher/payloads";
2021-07-03 10:06:42 +02:00
import InlineSpinner from "../elements/InlineSpinner";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
import QuestionDialog from "../dialogs/QuestionDialog";
import SettingsFieldset from "./SettingsFieldset";
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
2019-08-12 11:51:44 +01:00
2019-10-18 12:40:50 +01:00
// We'll wait up to this long when checking for 3PID bindings on the IS.
const REACHABILITY_TIMEOUT = 10000; // ms
2019-08-12 11:51:44 +01:00
/**
* Check an IS URL is valid, including liveness check
*
2019-08-13 12:59:34 +01:00
* @param {string} u The url to check
2019-08-12 11:51:44 +01:00
* @returns {string} null if url passes all checks, otherwise i18ned error string
*/
async function checkIdentityServerUrl(u: string): Promise<string | null> {
const parsedUrl = parseUrl(u);
2019-08-12 11:51:44 +01:00
if (parsedUrl.protocol !== "https:") return _t("identity_server|url_not_https");
2019-08-12 11:51:44 +01:00
// XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the
// js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it
2019-08-13 16:37:56 +01:00
try {
const response = await fetch(u + "/_matrix/identity/v2");
2019-08-13 16:37:56 +01:00
if (response.ok) {
return null;
} else if (response.status < 200 || response.status >= 300) {
return _t("identity_server|error_invalid", { code: response.status });
2019-08-13 16:37:56 +01:00
} else {
return _t("identity_server|error_connection");
2019-08-13 16:37:56 +01:00
}
2024-10-16 16:43:07 +01:00
} catch {
return _t("identity_server|error_connection");
2019-08-13 16:37:56 +01:00
}
2019-08-12 11:51:44 +01:00
}
2021-04-06 12:26:50 +01:00
interface IProps {
2021-07-13 15:26:38 +01:00
// Whether or not the identity server is missing terms. This affects the text
2021-04-06 12:26:50 +01:00
// shown to the user.
missingTerms: boolean;
}
2021-04-06 12:26:50 +01:00
interface IState {
defaultIdServer?: string;
currentClientIdServer?: string;
idServer: string;
2021-04-06 12:26:50 +01:00
error?: string;
busy: boolean;
disconnectBusy: boolean;
checking: boolean;
}
export default class SetIdServer extends React.Component<IProps, IState> {
private dispatcherRef?: string;
2021-04-06 12:26:50 +01:00
2023-02-13 11:39:16 +00:00
public constructor(props: IProps) {
2021-04-06 12:26:50 +01:00
super(props);
2019-08-12 11:51:44 +01:00
let defaultIdServer = "";
if (!MatrixClientPeg.safeGet().getIdentityServerUrl() && getDefaultIdentityServerUrl()) {
2021-07-13 15:26:38 +01:00
// If no identity server is configured but there's one in the config, prepopulate
// the field to help the user.
defaultIdServer = abbreviateUrl(getDefaultIdentityServerUrl());
2019-08-12 11:51:44 +01:00
}
this.state = {
defaultIdServer,
currentClientIdServer: MatrixClientPeg.safeGet().getIdentityServerUrl(),
idServer: "",
2019-08-12 11:51:44 +01:00
busy: false,
2019-08-15 12:04:07 +01:00
disconnectBusy: false,
checking: false,
2019-08-12 11:51:44 +01:00
};
}
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
}
public componentWillUnmount(): void {
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
}
private onAction = (payload: ActionPayload): void => {
2021-07-13 15:26:38 +01:00
// We react to changes in the identity server in the event the user is staring at this form
// when changing their identity server on another device.
if (payload.action !== "id_server_changed") return;
this.setState({
currentClientIdServer: MatrixClientPeg.safeGet().getIdentityServerUrl(),
});
};
private onIdentityServerChanged = (ev: React.ChangeEvent<HTMLInputElement>): void => {
2019-08-12 11:51:44 +01:00
const u = ev.target.value;
2021-06-29 13:11:58 +01:00
this.setState({ idServer: u });
2019-08-12 11:51:44 +01:00
};
private getTooltip = (): ReactNode => {
if (this.state.checking) {
2019-08-12 11:51:44 +01:00
return (
<div>
<InlineSpinner />
{_t("identity_server|checking")}
2019-08-12 11:51:44 +01:00
</div>
);
} else if (this.state.error) {
return <strong className="warning">{this.state.error}</strong>;
2019-08-12 11:51:44 +01:00
} else {
return null;
}
};
private idServerChangeEnabled = (): boolean => {
2019-08-12 11:51:44 +01:00
return !!this.state.idServer && !this.state.busy;
};
private saveIdServer = (fullUrl: string): void => {
// Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.safeGet().setAccountData("m.identity_server", {
base_url: fullUrl,
});
this.setState({
busy: false,
error: undefined,
currentClientIdServer: fullUrl,
idServer: "",
});
};
private checkIdServer = async (e: React.SyntheticEvent): Promise<void> => {
2019-08-13 16:20:30 +01:00
e.preventDefault();
const { idServer, currentClientIdServer } = this.state;
2019-08-13 16:20:30 +01:00
this.setState({ busy: true, checking: true, error: undefined });
2019-08-12 11:51:44 +01:00
const fullUrl = unabbreviateUrl(idServer);
2019-08-12 11:51:44 +01:00
let errStr = await checkIdentityServerUrl(fullUrl);
2019-08-12 11:51:44 +01:00
if (!errStr) {
try {
2021-06-29 13:11:58 +01:00
this.setState({ checking: false }); // clear tooltip
// Test the identity server by trying to register with it. This
// may result in a terms of service prompt.
const authClient = new IdentityAuthClient(fullUrl);
await authClient.getAccessToken();
let save = true;
// Double check that the identity server even has terms of service.
const hasTerms = await doesIdentityServerHaveTerms(MatrixClientPeg.safeGet(), fullUrl);
if (!hasTerms) {
const [confirmed] = await this.showNoTermsWarning(fullUrl);
save = !!confirmed;
}
// Show a general warning, possibly with details about any bound
// 3PIDs that would be left behind.
if (save && currentClientIdServer && fullUrl !== currentClientIdServer) {
const [confirmed] = await this.showServerChangeWarning({
title: _t("identity_server|change"),
unboundMessage: _t(
"identity_server|change_prompt",
{},
{
current: (sub) => <strong>{abbreviateUrl(currentClientIdServer)}</strong>,
new: (sub) => <strong>{abbreviateUrl(idServer)}</strong>,
},
),
button: _t("action|continue"),
});
save = !!confirmed;
}
if (save) {
this.saveIdServer(fullUrl);
}
} catch (e) {
2021-10-15 16:30:53 +02:00
logger.error(e);
errStr = _t("identity_server|error_invalid_or_terms");
}
2019-08-12 11:51:44 +01:00
}
this.setState({
busy: false,
checking: false,
error: errStr ?? undefined,
currentClientIdServer: MatrixClientPeg.safeGet().getIdentityServerUrl(),
2019-08-12 11:51:44 +01:00
});
};
private showNoTermsWarning(fullUrl: string): Promise<[ok?: boolean]> {
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("terms|identity_server_no_terms_title"),
2019-08-23 11:58:04 -06:00
description: (
<div>
<strong className="warning">{_t("identity_server|no_terms")}</strong>
<span>&nbsp;{_t("terms|identity_server_no_terms_description_2")}</span>
2019-08-23 11:58:04 -06:00
</div>
),
button: _t("action|continue"),
2019-08-23 11:58:04 -06:00
});
2019-09-06 10:48:24 +01:00
return finished;
2019-08-23 12:01:47 -06:00
}
2019-08-23 11:58:04 -06:00
private onDisconnectClicked = async (): Promise<void> => {
2021-06-29 13:11:58 +01:00
this.setState({ disconnectBusy: true });
2019-08-15 12:04:07 +01:00
try {
const [confirmed] = await this.showServerChangeWarning({
title: _t("identity_server|disconnect"),
unboundMessage: _t(
"identity_server|disconnect_server",
2019-08-15 12:04:07 +01:00
{},
{ idserver: (sub) => <strong>{abbreviateUrl(this.state.currentClientIdServer)}</strong> },
),
button: _t("action|disconnect"),
2019-08-15 12:04:07 +01:00
});
if (confirmed) {
this.disconnectIdServer();
}
2019-08-15 12:04:07 +01:00
} finally {
2021-06-29 13:11:58 +01:00
this.setState({ disconnectBusy: false });
2019-08-15 12:04:07 +01:00
}
2019-08-12 14:36:48 +01:00
};
private async showServerChangeWarning({
title,
unboundMessage,
button,
}: {
title: string;
unboundMessage: ReactNode;
button: string;
}): Promise<[ok?: boolean]> {
const { currentClientIdServer } = this.state;
let threepids: IThreepid[] = [];
let currentServerReachable = true;
try {
threepids = await timeout(
getThreepidsWithBindStatus(MatrixClientPeg.safeGet()),
Promise.reject(new Error("Timeout attempting to reach identity server")),
REACHABILITY_TIMEOUT,
);
} catch (e) {
currentServerReachable = false;
2021-10-15 16:31:29 +02:00
logger.warn(
`Unable to reach identity server at ${currentClientIdServer} to check ` +
`for 3PIDs during IS change flow`,
);
2021-10-15 16:31:29 +02:00
logger.warn(e);
}
const boundThreepids = threepids.filter((tp) => tp.bound);
let message;
2019-09-06 13:43:21 +01:00
let danger = false;
const messageElements = {
idserver: (sub: string) => <strong>{abbreviateUrl(currentClientIdServer)}</strong>,
b: (sub: string) => <strong>{sub}</strong>,
};
if (!currentServerReachable) {
message = (
<div>
<p>{_t("identity_server|disconnect_offline_warning", {}, messageElements)}</p>
<p>{_t("identity_server|suggestions")}</p>
<ul>
<li>{_t("identity_server|suggestions_1")}</li>
<li>
{_t(
"identity_server|suggestions_2",
{},
{
idserver: messageElements.idserver,
},
2022-12-12 12:24:14 +01:00
)}
</li>
<li>{_t("identity_server|suggestions_3")}</li>
</ul>
</div>
);
danger = true;
button = _t("identity_server|disconnect_anyway");
} else if (boundThreepids.length) {
2019-09-09 10:27:02 +01:00
message = (
<div>
<p>{_t("identity_server|disconnect_personal_data_warning_1", {}, messageElements)}</p>
<p>{_t("identity_server|disconnect_personal_data_warning_2")}</p>
2019-09-09 10:27:02 +01:00
</div>
);
2019-09-06 13:43:21 +01:00
danger = true;
button = _t("identity_server|disconnect_anyway");
} else {
message = unboundMessage;
}
const { finished } = Modal.createDialog(QuestionDialog, {
2019-09-06 10:48:24 +01:00
title,
description: message,
button,
cancelButton: _t("action|go_back"),
2019-09-06 13:43:21 +01:00
danger,
});
2019-09-06 10:48:24 +01:00
return finished;
}
private disconnectIdServer = (): void => {
// Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.safeGet().setAccountData("m.identity_server", {
base_url: null, // clear
});
2019-08-14 10:06:05 +01:00
let newFieldVal = "";
if (getDefaultIdentityServerUrl()) {
2019-08-14 10:06:05 +01:00
// Prepopulate the client's default so the user at least has some idea of
// a valid value they might enter
newFieldVal = abbreviateUrl(getDefaultIdentityServerUrl());
2019-08-14 10:06:05 +01:00
}
2019-08-12 14:36:48 +01:00
this.setState({
busy: false,
error: undefined,
currentClientIdServer: MatrixClientPeg.safeGet().getIdentityServerUrl(),
2019-08-14 10:06:05 +01:00
idServer: newFieldVal,
2019-08-12 14:36:48 +01:00
});
};
public render(): React.ReactNode {
2019-08-12 11:51:44 +01:00
const idServerUrl = this.state.currentClientIdServer;
let sectionTitle;
let bodyText;
if (idServerUrl) {
sectionTitle = _t("identity_server|url", { server: abbreviateUrl(idServerUrl) });
2019-08-12 11:51:44 +01:00
bodyText = _t(
"identity_server|description_connected",
2019-08-12 11:51:44 +01:00
{},
{ server: (sub) => <strong>{abbreviateUrl(idServerUrl)}</strong> },
2019-08-12 11:51:44 +01:00
);
if (this.props.missingTerms) {
bodyText = _t(
"identity_server|change_server_prompt",
{},
{ server: (sub) => <strong>{abbreviateUrl(idServerUrl)}</strong> },
);
}
2019-08-12 11:51:44 +01:00
} else {
sectionTitle = _t("common|identity_server");
bodyText = _t("identity_server|description_disconnected");
2019-08-12 11:51:44 +01:00
}
2019-08-12 14:36:48 +01:00
let discoSection;
if (idServerUrl) {
let discoButtonContent: React.ReactNode = _t("action|disconnect");
let discoBodyText = _t("identity_server|disconnect_warning");
if (this.props.missingTerms) {
discoBodyText = _t("identity_server|description_optional");
discoButtonContent = _t("identity_server|do_not_use");
}
2019-08-15 12:04:07 +01:00
if (this.state.disconnectBusy) {
discoButtonContent = <InlineSpinner />;
}
2019-08-12 14:36:48 +01:00
discoSection = (
<>
<SettingsSubsectionText>{discoBodyText}</SettingsSubsectionText>
<AccessibleButton onClick={this.onDisconnectClicked} kind="danger_sm">
{discoButtonContent}
2019-08-12 14:36:48 +01:00
</AccessibleButton>
</>
2019-08-12 14:36:48 +01:00
);
}
2019-08-12 11:51:44 +01:00
return (
<SettingsFieldset legend={sectionTitle} description={bodyText}>
<form className="mx_SetIdServer" onSubmit={this.checkIdServer}>
<Field
label={_t("identity_server|url_field_label")}
type="text"
autoComplete="off"
placeholder={this.state.defaultIdServer}
value={this.state.idServer}
onChange={this.onIdentityServerChanged}
tooltipContent={this.getTooltip()}
tooltipClassName="mx_SetIdServer_tooltip"
disabled={this.state.busy}
forceValidity={this.state.error ? false : undefined}
/>
<AccessibleButton
type="submit"
kind="primary_sm"
onClick={this.checkIdServer}
disabled={!this.idServerChangeEnabled()}
>
{_t("action|change")}
</AccessibleButton>
{discoSection}
</form>
</SettingsFieldset>
2019-08-12 11:51:44 +01:00
);
}
}