mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-09-13 06:12:26 +03:00
Link device account management with OIDC (#2390)
* load auth metadata configs on startup * deep-link cross-signing reset button with oidc * deep-link manage devices and delete device with oidc * fix import typo
This commit is contained in:
parent
c30c142653
commit
c462a3b8d5
7 changed files with 185 additions and 52 deletions
|
@ -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);
|
||||
}
|
52
src/app/components/ServerConfigsLoader.tsx
Normal file
52
src/app/components/ServerConfigsLoader.tsx
Normal file
|
@ -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<ServerConfigs, unknown>(
|
||||
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);
|
||||
}
|
|
@ -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<Set<string>>(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
|
|||
<>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Others</Text>
|
||||
{authMetadata && (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Device Dashboard"
|
||||
description="Manage your devices on OIDC dashboard."
|
||||
after={
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={handleDashboardOIDC}
|
||||
>
|
||||
<Text size="B300">Open</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
)}
|
||||
{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={
|
||||
<DeviceDeleteBtn
|
||||
deviceId={device.device_id}
|
||||
deleted={deleted.has(device.device_id)}
|
||||
onDeleteToggle={handleToggleDelete}
|
||||
disabled={deleting}
|
||||
/>
|
||||
authMetadata ? (
|
||||
<DeviceDeleteBtn
|
||||
deviceId={device.device_id}
|
||||
deleted={false}
|
||||
onDeleteToggle={handleDeleteOIDC}
|
||||
/>
|
||||
) : (
|
||||
<DeviceDeleteBtn
|
||||
deviceId={device.device_id}
|
||||
deleted={deleted.has(device.device_id)}
|
||||
onDeleteToggle={handleToggleDelete}
|
||||
disabled={deleting}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{showVerification && crypto && (
|
||||
|
|
|
@ -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<RectCords>();
|
||||
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);
|
||||
};
|
||||
|
||||
|
|
17
src/app/hooks/useAccountManagement.ts
Normal file
17
src/app/hooks/useAccountManagement.ts
Normal file
|
@ -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;
|
||||
};
|
12
src/app/hooks/useAuthMetadata.ts
Normal file
12
src/app/hooks/useAuthMetadata.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ValidatedAuthMetadata } from 'matrix-js-sdk';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const AuthMetadataContext = createContext<ValidatedAuthMetadata | undefined>(undefined);
|
||||
|
||||
export const AuthMetadataProvider = AuthMetadataContext.Provider;
|
||||
|
||||
export const useAuthMetadata = (): ValidatedAuthMetadata | undefined => {
|
||||
const metadata = useContext(AuthMetadataContext);
|
||||
|
||||
return metadata;
|
||||
};
|
|
@ -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) {
|
|||
<ClientRootLoading />
|
||||
) : (
|
||||
<MatrixClientProvider value={mx}>
|
||||
<CapabilitiesAndMediaConfigLoader>
|
||||
{(capabilities, mediaConfig) => (
|
||||
<CapabilitiesProvider value={capabilities ?? {}}>
|
||||
<MediaConfigProvider value={mediaConfig ?? {}}>
|
||||
{children}
|
||||
<Windows />
|
||||
<Dialogs />
|
||||
<ReusableContextMenu />
|
||||
<ServerConfigsLoader>
|
||||
{(serverConfigs) => (
|
||||
<CapabilitiesProvider value={serverConfigs.capabilities ?? {}}>
|
||||
<MediaConfigProvider value={serverConfigs.mediaConfig ?? {}}>
|
||||
<AuthMetadataProvider value={serverConfigs.authMetadata}>
|
||||
{children}
|
||||
<Windows />
|
||||
<Dialogs />
|
||||
<ReusableContextMenu />
|
||||
</AuthMetadataProvider>
|
||||
</MediaConfigProvider>
|
||||
</CapabilitiesProvider>
|
||||
)}
|
||||
</CapabilitiesAndMediaConfigLoader>
|
||||
</ServerConfigsLoader>
|
||||
</MatrixClientProvider>
|
||||
)}
|
||||
</SpecVersions>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue