diff --git a/src/app/components/CapabilitiesAndMediaConfigLoader.tsx b/src/app/components/CapabilitiesAndMediaConfigLoader.tsx deleted file mode 100644 index 574d0ca7..00000000 --- a/src/app/components/CapabilitiesAndMediaConfigLoader.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ReactNode, useCallback, useEffect } from 'react'; -import { Capabilities } from 'matrix-js-sdk'; -import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; -import { useMatrixClient } from '../hooks/useMatrixClient'; -import { MediaConfig } from '../hooks/useMediaConfig'; -import { promiseFulfilledResult } from '../utils/common'; - -type CapabilitiesAndMediaConfigLoaderProps = { - children: (capabilities?: Capabilities, mediaConfig?: MediaConfig) => ReactNode; -}; -export function CapabilitiesAndMediaConfigLoader({ - children, -}: CapabilitiesAndMediaConfigLoaderProps) { - const mx = useMatrixClient(); - - const [state, load] = useAsyncCallback< - [Capabilities | undefined, MediaConfig | undefined], - unknown, - [] - >( - useCallback(async () => { - const result = await Promise.allSettled([mx.getCapabilities(), mx.getMediaConfig()]); - const capabilities = promiseFulfilledResult(result[0]); - const mediaConfig = promiseFulfilledResult(result[1]); - return [capabilities, mediaConfig]; - }, [mx]) - ); - - useEffect(() => { - load(); - }, [load]); - - const [capabilities, mediaConfig] = - state.status === AsyncStatus.Success ? state.data : [undefined, undefined]; - return children(capabilities, mediaConfig); -} diff --git a/src/app/components/ServerConfigsLoader.tsx b/src/app/components/ServerConfigsLoader.tsx new file mode 100644 index 00000000..3c8ce8eb --- /dev/null +++ b/src/app/components/ServerConfigsLoader.tsx @@ -0,0 +1,52 @@ +import { ReactNode, useCallback, useMemo } from 'react'; +import { Capabilities, validateAuthMetadata, ValidatedAuthMetadata } from 'matrix-js-sdk'; +import { AsyncStatus, useAsyncCallbackValue } from '../hooks/useAsyncCallback'; +import { useMatrixClient } from '../hooks/useMatrixClient'; +import { MediaConfig } from '../hooks/useMediaConfig'; +import { promiseFulfilledResult } from '../utils/common'; + +export type ServerConfigs = { + capabilities?: Capabilities; + mediaConfig?: MediaConfig; + authMetadata?: ValidatedAuthMetadata; +}; + +type ServerConfigsLoaderProps = { + children: (configs: ServerConfigs) => ReactNode; +}; +export function ServerConfigsLoader({ children }: ServerConfigsLoaderProps) { + const mx = useMatrixClient(); + const fallbackConfigs = useMemo(() => ({}), []); + + const [configsState] = useAsyncCallbackValue( + useCallback(async () => { + const result = await Promise.allSettled([ + mx.getCapabilities(), + mx.getMediaConfig(), + mx.getAuthMetadata(), + ]); + + const capabilities = promiseFulfilledResult(result[0]); + const mediaConfig = promiseFulfilledResult(result[1]); + const authMetadata = promiseFulfilledResult(result[2]); + let validatedAuthMetadata: ValidatedAuthMetadata | undefined; + + try { + validatedAuthMetadata = validateAuthMetadata(authMetadata); + } catch (e) { + console.error(e); + } + + return { + capabilities, + mediaConfig, + authMetadata: validatedAuthMetadata, + }; + }, [mx]) + ); + + const configs: ServerConfigs = + configsState.status === AsyncStatus.Success ? configsState.data : fallbackConfigs; + + return children(configs); +} diff --git a/src/app/features/settings/devices/OtherDevices.tsx b/src/app/features/settings/devices/OtherDevices.tsx index 0d879e59..4bd83dd6 100644 --- a/src/app/features/settings/devices/OtherDevices.tsx +++ b/src/app/features/settings/devices/OtherDevices.tsx @@ -11,6 +11,10 @@ import { useUIAMatrixError } from '../../../hooks/useUIAFlows'; import { DeviceVerificationStatus } from '../../../components/DeviceVerificationStatus'; import { VerifyOtherDeviceTile } from './Verification'; import { VerificationStatus } from '../../../hooks/useDeviceVerificationStatus'; +import { useAuthMetadata } from '../../../hooks/useAuthMetadata'; +import { withSearchParam } from '../../../pages/pathUtils'; +import { useAccountManagementActions } from '../../../hooks/useAccountManagement'; +import { SettingTile } from '../../../components/setting-tile'; type OtherDevicesProps = { devices: IMyDevice[]; @@ -20,8 +24,39 @@ type OtherDevicesProps = { export function OtherDevices({ devices, refreshDeviceList, showVerification }: OtherDevicesProps) { const mx = useMatrixClient(); const crypto = mx.getCrypto(); + const authMetadata = useAuthMetadata(); + const accountManagementActions = useAccountManagementActions(); + const [deleted, setDeleted] = useState>(new Set()); + const handleDashboardOIDC = useCallback(() => { + const authUrl = authMetadata?.account_management_uri ?? authMetadata?.issuer; + if (!authUrl) return; + + window.open( + withSearchParam(authUrl, { + action: accountManagementActions.sessionsList, + }), + '_blank' + ); + }, [authMetadata, accountManagementActions]); + + const handleDeleteOIDC = useCallback( + (deviceId: string) => { + const authUrl = authMetadata?.account_management_uri ?? authMetadata?.issuer; + if (!authUrl) return; + + window.open( + withSearchParam(authUrl, { + action: accountManagementActions.sessionEnd, + device_id: deviceId, + }), + '_blank' + ); + }, + [authMetadata, accountManagementActions] + ); + const handleToggleDelete = useCallback((deviceId: string) => { setDeleted((deviceIds) => { const newIds = new Set(deviceIds); @@ -70,6 +105,31 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O <> Others + {authMetadata && ( + + + Open + + } + /> + + )} {devices .sort((d1, d2) => { if (!d1.last_seen_ts || !d2.last_seen_ts) return 0; @@ -89,12 +149,20 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O refreshDeviceList={refreshDeviceList} disabled={deleting} options={ - + authMetadata ? ( + + ) : ( + + ) } /> {showVerification && crypto && ( diff --git a/src/app/features/settings/devices/Verification.tsx b/src/app/features/settings/devices/Verification.tsx index 59fa6b67..6c7eab17 100644 --- a/src/app/features/settings/devices/Verification.tsx +++ b/src/app/features/settings/devices/Verification.tsx @@ -32,6 +32,9 @@ import { DeviceVerificationSetup, } from '../../../components/DeviceVerificationSetup'; import { stopPropagation } from '../../../utils/keyboard'; +import { useAuthMetadata } from '../../../hooks/useAuthMetadata'; +import { withSearchParam } from '../../../pages/pathUtils'; +import { useAccountManagementActions } from '../../../hooks/useAccountManagement'; type VerificationStatusBadgeProps = { verificationStatus: VerificationStatus; @@ -252,6 +255,8 @@ export function EnableVerification({ visible }: EnableVerificationProps) { export function DeviceVerificationOptions() { const [menuCords, setMenuCords] = useState(); + const authMetadata = useAuthMetadata(); + const accountManagementActions = useAccountManagementActions(); const [reset, setReset] = useState(false); @@ -265,6 +270,18 @@ export function DeviceVerificationOptions() { const handleReset = () => { setMenuCords(undefined); + + if (authMetadata) { + const authUrl = authMetadata.account_management_uri ?? authMetadata.issuer; + window.open( + withSearchParam(authUrl, { + action: accountManagementActions.crossSigningReset, + }), + '_blank' + ); + return; + } + setReset(true); }; diff --git a/src/app/hooks/useAccountManagement.ts b/src/app/hooks/useAccountManagement.ts new file mode 100644 index 00000000..5eafedc4 --- /dev/null +++ b/src/app/hooks/useAccountManagement.ts @@ -0,0 +1,17 @@ +import { useMemo } from 'react'; + +export const useAccountManagementActions = () => { + const actions = useMemo( + () => ({ + 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', + }), + [] + ); + + return actions; +}; diff --git a/src/app/hooks/useAuthMetadata.ts b/src/app/hooks/useAuthMetadata.ts new file mode 100644 index 00000000..db967463 --- /dev/null +++ b/src/app/hooks/useAuthMetadata.ts @@ -0,0 +1,12 @@ +import { ValidatedAuthMetadata } from 'matrix-js-sdk'; +import { createContext, useContext } from 'react'; + +const AuthMetadataContext = createContext(undefined); + +export const AuthMetadataProvider = AuthMetadataContext.Provider; + +export const useAuthMetadata = (): ValidatedAuthMetadata | undefined => { + const metadata = useContext(AuthMetadataContext); + + return metadata; +}; diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index 846d8ff3..c48dbf53 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -25,7 +25,7 @@ import { } from '../../../client/initMatrix'; import { getSecret } from '../../../client/state/auth'; import { SplashScreen } from '../../components/splash-screen'; -import { CapabilitiesAndMediaConfigLoader } from '../../components/CapabilitiesAndMediaConfigLoader'; +import { ServerConfigsLoader } from '../../components/ServerConfigsLoader'; import { CapabilitiesProvider } from '../../hooks/useCapabilities'; import { MediaConfigProvider } from '../../hooks/useMediaConfig'; import { MatrixClientProvider } from '../../hooks/useMatrixClient'; @@ -37,6 +37,7 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { useSyncState } from '../../hooks/useSyncState'; import { stopPropagation } from '../../utils/keyboard'; import { SyncStatus } from './SyncStatus'; +import { AuthMetadataProvider } from '../../hooks/useAuthMetadata'; function ClientRootLoading() { return ( @@ -207,18 +208,20 @@ export function ClientRoot({ children }: ClientRootProps) { ) : ( - - {(capabilities, mediaConfig) => ( - - - {children} - - - + + {(serverConfigs) => ( + + + + {children} + + + + )} - + )}