diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -76,6 +76,7 @@ } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { userActionsP2PMessageTypes, + type PrimaryDeviceLogoutP2PMessage, type SecondaryDeviceLogoutP2PMessage, } from '../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; import type { @@ -253,11 +254,82 @@ }); function usePrimaryDeviceLogOut(): () => Promise { + const identityContext = React.useContext(IdentityClientContext); + if (!identityContext) { + throw new Error('Identity service client is not initialized'); + } + + const { sendMessage } = useTunnelbroker(); const broadcastDeviceListUpdates = useBroadcastDeviceListUpdates(); const foreignPeerDevices = useSelector(getForeignPeerDevices); const logOut = useLogOut(primaryDeviceLogOutOptions); return React.useCallback(async () => { + const { identityClient, getAuthMetadata } = identityContext; + const authMetadata = await getAuthMetadata(); + const { userID, deviceID: thisDeviceID } = authMetadata; + if (!thisDeviceID || !userID) { + throw new Error('No auth metadata'); + } + const { + devices: [primaryDeviceID, ...secondaryDevices], + } = await fetchLatestDeviceList(identityClient, userID); + if (thisDeviceID !== primaryDeviceID) { + throw new Error('Used primary device logout on a non-primary device'); + } + + // create and send Olm Tunnelbroker messages to secondaryDevices + const { olmAPI } = getConfig(); + await olmAPI.initializeCryptoAccount(); + const messageContents: PrimaryDeviceLogoutP2PMessage = { + type: userActionsP2PMessageTypes.LOG_OUT_PRIMARY_DEVICE, + }; + for (const deviceID of secondaryDevices) { + try { + const encryptedData = await olmAPI.encrypt( + JSON.stringify(messageContents), + deviceID, + ); + const encryptedMessage: EncryptedMessage = { + type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, + senderInfo: { deviceID: thisDeviceID, userID }, + encryptedData, + }; + await sendMessage({ + deviceID, + payload: JSON.stringify(encryptedMessage), + }); + } catch { + try { + await createOlmSessionWithPeer( + authMetadata, + identityClient, + sendMessage, + userID, + deviceID, + ); + const encryptedData = await olmAPI.encrypt( + JSON.stringify(messageContents), + deviceID, + ); + const encryptedMessage: EncryptedMessage = { + type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, + senderInfo: { deviceID: thisDeviceID, userID }, + encryptedData, + }; + await sendMessage({ + deviceID, + payload: JSON.stringify(encryptedMessage), + }); + } catch (err) { + console.warn( + `Error sending primary device logout message to device ${deviceID}:`, + err, + ); + } + } + } + // - logOut() performs device list update by calling Identity RPC // - broadcastDeviceListUpdates asks peers to download it from identity // so we need to call them in this order to make sure peers have latest @@ -267,7 +339,13 @@ const logOutResult = await logOut(); await broadcastDeviceListUpdates(foreignPeerDevices); return logOutResult; - }, [broadcastDeviceListUpdates, foreignPeerDevices, logOut]); + }, [ + broadcastDeviceListUpdates, + foreignPeerDevices, + identityContext, + logOut, + sendMessage, + ]); } const secondaryDeviceLogOutOptions = Object.freeze({