Skip to content

Commit

Permalink
Add error screens for connecting to the JWT service
Browse files Browse the repository at this point in the history
  • Loading branch information
robintown committed Jan 16, 2025
1 parent b83d34f commit 873ec24
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 37 deletions.
8 changes: 7 additions & 1 deletion locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,22 @@
},
"disconnected_banner": "Connectivity to the server has been lost.",
"error": {
"auth_connection_failed_details": "<0>The application could not reach the call authentication service at <2>{{url}}</2>. If you are the server admin, check the network logs and make sure <5>lk-jwt-service</5> is listening at that address.</0>",
"auth_connection_rejected_details": "<0>The application connected to the call authentication service at <2>{{url}}</2>, but it responded with status code {{status}}. If you are the server admin, make sure <8>lk-jwt-service</8> is listening at that address and check the logs for more information.</0>",
"call_not_found": "Call not found",
"call_not_found_description": "<0>That link doesn't appear to belong to any existing call. Check that you have the right link, or <1>create a new one</1>.</0>",
"connection_failed": "Connection failed",
"connection_failed_description": "Could not connect to the call server. Check your network connection or try again later.",
"connection_lost": "Connection lost",
"connection_lost_description": "You were disconnected from the call.",
"connection_rejected_description": "Could not connect to the call server. Try again later or contact your server admin if this persists.",
"e2ee_unsupported": "Incompatible browser",
"e2ee_unsupported_description": "Your web browser does not support encrypted calls. Supported browsers include Chrome, Safari, and Firefox 117+.",
"generic": "Something went wrong",
"generic_description": "Submitting debug logs will help us track down the problem.",
"open_elsewhere": "Opened in another tab",
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page."
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
"show_details": "Show details"
},
"group_call_loader": {
"banned_body": "You have been banned from the room.",
Expand Down
133 changes: 129 additions & 4 deletions src/RichError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { type FC, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import {
type ReactElement,
useCallback,
useState,
type FC,
type ReactNode,
} from "react";
import { Trans, useTranslation } from "react-i18next";
import {
OfflineIcon,
PopOutIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { Button, Link } from "@vector-im/compound-web";

import { ErrorView } from "./ErrorView";

Expand All @@ -22,8 +32,9 @@ export class RichError extends Error {
* The pretty, more helpful message to be shown on the error screen.
*/
public readonly richMessage: ReactNode,
cause?: unknown,
) {
super(message);
super(message, { cause });
}
}

Expand All @@ -46,3 +57,117 @@ export class OpenElsewhereError extends RichError {
super("App opened in another tab", <OpenElsewhere />);
}
}

interface AuthConnectionFailedProps {
livekitServiceUrl: string;
}

const AuthConnectionFailed: FC<AuthConnectionFailedProps> = ({
livekitServiceUrl,
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const onShowDetailsClick = useCallback(() => setShowDetails(true), []);

return (
<ErrorView Icon={OfflineIcon} title={t("error.connection_failed")}>
<p>{t("error.connection_failed_description")}</p>
{showDetails ? (
<Trans
i18nKey="error.auth_connection_failed_details"
url={livekitServiceUrl}
>
<p>
The application could not reach the call authentication service at{" "}
<Link href={livekitServiceUrl} target="_blank">
{{ url: livekitServiceUrl } as unknown as ReactElement}
</Link>
. If you are the server admin, check the network logs and make sure{" "}
<Link
href="https://github.com/element-hq/lk-jwt-service/"
target="_blank"
>
lk-jwt-service
</Link>{" "}
is listening at that address.
</p>
</Trans>
) : (
<Button kind="tertiary" onClick={onShowDetailsClick}>
{t("error.show_details")}
</Button>
)}
</ErrorView>
);
};

export class AuthConnectionFailedError extends RichError {
public constructor(livekitServiceUrl: string, cause: unknown) {
super(
`Failed to connect to ${livekitServiceUrl}`,
<AuthConnectionFailed livekitServiceUrl={livekitServiceUrl} />,
cause,
);
}
}

interface AuthConnectionRejectedProps {
livekitServiceUrl: string;
status: number;
}

const AuthConnectionRejected: FC<AuthConnectionRejectedProps> = ({
livekitServiceUrl,
status,
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const onShowDetailsClick = useCallback(() => setShowDetails(true), []);

return (
<ErrorView Icon={OfflineIcon} title={t("error.connection_failed")}>
<p>{t("error.connection_rejected_description")}</p>
{showDetails ? (
<Trans
i18nKey="error.auth_connection_rejected_details"
url={livekitServiceUrl}
status={status}
>
<p>
The application connected to the call authentication service at{" "}
<Link href={livekitServiceUrl} target="_blank">
{{ url: livekitServiceUrl } as unknown as ReactElement}
</Link>
, but it responded with status code{" "}
{{ status } as unknown as ReactElement}. If you are the server
admin, make sure{" "}
<Link
href="https://github.com/element-hq/lk-jwt-service/"
target="_blank"
>
lk-jwt-service
</Link>{" "}
is listening at that address and check the logs for more
information.
</p>
</Trans>
) : (
<Button kind="tertiary" onClick={onShowDetailsClick}>
{t("error.show_details")}
</Button>
)}
</ErrorView>
);
};

export class AuthConnectionRejectedError extends RichError {
public constructor(livekitServiceUrl: string, status: number) {
super(
`Failed to connect to ${livekitServiceUrl} (status ${status})`,
<AuthConnectionRejected
livekitServiceUrl={livekitServiceUrl}
status={status}
/>,
);
}
}
60 changes: 28 additions & 32 deletions src/livekit/openIDSFU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { useEffect, useState } from "react";
import { type LivekitFocus } from "matrix-js-sdk/src/matrixrtc/LivekitFocus";

import { useActiveLivekitFocus } from "../room/useActiveFocus";
import {
AuthConnectionFailedError,
AuthConnectionRejectedError,
} from "../RichError";

export interface SFUConfig {
url: string;
Expand All @@ -35,25 +39,24 @@ export function useOpenIDSFU(
client: OpenIDClientParts,
rtcSession: MatrixRTCSession,
): SFUConfig | undefined {
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
const [sfuConfig, setSFUConfig] = useState<SFUConfig | Error | undefined>(
undefined,
);

const activeFocus = useActiveLivekitFocus(rtcSession);

useEffect(() => {
if (activeFocus) {
getSFUConfigWithOpenID(client, activeFocus).then(
(sfuConfig) => {
setSFUConfig(sfuConfig);
},
(e) => {
logger.error("Failed to get SFU config", e);
},
(sfuConfig) => setSFUConfig(sfuConfig),
(e) => setSFUConfig(e),
);
} else {
setSFUConfig(undefined);
}
}, [client, activeFocus]);

if (sfuConfig instanceof Error) throw sfuConfig;
return sfuConfig;
}

Expand All @@ -64,26 +67,18 @@ export async function getSFUConfigWithOpenID(
const openIdToken = await client.getOpenIdToken();
logger.debug("Got openID token", openIdToken);

try {
logger.info(
`Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`,
);
const sfuConfig = await getLiveKitJWT(
client,
activeFocus.livekit_service_url,
activeFocus.livekit_alias,
openIdToken,
);
logger.info(`Got JWT from call's active focus URL.`);
logger.info(
`Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`,
);
const sfuConfig = await getLiveKitJWT(
client,
activeFocus.livekit_service_url,
activeFocus.livekit_alias,
openIdToken,
);
logger.info(`Got JWT from call's active focus URL.`);

return sfuConfig;
} catch (e) {
logger.warn(
`Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`,
e,
);
return undefined;
}
return sfuConfig;
}

async function getLiveKitJWT(
Expand All @@ -92,8 +87,9 @@ async function getLiveKitJWT(
roomName: string,
openIDToken: IOpenIDToken,
): Promise<SFUConfig> {
let res: Response;
try {
const res = await fetch(livekitServiceURL + "/sfu/get", {
res = await fetch(livekitServiceURL + "/sfu/get", {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand All @@ -104,11 +100,11 @@ async function getLiveKitJWT(
device_id: client.getDeviceId(),
}),
});
if (!res.ok) {
throw new Error("SFU Config fetch failed with status code " + res.status);
}
return await res.json();
} catch (e) {
throw new Error("SFU Config fetch failed with exception " + e);
throw new AuthConnectionFailedError(livekitServiceURL, e);
}
if (!res.ok) {
throw new AuthConnectionRejectedError(livekitServiceURL, res.status);
}
return await res.json();
}

0 comments on commit 873ec24

Please sign in to comment.