Skip to content

Commit 2d400be

Browse files
Kerryweeman1337
authored andcommitted
Device manager - prune client information events after remote sign out (matrix-org#9874)
* prune client infromation events from old devices * test client data pruning * remove debug * strict * Update src/components/views/settings/devices/useOwnDevices.ts Co-authored-by: Michael Weimann <michaelw@matrix.org> * improvements from review Co-authored-by: Michael Weimann <michaelw@matrix.org>
1 parent 525088b commit 2d400be

File tree

3 files changed

+80
-15
lines changed

3 files changed

+80
-15
lines changed

src/components/views/settings/devices/useOwnDevices.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto";
3535

3636
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
3737
import { _t } from "../../../../languageHandler";
38-
import { getDeviceClientInformation } from "../../../../utils/device/clientInformation";
38+
import { getDeviceClientInformation, pruneClientInformation } from "../../../../utils/device/clientInformation";
3939
import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types";
4040
import { useEventEmitter } from "../../../../hooks/useEventEmitter";
4141
import { parseUserAgent } from "../../../../utils/device/parseUserAgent";
@@ -116,8 +116,8 @@ export type DevicesState = {
116116
export const useOwnDevices = (): DevicesState => {
117117
const matrixClient = useContext(MatrixClientContext);
118118

119-
const currentDeviceId = matrixClient.getDeviceId();
120-
const userId = matrixClient.getUserId();
119+
const currentDeviceId = matrixClient.getDeviceId()!;
120+
const userId = matrixClient.getSafeUserId();
121121

122122
const [devices, setDevices] = useState<DevicesState["devices"]>({});
123123
const [pushers, setPushers] = useState<DevicesState["pushers"]>([]);
@@ -138,11 +138,6 @@ export const useOwnDevices = (): DevicesState => {
138138
const refreshDevices = useCallback(async () => {
139139
setIsLoadingDeviceList(true);
140140
try {
141-
// realistically we should never hit this
142-
// but it satisfies types
143-
if (!userId) {
144-
throw new Error("Cannot fetch devices without user id");
145-
}
146141
const devices = await fetchDevicesWithVerification(matrixClient, userId);
147142
setDevices(devices);
148143

@@ -176,6 +171,15 @@ export const useOwnDevices = (): DevicesState => {
176171
refreshDevices();
177172
}, [refreshDevices]);
178173

174+
useEffect(() => {
175+
const deviceIds = Object.keys(devices);
176+
// empty devices means devices have not been fetched yet
177+
// as there is always at least the current device
178+
if (deviceIds.length) {
179+
pruneClientInformation(deviceIds, matrixClient);
180+
}
181+
}, [devices, matrixClient]);
182+
179183
useEventEmitter(matrixClient, CryptoEvent.DevicesUpdated, (users: string[]): void => {
180184
if (users.includes(userId)) {
181185
refreshDevices();

src/utils/device/clientInformation.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ const formatUrl = (): string | undefined => {
4040
].join("");
4141
};
4242

43-
export const getClientInformationEventType = (deviceId: string): string =>
44-
`io.element.matrix_client_information.${deviceId}`;
43+
const clientInformationEventPrefix = "io.element.matrix_client_information.";
44+
export const getClientInformationEventType = (deviceId: string): string => `${clientInformationEventPrefix}${deviceId}`;
4545

4646
/**
4747
* Record extra client information for the current device
@@ -52,7 +52,7 @@ export const recordClientInformation = async (
5252
sdkConfig: IConfigOptions,
5353
platform: BasePlatform,
5454
): Promise<void> => {
55-
const deviceId = matrixClient.getDeviceId();
55+
const deviceId = matrixClient.getDeviceId()!;
5656
const { brand } = sdkConfig;
5757
const version = await platform.getAppVersion();
5858
const type = getClientInformationEventType(deviceId);
@@ -66,12 +66,27 @@ export const recordClientInformation = async (
6666
};
6767

6868
/**
69-
* Remove extra client information
70-
* @todo(kerrya) revisit after MSC3391: account data deletion is done
71-
* (PSBE-12)
69+
* Remove client information events for devices that no longer exist
70+
* @param validDeviceIds - ids of current devices,
71+
* client information for devices NOT in this list will be removed
72+
*/
73+
export const pruneClientInformation = (validDeviceIds: string[], matrixClient: MatrixClient): void => {
74+
Object.values(matrixClient.store.accountData).forEach((event) => {
75+
if (!event.getType().startsWith(clientInformationEventPrefix)) {
76+
return;
77+
}
78+
const [, deviceId] = event.getType().split(clientInformationEventPrefix);
79+
if (deviceId && !validDeviceIds.includes(deviceId)) {
80+
matrixClient.deleteAccountData(event.getType());
81+
}
82+
});
83+
};
84+
85+
/**
86+
* Remove extra client information for current device
7287
*/
7388
export const removeClientInformation = async (matrixClient: MatrixClient): Promise<void> => {
74-
const deviceId = matrixClient.getDeviceId();
89+
const deviceId = matrixClient.getDeviceId()!;
7590
const type = getClientInformationEventType(deviceId);
7691
const clientInformation = getDeviceClientInformation(matrixClient, deviceId);
7792

test/components/views/settings/tabs/user/SessionManagerTab-test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import LogoutDialog from "../../../../../../src/components/views/dialogs/LogoutD
4646
import { DeviceSecurityVariation, ExtendedDevice } from "../../../../../../src/components/views/settings/devices/types";
4747
import { INACTIVE_DEVICE_AGE_MS } from "../../../../../../src/components/views/settings/devices/filter";
4848
import SettingsStore from "../../../../../../src/settings/SettingsStore";
49+
import { getClientInformationEventType } from "../../../../../../src/utils/device/clientInformation";
4950

5051
mockPlatformPeg();
5152

@@ -87,6 +88,7 @@ describe("<SessionManagerTab />", () => {
8788
generateClientSecret: jest.fn(),
8889
setDeviceDetails: jest.fn(),
8990
getAccountData: jest.fn(),
91+
deleteAccountData: jest.fn(),
9092
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true),
9193
getPushers: jest.fn(),
9294
setPusher: jest.fn(),
@@ -182,6 +184,9 @@ describe("<SessionManagerTab />", () => {
182184
],
183185
});
184186

187+
// @ts-ignore mock
188+
mockClient.store = { accountData: {} };
189+
185190
mockClient.getAccountData.mockReset().mockImplementation((eventType) => {
186191
if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
187192
return new MatrixEvent({
@@ -667,6 +672,47 @@ describe("<SessionManagerTab />", () => {
667672
);
668673
});
669674

675+
it("removes account data events for devices after sign out", async () => {
676+
const mobileDeviceClientInfo = new MatrixEvent({
677+
type: getClientInformationEventType(alicesMobileDevice.device_id),
678+
content: {
679+
name: "test",
680+
},
681+
});
682+
// @ts-ignore setup mock
683+
mockClient.store = {
684+
// @ts-ignore setup mock
685+
accountData: {
686+
[mobileDeviceClientInfo.getType()]: mobileDeviceClientInfo,
687+
},
688+
};
689+
690+
mockClient.getDevices
691+
.mockResolvedValueOnce({
692+
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
693+
})
694+
.mockResolvedValueOnce({
695+
// refreshed devices after sign out
696+
devices: [alicesDevice],
697+
});
698+
699+
const { getByTestId, getByLabelText } = render(getComponent());
700+
701+
await act(async () => {
702+
await flushPromises();
703+
});
704+
705+
expect(mockClient.deleteAccountData).not.toHaveBeenCalled();
706+
707+
fireEvent.click(getByTestId("current-session-menu"));
708+
fireEvent.click(getByLabelText("Sign out of all other sessions (2)"));
709+
await confirmSignout(getByTestId);
710+
711+
// only called once for signed out device with account data event
712+
expect(mockClient.deleteAccountData).toHaveBeenCalledTimes(1);
713+
expect(mockClient.deleteAccountData).toHaveBeenCalledWith(mobileDeviceClientInfo.getType());
714+
});
715+
670716
describe("other devices", () => {
671717
const interactiveAuthError = { httpStatus: 401, data: { flows: [{ stages: ["m.login.password"] }] } };
672718

0 commit comments

Comments
 (0)