mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-09-13 14:22:25 +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 { DeviceVerificationStatus } from '../../../components/DeviceVerificationStatus';
|
||||||
import { VerifyOtherDeviceTile } from './Verification';
|
import { VerifyOtherDeviceTile } from './Verification';
|
||||||
import { VerificationStatus } from '../../../hooks/useDeviceVerificationStatus';
|
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 = {
|
type OtherDevicesProps = {
|
||||||
devices: IMyDevice[];
|
devices: IMyDevice[];
|
||||||
|
@ -20,8 +24,39 @@ type OtherDevicesProps = {
|
||||||
export function OtherDevices({ devices, refreshDeviceList, showVerification }: OtherDevicesProps) {
|
export function OtherDevices({ devices, refreshDeviceList, showVerification }: OtherDevicesProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const crypto = mx.getCrypto();
|
const crypto = mx.getCrypto();
|
||||||
|
const authMetadata = useAuthMetadata();
|
||||||
|
const accountManagementActions = useAccountManagementActions();
|
||||||
|
|
||||||
const [deleted, setDeleted] = useState<Set<string>>(new Set());
|
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) => {
|
const handleToggleDelete = useCallback((deviceId: string) => {
|
||||||
setDeleted((deviceIds) => {
|
setDeleted((deviceIds) => {
|
||||||
const newIds = new Set(deviceIds);
|
const newIds = new Set(deviceIds);
|
||||||
|
@ -70,6 +105,31 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
|
||||||
<>
|
<>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">Others</Text>
|
<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
|
{devices
|
||||||
.sort((d1, d2) => {
|
.sort((d1, d2) => {
|
||||||
if (!d1.last_seen_ts || !d2.last_seen_ts) return 0;
|
if (!d1.last_seen_ts || !d2.last_seen_ts) return 0;
|
||||||
|
@ -89,12 +149,20 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
|
||||||
refreshDeviceList={refreshDeviceList}
|
refreshDeviceList={refreshDeviceList}
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
options={
|
options={
|
||||||
<DeviceDeleteBtn
|
authMetadata ? (
|
||||||
deviceId={device.device_id}
|
<DeviceDeleteBtn
|
||||||
deleted={deleted.has(device.device_id)}
|
deviceId={device.device_id}
|
||||||
onDeleteToggle={handleToggleDelete}
|
deleted={false}
|
||||||
disabled={deleting}
|
onDeleteToggle={handleDeleteOIDC}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<DeviceDeleteBtn
|
||||||
|
deviceId={device.device_id}
|
||||||
|
deleted={deleted.has(device.device_id)}
|
||||||
|
onDeleteToggle={handleToggleDelete}
|
||||||
|
disabled={deleting}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{showVerification && crypto && (
|
{showVerification && crypto && (
|
||||||
|
|
|
@ -32,6 +32,9 @@ import {
|
||||||
DeviceVerificationSetup,
|
DeviceVerificationSetup,
|
||||||
} from '../../../components/DeviceVerificationSetup';
|
} from '../../../components/DeviceVerificationSetup';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
import { useAuthMetadata } from '../../../hooks/useAuthMetadata';
|
||||||
|
import { withSearchParam } from '../../../pages/pathUtils';
|
||||||
|
import { useAccountManagementActions } from '../../../hooks/useAccountManagement';
|
||||||
|
|
||||||
type VerificationStatusBadgeProps = {
|
type VerificationStatusBadgeProps = {
|
||||||
verificationStatus: VerificationStatus;
|
verificationStatus: VerificationStatus;
|
||||||
|
@ -252,6 +255,8 @@ export function EnableVerification({ visible }: EnableVerificationProps) {
|
||||||
|
|
||||||
export function DeviceVerificationOptions() {
|
export function DeviceVerificationOptions() {
|
||||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||||
|
const authMetadata = useAuthMetadata();
|
||||||
|
const accountManagementActions = useAccountManagementActions();
|
||||||
|
|
||||||
const [reset, setReset] = useState(false);
|
const [reset, setReset] = useState(false);
|
||||||
|
|
||||||
|
@ -265,6 +270,18 @@ export function DeviceVerificationOptions() {
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setMenuCords(undefined);
|
setMenuCords(undefined);
|
||||||
|
|
||||||
|
if (authMetadata) {
|
||||||
|
const authUrl = authMetadata.account_management_uri ?? authMetadata.issuer;
|
||||||
|
window.open(
|
||||||
|
withSearchParam(authUrl, {
|
||||||
|
action: accountManagementActions.crossSigningReset,
|
||||||
|
}),
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setReset(true);
|
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';
|
} from '../../../client/initMatrix';
|
||||||
import { getSecret } from '../../../client/state/auth';
|
import { getSecret } from '../../../client/state/auth';
|
||||||
import { SplashScreen } from '../../components/splash-screen';
|
import { SplashScreen } from '../../components/splash-screen';
|
||||||
import { CapabilitiesAndMediaConfigLoader } from '../../components/CapabilitiesAndMediaConfigLoader';
|
import { ServerConfigsLoader } from '../../components/ServerConfigsLoader';
|
||||||
import { CapabilitiesProvider } from '../../hooks/useCapabilities';
|
import { CapabilitiesProvider } from '../../hooks/useCapabilities';
|
||||||
import { MediaConfigProvider } from '../../hooks/useMediaConfig';
|
import { MediaConfigProvider } from '../../hooks/useMediaConfig';
|
||||||
import { MatrixClientProvider } from '../../hooks/useMatrixClient';
|
import { MatrixClientProvider } from '../../hooks/useMatrixClient';
|
||||||
|
@ -37,6 +37,7 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { useSyncState } from '../../hooks/useSyncState';
|
import { useSyncState } from '../../hooks/useSyncState';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import { SyncStatus } from './SyncStatus';
|
import { SyncStatus } from './SyncStatus';
|
||||||
|
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
|
||||||
|
|
||||||
function ClientRootLoading() {
|
function ClientRootLoading() {
|
||||||
return (
|
return (
|
||||||
|
@ -207,18 +208,20 @@ export function ClientRoot({ children }: ClientRootProps) {
|
||||||
<ClientRootLoading />
|
<ClientRootLoading />
|
||||||
) : (
|
) : (
|
||||||
<MatrixClientProvider value={mx}>
|
<MatrixClientProvider value={mx}>
|
||||||
<CapabilitiesAndMediaConfigLoader>
|
<ServerConfigsLoader>
|
||||||
{(capabilities, mediaConfig) => (
|
{(serverConfigs) => (
|
||||||
<CapabilitiesProvider value={capabilities ?? {}}>
|
<CapabilitiesProvider value={serverConfigs.capabilities ?? {}}>
|
||||||
<MediaConfigProvider value={mediaConfig ?? {}}>
|
<MediaConfigProvider value={serverConfigs.mediaConfig ?? {}}>
|
||||||
{children}
|
<AuthMetadataProvider value={serverConfigs.authMetadata}>
|
||||||
<Windows />
|
{children}
|
||||||
<Dialogs />
|
<Windows />
|
||||||
<ReusableContextMenu />
|
<Dialogs />
|
||||||
|
<ReusableContextMenu />
|
||||||
|
</AuthMetadataProvider>
|
||||||
</MediaConfigProvider>
|
</MediaConfigProvider>
|
||||||
</CapabilitiesProvider>
|
</CapabilitiesProvider>
|
||||||
)}
|
)}
|
||||||
</CapabilitiesAndMediaConfigLoader>
|
</ServerConfigsLoader>
|
||||||
</MatrixClientProvider>
|
</MatrixClientProvider>
|
||||||
)}
|
)}
|
||||||
</SpecVersions>
|
</SpecVersions>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue