OIDC settings tweaks (#28787)
* Hide 3pid account settings if account is managed externally As they would be disabled and just confusing otherwise Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Show manage device button instead of sign out button for other devices in OIDC mode Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Tidy up Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>pull/28813/head
parent
9d5141cfaa
commit
16d2cccb73
|
@ -19,6 +19,7 @@ ignore.push("/OpenSpotlightPayload.ts");
|
|||
ignore.push("/PinnedMessageBadge.tsx");
|
||||
ignore.push("/editor/mock.ts");
|
||||
ignore.push("DeviceIsolationModeController.ts");
|
||||
ignore.push("urls.ts");
|
||||
ignore.push("/json.ts");
|
||||
ignore.push("/ReleaseAnnouncementStore.ts");
|
||||
ignore.push("/WidgetLayoutStore.ts");
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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, { useState } from "react";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import BaseDialog from "../BaseDialog";
|
||||
import { getOidcLogoutUrl } from "../../../../utils/oidc/getOidcLogoutUrl";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
|
||||
export interface OidcLogoutDialogProps {
|
||||
delegatedAuthAccountUrl: string;
|
||||
deviceId: string;
|
||||
onFinished(ok?: boolean): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle logout of OIDC sessions other than the current session
|
||||
* - ask for user confirmation to open the delegated auth provider
|
||||
* - open the auth provider in a new tab
|
||||
* - wait for the user to return and close the modal, we assume the user has completed sign out of the session in auth provider UI
|
||||
* and trigger a refresh of the session list
|
||||
*/
|
||||
export const OidcLogoutDialog: React.FC<OidcLogoutDialogProps> = ({
|
||||
delegatedAuthAccountUrl,
|
||||
deviceId,
|
||||
onFinished,
|
||||
}) => {
|
||||
const [hasOpenedLogoutLink, setHasOpenedLogoutLink] = useState(false);
|
||||
const logoutUrl = getOidcLogoutUrl(delegatedAuthAccountUrl, deviceId);
|
||||
|
||||
return (
|
||||
<BaseDialog onFinished={onFinished} title={_t("action|sign_out")} contentId="mx_Dialog_content">
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
{_t("auth|oidc|logout_redirect_warning")}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
{hasOpenedLogoutLink ? (
|
||||
<AccessibleButton kind="primary" onClick={() => onFinished(true)}>
|
||||
{_t("action|close")}
|
||||
</AccessibleButton>
|
||||
) : (
|
||||
<>
|
||||
<AccessibleButton kind="secondary" onClick={() => onFinished(false)}>
|
||||
{_t("action|cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
element="a"
|
||||
onClick={() => setHasOpenedLogoutLink(true)}
|
||||
kind="primary"
|
||||
href={logoutUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{_t("action|continue")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
|
@ -34,6 +34,7 @@ interface Props {
|
|||
onSignOutCurrentDevice: () => void;
|
||||
signOutAllOtherSessions?: () => void;
|
||||
saveDeviceName: (deviceName: string) => Promise<void>;
|
||||
delegatedAuthAccountUrl?: string;
|
||||
}
|
||||
|
||||
type CurrentDeviceSectionHeadingProps = Pick<
|
||||
|
@ -90,6 +91,7 @@ const CurrentDeviceSection: React.FC<Props> = ({
|
|||
onSignOutCurrentDevice,
|
||||
signOutAllOtherSessions,
|
||||
saveDeviceName,
|
||||
delegatedAuthAccountUrl,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
|
@ -126,6 +128,8 @@ const CurrentDeviceSection: React.FC<Props> = ({
|
|||
onSignOutDevice={onSignOutCurrentDevice}
|
||||
saveDeviceName={saveDeviceName}
|
||||
className="mx_CurrentDeviceSection_deviceDetails"
|
||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
||||
isCurrentDevice
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -18,6 +18,7 @@ import ToggleSwitch from "../../elements/ToggleSwitch";
|
|||
import { DeviceDetailHeading } from "./DeviceDetailHeading";
|
||||
import { DeviceVerificationStatusCard } from "./DeviceVerificationStatusCard";
|
||||
import { ExtendedDevice } from "./types";
|
||||
import { getManageDeviceUrl } from "../../../../utils/oidc/urls.ts";
|
||||
|
||||
interface Props {
|
||||
device: ExtendedDevice;
|
||||
|
@ -31,6 +32,7 @@ interface Props {
|
|||
supportsMSC3881?: boolean;
|
||||
className?: string;
|
||||
isCurrentDevice?: boolean;
|
||||
delegatedAuthAccountUrl?: string;
|
||||
}
|
||||
|
||||
interface MetadataTable {
|
||||
|
@ -39,6 +41,22 @@ interface MetadataTable {
|
|||
values: { label: string; value?: string | React.ReactNode }[];
|
||||
}
|
||||
|
||||
function isPushNotificationsEnabled(pusher?: IPusher, notificationSettings?: LocalNotificationSettings): boolean {
|
||||
if (pusher) return !!pusher[PUSHER_ENABLED.name];
|
||||
if (notificationSettings) return !notificationSettings.is_silenced;
|
||||
return true;
|
||||
}
|
||||
|
||||
function isCheckboxDisabled(
|
||||
pusher?: IPusher,
|
||||
notificationSettings?: LocalNotificationSettings,
|
||||
supportsMSC3881?: boolean,
|
||||
): boolean {
|
||||
if (notificationSettings) return false;
|
||||
if (pusher && !supportsMSC3881) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const DeviceDetails: React.FC<Props> = ({
|
||||
device,
|
||||
pusher,
|
||||
|
@ -51,6 +69,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
supportsMSC3881,
|
||||
className,
|
||||
isCurrentDevice,
|
||||
delegatedAuthAccountUrl,
|
||||
}) => {
|
||||
const metadata: MetadataTable[] = [
|
||||
{
|
||||
|
@ -95,18 +114,6 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
|
||||
const showPushNotificationSection = !!pusher || !!localNotificationSettings;
|
||||
|
||||
function isPushNotificationsEnabled(pusher?: IPusher, notificationSettings?: LocalNotificationSettings): boolean {
|
||||
if (pusher) return !!pusher[PUSHER_ENABLED.name];
|
||||
if (localNotificationSettings) return !localNotificationSettings.is_silenced;
|
||||
return true;
|
||||
}
|
||||
|
||||
function isCheckboxDisabled(pusher?: IPusher, notificationSettings?: LocalNotificationSettings): boolean {
|
||||
if (localNotificationSettings) return false;
|
||||
if (pusher && !supportsMSC3881) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames("mx_DeviceDetails", className)} data-testid={`device-detail-${device.device_id}`}>
|
||||
<section className="mx_DeviceDetails_section">
|
||||
|
@ -117,32 +124,34 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
isCurrentDevice={isCurrentDevice}
|
||||
/>
|
||||
</section>
|
||||
<section className="mx_DeviceDetails_section">
|
||||
<p className="mx_DeviceDetails_sectionHeading">{_t("settings|sessions|details_heading")}</p>
|
||||
{metadata.map(({ heading, values, id }, index) => (
|
||||
<table
|
||||
className="mx_DeviceDetails_metadataTable"
|
||||
key={index}
|
||||
data-testid={`device-detail-metadata-${id}`}
|
||||
>
|
||||
{heading && (
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{heading}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{values.map(({ label, value }) => (
|
||||
<tr key={label}>
|
||||
<td className="mxDeviceDetails_metadataLabel">{label}</td>
|
||||
<td className="mxDeviceDetails_metadataValue">{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
</section>
|
||||
{!delegatedAuthAccountUrl && (
|
||||
<section className="mx_DeviceDetails_section">
|
||||
<p className="mx_DeviceDetails_sectionHeading">{_t("settings|sessions|details_heading")}</p>
|
||||
{metadata.map(({ heading, values, id }, index) => (
|
||||
<table
|
||||
className="mx_DeviceDetails_metadataTable"
|
||||
key={index}
|
||||
data-testid={`device-detail-metadata-${id}`}
|
||||
>
|
||||
{heading && (
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{heading}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{values.map(({ label, value }) => (
|
||||
<tr key={label}>
|
||||
<td className="mxDeviceDetails_metadataLabel">{label}</td>
|
||||
<td className="mxDeviceDetails_metadataValue">{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
{showPushNotificationSection && (
|
||||
<section
|
||||
className="mx_DeviceDetails_section mx_DeviceDetails_pushNotifications"
|
||||
|
@ -152,7 +161,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
// For backwards compatibility, if `enabled` is missing
|
||||
// default to `true`
|
||||
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
|
||||
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
|
||||
disabled={isCheckboxDisabled(pusher, localNotificationSettings, supportsMSC3881)}
|
||||
onChange={(checked) => setPushNotifications?.(device.device_id, checked)}
|
||||
title={_t("settings|sessions|push_toggle")}
|
||||
data-testid="device-detail-push-notification-checkbox"
|
||||
|
@ -166,17 +175,30 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
</section>
|
||||
)}
|
||||
<section className="mx_DeviceDetails_section">
|
||||
<AccessibleButton
|
||||
onClick={onSignOutDevice}
|
||||
kind="danger_inline"
|
||||
disabled={isSigningOut}
|
||||
data-testid="device-detail-sign-out-cta"
|
||||
>
|
||||
<span className="mx_DeviceDetails_signOutButtonContent">
|
||||
{_t("settings|sessions|sign_out")}
|
||||
{isSigningOut && <Spinner w={16} h={16} />}
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
{delegatedAuthAccountUrl && !isCurrentDevice ? (
|
||||
<AccessibleButton
|
||||
element="a"
|
||||
onClick={null}
|
||||
kind="link_inline"
|
||||
href={getManageDeviceUrl(delegatedAuthAccountUrl, device.device_id)}
|
||||
target="_blank"
|
||||
data-testid="device-detail-sign-out-cta"
|
||||
>
|
||||
<span className="mx_DeviceDetails_signOutButtonContent">{_t("settings|sessions|manage")}</span>
|
||||
</AccessibleButton>
|
||||
) : (
|
||||
<AccessibleButton
|
||||
onClick={onSignOutDevice}
|
||||
kind="danger_inline"
|
||||
disabled={isSigningOut}
|
||||
data-testid="device-detail-sign-out-cta"
|
||||
>
|
||||
<span className="mx_DeviceDetails_signOutButtonContent">
|
||||
{_t("settings|sessions|sign_out")}
|
||||
{isSigningOut && <Spinner w={16} h={16} />}
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -41,10 +41,12 @@ interface Props {
|
|||
setSelectedDeviceIds: (deviceIds: ExtendedDevice["device_id"][]) => void;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
/**
|
||||
* Only allow sessions to be signed out individually
|
||||
* If the user's account is managed externally then sessions must be signed out individually
|
||||
* Removes checkboxes and multi selection header
|
||||
* Removes session info as that can be seen in the account management
|
||||
* Changes sign out button to be a manage button
|
||||
*/
|
||||
disableMultipleSignout?: boolean;
|
||||
delegatedAuthAccountUrl?: string;
|
||||
}
|
||||
|
||||
const isDeviceSelected = (
|
||||
|
@ -172,6 +174,7 @@ const DeviceListItem: React.FC<{
|
|||
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
isSelectDisabled?: boolean;
|
||||
delegatedAuthAccountUrl?: string;
|
||||
}> = ({
|
||||
device,
|
||||
pusher,
|
||||
|
@ -187,6 +190,7 @@ const DeviceListItem: React.FC<{
|
|||
toggleSelected,
|
||||
supportsMSC3881,
|
||||
isSelectDisabled,
|
||||
delegatedAuthAccountUrl,
|
||||
}) => {
|
||||
const tileContent = (
|
||||
<>
|
||||
|
@ -222,6 +226,7 @@ const DeviceListItem: React.FC<{
|
|||
setPushNotifications={setPushNotifications}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
className="mx_FilteredDeviceList_deviceDetails"
|
||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
|
@ -250,7 +255,7 @@ export const FilteredDeviceList = forwardRef(
|
|||
setPushNotifications,
|
||||
setSelectedDeviceIds,
|
||||
supportsMSC3881,
|
||||
disableMultipleSignout,
|
||||
delegatedAuthAccountUrl,
|
||||
}: Props,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
|
@ -311,7 +316,7 @@ export const FilteredDeviceList = forwardRef(
|
|||
selectedDeviceCount={selectedDeviceIds.length}
|
||||
isAllSelected={isAllSelected}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
isSelectDisabled={disableMultipleSignout}
|
||||
isSelectDisabled={!!delegatedAuthAccountUrl}
|
||||
>
|
||||
{selectedDeviceIds.length ? (
|
||||
<>
|
||||
|
@ -361,7 +366,7 @@ export const FilteredDeviceList = forwardRef(
|
|||
isExpanded={expandedDeviceIds.includes(device.device_id)}
|
||||
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
|
||||
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
|
||||
isSelectDisabled={disableMultipleSignout}
|
||||
isSelectDisabled={!!delegatedAuthAccountUrl}
|
||||
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
|
||||
onSignOutDevice={() => onSignOutDevices([device.device_id])}
|
||||
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
|
||||
|
@ -373,6 +378,7 @@ export const FilteredDeviceList = forwardRef(
|
|||
setPushNotifications={setPushNotifications}
|
||||
toggleSelected={() => toggleSelection(device.device_id)}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
|
|
|
@ -186,7 +186,9 @@ const AccountUserSettingsTab: React.FC<IProps> = ({ closeSettingsFn }) => {
|
|||
canSetDisplayName={canSetDisplayName}
|
||||
canSetAvatar={canSetAvatar}
|
||||
/>
|
||||
<UserPersonalInfoSettings canMake3pidChanges={canMake3pidChanges} />
|
||||
{(!isAccountManagedExternally || canMake3pidChanges) && (
|
||||
<UserPersonalInfoSettings canMake3pidChanges={canMake3pidChanges} />
|
||||
)}
|
||||
<AccountSection
|
||||
canChangePassword={canChangePassword}
|
||||
onPasswordChanged={onPasswordChanged}
|
||||
|
|
|
@ -31,7 +31,7 @@ import QuestionDialog from "../../../dialogs/QuestionDialog";
|
|||
import { FilterVariation } from "../../devices/filter";
|
||||
import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionHeading";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { OidcLogoutDialog } from "../../../dialogs/oidc/OidcLogoutDialog";
|
||||
import { getManageDeviceUrl } from "../../../../../utils/oidc/urls.ts";
|
||||
import { SDKContext } from "../../../../../contexts/SDKContext";
|
||||
import Spinner from "../../../elements/Spinner";
|
||||
|
||||
|
@ -58,16 +58,6 @@ const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean>
|
|||
return !!confirmed;
|
||||
};
|
||||
|
||||
const confirmDelegatedAuthSignOut = async (delegatedAuthAccountUrl: string, deviceId: string): Promise<boolean> => {
|
||||
const { finished } = Modal.createDialog(OidcLogoutDialog, {
|
||||
deviceId,
|
||||
delegatedAuthAccountUrl,
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
|
||||
return !!confirmed;
|
||||
};
|
||||
|
||||
const useSignOut = (
|
||||
matrixClient: MatrixClient,
|
||||
onSignoutResolvedCallback: () => Promise<void>,
|
||||
|
@ -93,20 +83,10 @@ const useSignOut = (
|
|||
if (!deviceIds.length) {
|
||||
return;
|
||||
}
|
||||
// we can only sign out exactly one OIDC-aware device at a time
|
||||
// we should not encounter this
|
||||
if (delegatedAuthAccountUrl && deviceIds.length !== 1) {
|
||||
logger.warn("Unexpectedly tried to sign out multiple OIDC-aware devices.");
|
||||
return;
|
||||
}
|
||||
|
||||
// delegated auth logout flow confirms and signs out together
|
||||
// so only confirm if we are NOT doing a delegated auth sign out
|
||||
if (!delegatedAuthAccountUrl) {
|
||||
const userConfirmedSignout = await confirmSignOut(deviceIds.length);
|
||||
if (!userConfirmedSignout) {
|
||||
return;
|
||||
}
|
||||
const userConfirmedSignout = await confirmSignOut(deviceIds.length);
|
||||
if (!userConfirmedSignout) {
|
||||
return;
|
||||
}
|
||||
|
||||
let success = false;
|
||||
|
@ -115,11 +95,8 @@ const useSignOut = (
|
|||
|
||||
if (delegatedAuthAccountUrl) {
|
||||
const [deviceId] = deviceIds;
|
||||
try {
|
||||
success = await confirmDelegatedAuthSignOut(delegatedAuthAccountUrl, deviceId);
|
||||
} catch (error) {
|
||||
logger.error("Error deleting OIDC-aware sessions", error);
|
||||
}
|
||||
const url = getManageDeviceUrl(delegatedAuthAccountUrl, deviceId);
|
||||
window.open(url, "_blank");
|
||||
} else {
|
||||
const deferredSuccess = defer<boolean>();
|
||||
await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, async (success) => {
|
||||
|
@ -323,6 +300,7 @@ const SessionManagerTab: React.FC<{
|
|||
onSignOutCurrentDevice={onSignOutCurrentDevice}
|
||||
signOutAllOtherSessions={signOutAllOtherSessions}
|
||||
otherSessionsCount={otherSessionsCount}
|
||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
||||
/>
|
||||
{shouldShowOtherSessions && (
|
||||
<SettingsSubsection
|
||||
|
@ -356,7 +334,7 @@ const SessionManagerTab: React.FC<{
|
|||
setPushNotifications={setPushNotifications}
|
||||
ref={filteredDeviceListRef}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
disableMultipleSignout={disableMultipleSignout}
|
||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
)}
|
||||
|
|
|
@ -238,7 +238,6 @@
|
|||
"oidc": {
|
||||
"error_title": "We couldn't log you in",
|
||||
"generic_auth_error": "Something went wrong during authentication. Go to the sign in page and try again.",
|
||||
"logout_redirect_warning": "You will be redirected to your server's authentication provider to complete sign out.",
|
||||
"missing_or_invalid_stored_state": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again."
|
||||
},
|
||||
"password_field_keep_going_prompt": "Keep going…",
|
||||
|
@ -2821,6 +2820,7 @@
|
|||
"inactive_sessions_list_description": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.",
|
||||
"ip": "IP address",
|
||||
"last_activity": "Last activity",
|
||||
"manage": "Manage this session",
|
||||
"mobile_session": "Mobile session",
|
||||
"n_sessions_selected": {
|
||||
"one": "%(count)s session selected",
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a delegated auth account management URL with logout params as per MSC3824 and MSC2965
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/blob/hughns/sso-redirect-action/proposals/3824-oidc-aware-clients.md#definition-of-oidc-aware
|
||||
* https://github.com/sandhose/matrix-doc/blob/msc/sandhose/oidc-discovery/proposals/2965-oidc-discovery.md#account-management-url-parameters
|
||||
*/
|
||||
export const getOidcLogoutUrl = (delegatedAuthAccountUrl: string, deviceId: string): string => {
|
||||
const logoutUrl = new URL(delegatedAuthAccountUrl);
|
||||
logoutUrl.searchParams.set("action", "session_end");
|
||||
logoutUrl.searchParams.set("device_id", deviceId);
|
||||
|
||||
return logoutUrl.toString();
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
enum Action {
|
||||
Profile = "org.matrix.profile",
|
||||
SessionsList = "org.matrix.sessions_list",
|
||||
SessionView = "org.matrix.session_view",
|
||||
SessionEnd = "org.matrix.session_end",
|
||||
AccountDeactivate = "org.matrix.account_deactivate",
|
||||
CrossSigningReset = "org.matrix.cross_signing_reset",
|
||||
}
|
||||
|
||||
const getUrl = (authUrl: string, action: Action): URL => {
|
||||
const url = new URL(authUrl);
|
||||
url.searchParams.set("action", action);
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a delegated auth account management URL with logout params as per MSC4191
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/blob/quenting/account-deeplink/proposals/4191-account-deeplink.md#possible-actions
|
||||
*/
|
||||
export const getManageDeviceUrl = (delegatedAuthAccountUrl: string, deviceId: string): string => {
|
||||
const url = getUrl(delegatedAuthAccountUrl, Action.SessionView);
|
||||
url.searchParams.set("device_id", deviceId);
|
||||
return url.toString();
|
||||
};
|
|
@ -49,7 +49,7 @@ HTMLCollection [
|
|||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
Verify or sign out from this session for best security and reliability.
|
||||
Verify your current session for enhanced secure messaging.
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
|
|
|
@ -114,7 +114,7 @@ describe("<AccountUserSettingsTab />", () => {
|
|||
expect(manageAccountLink.getAttribute("href")).toMatch(accountManagementLink);
|
||||
});
|
||||
|
||||
describe("deactive account", () => {
|
||||
describe("deactivate account", () => {
|
||||
it("should not render section when account deactivation feature is disabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName !== UIFeature.Deactivate,
|
||||
|
@ -198,6 +198,11 @@ describe("<AccountUserSettingsTab />", () => {
|
|||
|
||||
describe("3pids", () => {
|
||||
beforeEach(() => {
|
||||
const mockOidcClientStore = {
|
||||
accountManagementEndpoint: undefined,
|
||||
} as unknown as OidcClientStore;
|
||||
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
||||
|
||||
mockClient.getCapabilities.mockResolvedValue({
|
||||
"m.3pid_changes": {
|
||||
enabled: true,
|
||||
|
|
|
@ -1234,34 +1234,13 @@ describe("<SessionManagerTab />", () => {
|
|||
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
const deviceDetails = getByTestId(`device-detail-${alicesMobileDevice.device_id}`);
|
||||
const signOutButton = deviceDetails.querySelector(
|
||||
const manageDeviceButton = deviceDetails.querySelector(
|
||||
'[data-testid="device-detail-sign-out-cta"]',
|
||||
) as Element;
|
||||
fireEvent.click(signOutButton);
|
||||
|
||||
await screen.findByRole("dialog");
|
||||
expect(
|
||||
screen.getByText(
|
||||
"You will be redirected to your server's authentication provider to complete sign out.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
// correct link to auth provider
|
||||
expect(screen.getByText("Continue")).toHaveAttribute(
|
||||
expect(manageDeviceButton).toHaveAttribute(
|
||||
"href",
|
||||
`https://issuer.org/account?action=session_end&device_id=${alicesMobileDevice.device_id}`,
|
||||
`https://issuer.org/account?action=org.matrix.session_view&device_id=${alicesMobileDevice.device_id}`,
|
||||
);
|
||||
|
||||
// go to the link
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
await flushPromises();
|
||||
|
||||
// come back from the link and close the modal
|
||||
fireEvent.click(screen.getByText("Close"));
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// devices were refreshed
|
||||
expect(mockClient.getDevices).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not allow removing multiple devices at once", async () => {
|
||||
|
|
|
@ -169,7 +169,7 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`<AccountUserSettingsTab /> deactive account should render section when account deactivation feature is enabled 1`] = `
|
||||
exports[`<AccountUserSettingsTab /> deactivate account should render section when account deactivation feature is enabled 1`] = `
|
||||
<div
|
||||
class="mx_SettingsSection"
|
||||
>
|
||||
|
|
Loading…
Reference in New Issue