diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts index 9f4d842a1f..e66bbee048 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts @@ -95,21 +95,23 @@ describe('ConnectionsManagerService', () => { 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' ) - const launchCommunityPayload: InitCommunityPayload = { + + // Using the factory includes extra properties that affect the assertion + // below + const actualCommunity = { id: community.id, + peerList: [remotePeer], + } + await localDbService.setCommunity(actualCommunity) + await localDbService.setCurrentCommunityId(community.id) + // TODO: Revisit this when we move the Identity model to the backend, since + // this network data lives in that model. + const network = { peerId: userIdentity.peerId, hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - CA: [communityRootCa], - }, - peers: [remotePeer], } + await localDbService.setNetworkInfo(network) - await localDbService.put(LocalDBKeys.COMMUNITY, launchCommunityPayload) await connectionsManagerService.closeAllServices() const launchCommunitySpy = jest.spyOn(connectionsManagerService, 'launchCommunity').mockResolvedValue() @@ -117,7 +119,10 @@ describe('ConnectionsManagerService', () => { await connectionsManagerService.init() const localPeerAddress = createLibp2pAddress(userIdentity.hiddenService.onionAddress, userIdentity.peerId.id) - const updatedLaunchCommunityPayload = { ...launchCommunityPayload, peers: [localPeerAddress, remotePeer] } + const updatedLaunchCommunityPayload = { + community: { ...actualCommunity, peerList: [localPeerAddress, remotePeer] }, + network, + } expect(launchCommunitySpy).toHaveBeenCalledWith(updatedLaunchCommunityPayload) }) @@ -129,20 +134,13 @@ describe('ConnectionsManagerService', () => { expect(launchCommunitySpy).not.toHaveBeenCalled() }) - // At this moment, that test have to be skipped, because checking statues is called before launchCommunity method - it.skip('community is only launched once', async () => { - const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - CA: [communityRootCa], + it('community is only launched once', async () => { + const launchCommunityPayload = { + community: community, + network: { + peerId: userIdentity.peerId, + hiddenService: userIdentity.hiddenService, }, - peers: community.peerList, } //@ts-ignore @@ -157,22 +155,15 @@ describe('ConnectionsManagerService', () => { }) it('Bug reproduction - Error on startup - Error: TOR: Connection already established - Trigger launchCommunity from backend and state manager', async () => { - const launchCommunityPayload: InitCommunityPayload = { - id: community.id, + await localDbService.setCommunity(community) + await localDbService.setCurrentCommunityId(community.id) + // TODO: Revisit this when we move the Identity model to the backend, since + // this network data lives in that model. + const network = { peerId: userIdentity.peerId, hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - CA: [communityRootCa], - }, - peers: community.peerList, } - - // await connectionsManager.init() - await localDbService.put(LocalDBKeys.COMMUNITY, launchCommunityPayload) + await localDbService.setNetworkInfo(network) const peerid = 'QmaEvCkpUG7GxhgvMkk8wxurfi1ehjHhSUNRksWTmXN2ix' await localDbService.put(LocalDBKeys.PEERS, { diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts index f545231518..fb41bf4aa2 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts @@ -135,22 +135,6 @@ describe('Connections manager', () => { } const emitSpy = jest.spyOn(libp2pService, 'emit') - const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - CA: [communityRootCa], - }, - peers: community.peerList, - } - - await localDbService.put(LocalDBKeys.COMMUNITY, launchCommunityPayload) - // Peer connected await connectionsManagerService.init() libp2pService.connectedPeers.set(peerId.toString(), DateTime.utc().valueOf()) @@ -206,19 +190,15 @@ describe('Connections manager', () => { ) } - const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error Identity.userCertificate can be null - certificate: userIdentity.userCertificate, - // @ts-expect-error Identity.userCertificate userCsr.userKey can be undefined - key: userIdentity.userCsr?.userKey, - // @ts-expect-error - CA: [community.rootCa], + const launchCommunityPayload = { + community: { + id: community.id, + peerList, + }, + network: { + peerId: userIdentity.peerId, + hiddenService: userIdentity.hiddenService, }, - peers: peerList, } await connectionsManagerService.init() await connectionsManagerService.launchCommunity(launchCommunityPayload) @@ -248,19 +228,15 @@ describe('Connections manager', () => { ) } - const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error Identity.userCertificate can be null - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - // @ts-expect-error - CA: [community.rootCa], + const launchCommunityPayload = { + community: { + id: community.id, + peerList, + }, + network: { + peerId: userIdentity.peerId, + hiddenService: userIdentity.hiddenService, }, - peers: peerList, } await connectionsManagerService.launchCommunity(launchCommunityPayload) diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.ts index be707b27b4..b3b29e226e 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -29,6 +29,7 @@ import { NetworkDataPayload, NetworkInfo, NetworkStats, + type SavedOwnerCertificatePayload, PushNotificationPayload, RegisterOwnerCertificatePayload, RemoveDownloadStatus, @@ -61,7 +62,8 @@ import { StorageEvents } from '../storage/storage.types' import { LazyModuleLoader } from '@nestjs/core' import Logger from '../common/logger' import { emitError } from '../socket/socket.errors' -import { isPSKcodeValid } from '@quiet/common' +import { createLibp2pAddress, isPSKcodeValid } from '@quiet/common' +import { CertFieldsTypes, createRootCA, getCertFieldValue, loadCertificate } from '@quiet/identity' @Injectable() export class ConnectionsManagerService extends EventEmitter implements OnModuleInit { @@ -138,7 +140,6 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } this.attachSocketServiceListeners() - this.attachRegistrationListeners() this.attachTorEventsListeners() this.attachStorageListeners() @@ -147,26 +148,75 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } if (this.configOptions.torControlPort) { - console.log('launch 1') + await this.migrateLevelDb() await this.launchCommunityFromStorage() } } + /** + * Migrate LevelDB when upgrading Quiet for existing communities + * + * Move data from Redux in the frontend to LevelDB in the backend for existing + * communities when upgrading. Hopefully this will make features easier to + * test and develop. In order to do this, we need the data to be accessible on + * the backend before it's first used. Since the backend starts up + * asynchronously, independent of the frontend, we wait for the frontend to + * load migration data before launching the community. + */ + public async migrateLevelDb(): Promise { + // Empty promise used to wait on a callback below + let onDataReceived: () => void + const dataReceivedPromise = new Promise((resolve: () => void) => { + onDataReceived = resolve + }) + // This is related to a specific migration, perhaps there is a way to + // encapsulate this in LocalDbService. + const keys = [LocalDBKeys.CURRENT_COMMUNITY_ID, LocalDBKeys.COMMUNITIES] + const keysRequired: string[] = [] + + for (const key of keys) { + if (!(await this.localDbService.exists(key))) { + keysRequired.push(key) + } + } + + this.socketService.on(SocketActionTypes.LOAD_MIGRATION_DATA, async (data: Record) => { + this.logger('Migrating LevelDB') + await this.localDbService.load(data) + onDataReceived() + }) + + // Only require migration data for existing communities. We can tell because + // they are using the deprecated COMMUNITY key in LevelDB. This is related + // to a specific migration. Perhaps we want a more general purpose migration + // mechanism, like a table to hold migrations that have already been + // applied. + if ((await this.localDbService.exists(LocalDBKeys.COMMUNITY)) && keysRequired.length > 0) { + this.logger('Migration data required:', keysRequired) + this.serverIoProvider.io.emit(SocketActionTypes.MIGRATION_DATA_REQUIRED, keysRequired) + await dataReceivedPromise + } else { + this.logger('Nothing to migrate') + } + } + public async launchCommunityFromStorage() { - this.logger('launchCommunityFromStorage') + this.logger('Launching community from storage') - const community: InitCommunityPayload = await this.localDbService.get(LocalDBKeys.COMMUNITY) - this.logger('launchCommunityFromStorage - community peers', community?.peers) - if (community) { - const sortedPeers = await this.localDbService.getSortedPeers(community.peers) + const community = await this.localDbService.getCurrentCommunity() + // TODO: Revisit this when we move the Identity model to the backend, since + // this network data lives in that model. + const network = await this.localDbService.getNetworkInfo() + + if (community && network) { + const sortedPeers = await this.localDbService.getSortedPeers(community.peerList) this.logger('launchCommunityFromStorage - sorted peers', sortedPeers) if (sortedPeers.length > 0) { - community.peers = sortedPeers + community.peerList = sortedPeers } - await this.localDbService.put(LocalDBKeys.COMMUNITY, community) - if ([ServiceState.LAUNCHING, ServiceState.LAUNCHED].includes(this.communityState)) return - this.communityState = ServiceState.LAUNCHING - await this.launchCommunity(community) + await this.localDbService.setCommunity(community) + + await this.launchCommunity({ community, network }) } } @@ -233,8 +283,10 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI public async getNetwork(): Promise { const hiddenService = await this.tor.createNewHiddenService({ targetPort: this.ports.libp2pHiddenService }) - await this.tor.destroyHiddenService(hiddenService.onionAddress.split('.')[0]) + + // TODO: Do we want to create the PeerId here? It doesn't necessarily have + // anything to do with Tor. const peerId: PeerId = await PeerId.create() const peerIdJson = peerId.toJSON() this.logger(`Created network for peer ${peerId.toString()}. Address: ${hiddenService.onionAddress}`) @@ -260,106 +312,176 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI return } + // TODO: Should we save this network info in LevelDB at this point? return network } - private async generatePSK() { - const pskBase64 = Libp2pService.generateLibp2pPSK().psk - await this.localDbService.put(LocalDBKeys.PSK, pskBase64) - this.logger('Generated Libp2p PSK') - this.serverIoProvider.io.emit(SocketActionTypes.LIBP2P_PSK_STORED, { psk: pskBase64 }) - } - - public async createCommunity(payload: InitCommunityPayload) { + public async createCommunity(payload: InitCommunityPayload): Promise { this.logger('Creating community: peers:', payload.peers) - await this.generatePSK() + if (!payload.CA || !payload.rootCa) { + this.logger.error('CA and rootCa are required to create community') + return + } + + if (!payload.ownerCsr) { + this.logger.error('ownerCsr is required to create community') + return + } + + const psk = Libp2pService.generateLibp2pPSK().psk + let ownerCertResult: SavedOwnerCertificatePayload + + try { + this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.REGISTERING_OWNER_CERTIFICATE) + ownerCertResult = await this.registrationService.registerOwnerCertificate({ + communityId: payload.id, + userCsr: payload.ownerCsr, + permsData: { + certificate: payload.CA.rootCertString, + privKey: payload.CA.rootKeyString, + }, + }) + } catch (e) { + this.logger.error('Failed to register owner certificate') + return + } + + const localAddress = createLibp2pAddress(payload.hiddenService.onionAddress, payload.peerId.id) - await this.launchCommunity(payload) - this.logger(`Created and launched community ${payload.id}`) - this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_CREATED, { id: payload.id }) + const community = { + id: payload.id, + name: payload.name, + CA: payload.CA, + rootCa: payload.rootCa, + peerList: [localAddress], + ownerCertificate: ownerCertResult.network.certificate, + psk: psk, + } + + const network = { + hiddenService: payload.hiddenService, + peerId: payload.peerId, + } + + await this.localDbService.setCommunity(community) + await this.localDbService.setCurrentCommunityId(community.id) + // TODO: Revisit this when we move the Identity model to the backend, since + // this network data lives in that model. + await this.localDbService.setNetworkInfo(network) + + await this.launchCommunity({ community, network }) + this.logger(`Created and launched community ${community.id}`) + + return community } - public async launchCommunity(payload: InitCommunityPayload) { - this.logger('Launching community: peers:', payload.peers) - this.communityState = ServiceState.LAUNCHING + public async joinCommunity(payload: InitCommunityPayload): Promise { + this.logger('Joining community: peers:', payload.peers) - // TODO: Move community creation to the backend so that - // launchCommunity/createCommunity return a Community object to the - // frontend. Also deprecate the COMMUNITY/PSK/OWNER_ORBIT_DB_IDENTITY - // IndexDB keys in favor of COMMUNITIES/CURRENT_COMMUNITY_ID/IDENTITIES, - // mirroring the frontend state so that we can easily move things from the - // frontend to the backend. - const communityData: InitCommunityPayload = await this.localDbService.get(LocalDBKeys.COMMUNITY) - if (!communityData) { - await this.localDbService.put(LocalDBKeys.COMMUNITY, payload) + if (!payload.peers || payload.peers.length === 0) { + this.logger.error('Joining community: Peers required') + return } - const psk = payload.psk - if (psk) { - this.logger('Launching community: received Libp2p PSK') - if (!isPSKcodeValid(psk)) { - this.logger.error('Launching community: received Libp2p PSK is not valid') - emitError(this.serverIoProvider.io, { - type: SocketActionTypes.LAUNCH_COMMUNITY, - message: ErrorMessages.NETWORK_SETUP_FAILED, - community: payload.id, - }) - return - } - await this.localDbService.put(LocalDBKeys.PSK, psk) + if (!payload.psk || !isPSKcodeValid(payload.psk)) { + this.logger.error('Joining community: Libp2p PSK is not valid') + emitError(this.serverIoProvider.io, { + type: SocketActionTypes.LAUNCH_COMMUNITY, + message: ErrorMessages.NETWORK_SETUP_FAILED, + community: payload.id, + }) + return } - const ownerOrbitDbIdentity = payload.ownerOrbitDbIdentity - if (ownerOrbitDbIdentity) { - this.logger("Creating network: received owner's OrbitDB identity") - await this.localDbService.putOwnerOrbitDbIdentity(ownerOrbitDbIdentity) + if (!payload.ownerOrbitDbIdentity) { + this.logger.error('Joining community: ownerOrbitDbIdentity is not valid') + emitError(this.serverIoProvider.io, { + type: SocketActionTypes.LAUNCH_COMMUNITY, + message: ErrorMessages.NETWORK_SETUP_FAILED, + community: payload.id, + }) + return + } + + const localAddress = createLibp2pAddress(payload.hiddenService.onionAddress, payload.peerId.id) + + const community = { + id: payload.id, + peerList: [...new Set([localAddress, ...payload.peers])], + psk: payload.psk, + ownerOrbitDbIdentity: payload.ownerOrbitDbIdentity, + } + + const network = { + hiddenService: payload.hiddenService, + peerId: payload.peerId, + } + + await this.localDbService.setCommunity(community) + await this.localDbService.setCurrentCommunityId(community.id) + // TODO: Revisit this when we move the Identity model to the backend, since + // this network data lives in that model. + await this.localDbService.setNetworkInfo(network) + + await this.launchCommunity({ community, network }) + this.logger(`Joined and launched community ${community.id}`) + + return community + } + + public async launchCommunity({ community, network }: { community: Community; network: NetworkInfo }) { + if ([ServiceState.LAUNCHING, ServiceState.LAUNCHED].includes(this.communityState)) { + this.logger.error( + 'Cannot launch community more than once.' + + ' Community has already been launched or is currently being launched.' + ) + return } + this.communityState = ServiceState.LAUNCHING try { - await this.launch(payload) + await this.launch({ community, network }) } catch (e) { - this.logger(`Couldn't launch community for peer ${payload.peerId.id}.`, e) + this.logger(`Couldn't launch community for peer ${network.peerId.id}.`, e) emitError(this.serverIoProvider.io, { type: SocketActionTypes.LAUNCH_COMMUNITY, message: ErrorMessages.COMMUNITY_LAUNCH_FAILED, - community: payload.id, + community: community.id, trace: e.stack, }) return } - this.logger(`Launched community ${payload.id}`) + this.logger(`Launched community ${community.id}`) this.serverIoProvider.io.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.COMMUNITY_LAUNCHED) - this.communityId = payload.id + this.communityId = community.id this.communityState = ServiceState.LAUNCHED - console.log('Hunting for heisenbug: Backend initialized community and sent event to state manager') - // Unblock websocket endpoints this.socketService.resolveReadyness() - this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { id: payload.id }) + this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { id: community.id }) } - public async spawnTorHiddenService(payload: InitCommunityPayload): Promise { - this.logger(`Spawning hidden service for community ${payload.id}, peer: ${payload.peerId.id}`) + public async spawnTorHiddenService(communityId: string, network: NetworkInfo): Promise { + this.logger(`Spawning hidden service for community ${communityId}, peer: ${network.peerId.id}`) this.serverIoProvider.io.emit( SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.SPAWNING_HIDDEN_SERVICE ) return await this.tor.spawnHiddenService({ targetPort: this.ports.libp2pHiddenService, - privKey: payload.hiddenService.privateKey, + privKey: network.hiddenService.privateKey, }) } - public async launch(payload: InitCommunityPayload) { - this.logger(`Launching community ${payload.id}: peer: ${payload.peerId.id}`) + public async launch({ community, network }: { community: Community; network: NetworkInfo }) { + this.logger(`Launching community ${community.id}: peer: ${network.peerId.id}`) - const onionAddress = await this.spawnTorHiddenService(payload) + const onionAddress = await this.spawnTorHiddenService(community.id, network) const { Libp2pModule } = await import('../libp2p/libp2p.module') const moduleRef = await this.lazyModuleLoader.load(() => Libp2pModule) @@ -367,37 +489,28 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI const lazyService = moduleRef.get(Libp2pService) this.libp2pService = lazyService - const restoredRsa = await PeerId.createFromJSON(payload.peerId) - const _peerId = await peerIdFromKeys(restoredRsa.marshalPubKey(), restoredRsa.marshalPrivKey()) + const restoredRsa = await PeerId.createFromJSON(network.peerId) + const peerId = await peerIdFromKeys(restoredRsa.marshalPubKey(), restoredRsa.marshalPrivKey()) - let peers = payload.peers - this.logger(`Launching community ${payload.id}: payload peers: ${peers}`) - if (!peers || peers.length === 0) { - peers = [this.libp2pService.createLibp2pAddress(onionAddress, _peerId.toString())] - } + const peers = community.peerList + this.logger(`Launching community ${community.id}: payload peers: ${peers}`) - const pskValue: string = await this.localDbService.get(LocalDBKeys.PSK) - if (!pskValue) { - throw new Error('No psk in local db') - } - this.logger(`Launching community ${payload.id}: retrieved Libp2p PSK`) - - const libp2pPSK = Libp2pService.generateLibp2pPSK(pskValue).fullKey const params: Libp2pNodeParams = { - peerId: _peerId, + peerId, listenAddresses: [this.libp2pService.createLibp2pListenAddress(onionAddress)], agent: this.socksProxyAgent, - localAddress: this.libp2pService.createLibp2pAddress(onionAddress, _peerId.toString()), + localAddress: this.libp2pService.createLibp2pAddress(onionAddress, peerId.toString()), targetPort: this.ports.libp2pHiddenService, - peers, - psk: libp2pPSK, + peers: peers ?? [], + psk: Libp2pService.generateLibp2pPSK(community.psk).fullKey, } - await this.libp2pService.createInstance(params) + // Libp2p event listeners this.libp2pService.on(Libp2pEvents.PEER_CONNECTED, (payload: { peers: string[] }) => { this.serverIoProvider.io.emit(SocketActionTypes.PEER_CONNECTED, payload) }) + this.libp2pService.on(Libp2pEvents.PEER_DISCONNECTED, async (payload: NetworkDataPayload) => { const peerPrevStats = await this.localDbService.find(LocalDBKeys.PEERS, payload.peer) const prev = peerPrevStats?.connectionTime || 0 @@ -414,7 +527,8 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI // BARTEK: Potentially obsolete to send this to state-manager this.serverIoProvider.io.emit(SocketActionTypes.PEER_DISCONNECTED, payload) }) - await this.storageService.init(_peerId) + + await this.storageService.init(peerId) // We can use Nest for dependency injection, but I think since the // registration service depends on the storage service being // initialized, this is helpful to manually inject the storage @@ -440,15 +554,6 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI }) } - private attachRegistrationListeners() { - this.registrationService.on(SocketActionTypes.OWNER_CERTIFICATE_ISSUED, payload => { - this.serverIoProvider.io.emit(SocketActionTypes.OWNER_CERTIFICATE_ISSUED, payload) - }) - this.registrationService.on(RegistrationEvents.ERROR, payload => { - emitError(this.serverIoProvider.io, payload) - }) - } - private attachSocketServiceListeners() { // Community this.socketService.on(SocketActionTypes.CONNECTION, async () => { @@ -470,24 +575,43 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI }) this.socketService.on( SocketActionTypes.CREATE_NETWORK, - async (communityId: string, callback: (response?: NetworkInfo) => void) => { + async (communityId: string, callback: (response: NetworkInfo | undefined) => void) => { this.logger(`socketService - ${SocketActionTypes.CREATE_NETWORK}`) callback(await this.createNetwork(communityId)) } ) - this.socketService.on(SocketActionTypes.CREATE_COMMUNITY, async (args: InitCommunityPayload) => { - await this.createCommunity(args) - }) - this.socketService.on(SocketActionTypes.LAUNCH_COMMUNITY, async (args: InitCommunityPayload) => { - this.logger(`socketService - ${SocketActionTypes.LAUNCH_COMMUNITY}`) - if ([ServiceState.LAUNCHING, ServiceState.LAUNCHED].includes(this.communityState)) return - this.communityState = ServiceState.LAUNCHING - await this.launchCommunity(args) - }) + this.socketService.on( + SocketActionTypes.CREATE_COMMUNITY, + async (args: InitCommunityPayload, callback: (response: Community | undefined) => void) => { + this.logger(`socketService - ${SocketActionTypes.CREATE_COMMUNITY}`) + callback(await this.createCommunity(args)) + } + ) + // TODO: Rename to JOIN_COMMUNITY? + this.socketService.on( + SocketActionTypes.LAUNCH_COMMUNITY, + async (args: InitCommunityPayload, callback: (response: Community | undefined) => void) => { + this.logger(`socketService - ${SocketActionTypes.LAUNCH_COMMUNITY}`) + callback(await this.joinCommunity(args)) + } + ) + // TODO: With the Community model on the backend, there is no need to call + // SET_COMMUNITY_METADATA anymore. We can call updateCommunityMetadata when + // creating the community. this.socketService.on( SocketActionTypes.SET_COMMUNITY_METADATA, - async (payload: CommunityMetadata, callback: (response?: CommunityMetadata) => void) => { - const meta = await this.storageService?.updateCommunityMetadata(payload) + async (payload: CommunityMetadata, callback: (response: CommunityMetadata | undefined) => void) => { + const meta = await this.storageService.updateCommunityMetadata(payload) + const community = await this.localDbService.getCurrentCommunity() + + if (meta && community) { + const updatedCommunity = { + ...community, + ownerOrbitDbIdentity: meta.ownerOrbitDbIdentity, + } + await this.localDbService.setCommunity(updatedCommunity) + this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_UPDATED, updatedCommunity) + } callback(meta) } ) @@ -500,15 +624,9 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.logger(`socketService - ${SocketActionTypes.ADD_CSR}`) await this.storageService?.saveCSR(payload) }) - this.socketService.on( - SocketActionTypes.REGISTER_OWNER_CERTIFICATE, - async (args: RegisterOwnerCertificatePayload) => { - await this.registrationService.registerOwnerCertificate(args) - } - ) - // TODO: Save community CA data in LevelDB. Perhaps save the - // entire Community type in LevelDB. We can probably do this once - // when creating the community. + // TODO: With the Community model on the backend, there is no need to call + // SET_COMMUNITY_CA_DATA anymore. We can call setPermsData when + // creating the community. this.socketService.on(SocketActionTypes.SET_COMMUNITY_CA_DATA, async (payload: PermsData) => { this.logger(`socketService - ${SocketActionTypes.SET_COMMUNITY_CA_DATA}`) this.registrationService.setPermsData(payload) @@ -609,8 +727,8 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.storageService.on(StorageEvents.MESSAGE_MEDIA_UPDATED, (payload: FileMetadata) => { this.serverIoProvider.io.emit(SocketActionTypes.MESSAGE_MEDIA_UPDATED, payload) }) - this.storageService.on(StorageEvents.UPDATE_PEERS_LIST, (payload: StorePeerListPayload) => { - this.serverIoProvider.io.emit(SocketActionTypes.PEER_LIST, payload) + this.storageService.on(StorageEvents.COMMUNITY_UPDATED, (payload: Community) => { + this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_UPDATED, payload) }) this.storageService.on(StorageEvents.SEND_PUSH_NOTIFICATION, (payload: PushNotificationPayload) => { this.serverIoProvider.io.emit(SocketActionTypes.PUSH_NOTIFICATION, payload) @@ -623,7 +741,27 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI }) this.storageService.on(StorageEvents.COMMUNITY_METADATA_STORED, async (meta: CommunityMetadata) => { this.logger(`Storage - ${StorageEvents.COMMUNITY_METADATA_STORED}: ${meta}`) - this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_METADATA_STORED, meta) + const community = await this.localDbService.getCurrentCommunity() + + if (community) { + const rootCaCert = loadCertificate(meta.rootCa) + const communityName = getCertFieldValue(rootCaCert, CertFieldsTypes.commonName) + + if (!communityName) { + this.logger.error(`Could not retrieve ${CertFieldsTypes.commonName} from CommunityMetadata.rootCa`) + } + + const updatedCommunity = { + ...community, + name: communityName ?? undefined, + rootCa: meta.rootCa, + ownerCertificate: meta.ownerCertificate, + ownerOrbitDbIdentity: meta.ownerOrbitDbIdentity, + } + await this.localDbService.setCommunity(updatedCommunity) + + this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_UPDATED, updatedCommunity) + } }) this.storageService.on(StorageEvents.USER_PROFILES_STORED, (payload: UserProfilesStoredEvent) => { this.serverIoProvider.io.emit(SocketActionTypes.USER_PROFILES_STORED, payload) diff --git a/packages/backend/src/nest/libp2p/libp2p.types.ts b/packages/backend/src/nest/libp2p/libp2p.types.ts index 1f9d8c9fb7..985c3a4907 100644 --- a/packages/backend/src/nest/libp2p/libp2p.types.ts +++ b/packages/backend/src/nest/libp2p/libp2p.types.ts @@ -17,12 +17,3 @@ export interface Libp2pNodeParams { peers: string[] psk: Uint8Array } - -export interface InitLibp2pParams { - peerId: any - address: string - addressPort: number - targetPort: number - bootstrapMultiaddrs: string[] - certs: Certificates -} diff --git a/packages/backend/src/nest/local-db/local-db.service.ts b/packages/backend/src/nest/local-db/local-db.service.ts index 327a7b43f1..6c85552ac8 100644 --- a/packages/backend/src/nest/local-db/local-db.service.ts +++ b/packages/backend/src/nest/local-db/local-db.service.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common' import { Level } from 'level' -import { InitCommunityPayload, NetworkStats } from '@quiet/types' +import { type Community, type Identity, InitCommunityPayload, type NetworkInfo, NetworkStats } from '@quiet/types' import { createLibp2pAddress, filterAndSortPeers } from '@quiet/common' import { LEVEL_DB } from '../const' import { LocalDBKeys, LocalDbStatus } from './local-db.types' @@ -42,6 +42,10 @@ export class LocalDbService { return data } + public async exists(key: string): Promise { + return Boolean(await this.get(key)) + } + public async put(key: string, value: any) { await this.db.put(key, value) } @@ -72,23 +76,86 @@ export class LocalDbService { } } + public async delete(key: string) { + await this.db.del(key) + } + + public async load(data: any) { + for (const key in data) { + if (typeof data[key] === 'object' && Object.keys(data[key]).length === 0) { + continue + } + if (typeof data[key] === 'string' && data[key].length === 0) { + continue + } + if (Array.isArray(data[key]) && data[key].length === 0) { + continue + } + await this.put(key, data[key]) + } + } + public async getSortedPeers(peers: string[] = []): Promise { const peersStats = (await this.get(LocalDBKeys.PEERS)) || {} const stats: NetworkStats[] = Object.values(peersStats) - const community: InitCommunityPayload = await this.get(LocalDBKeys.COMMUNITY) - if (!community) { + const network = await this.getNetworkInfo() + + if (network) { + const localPeerAddress = createLibp2pAddress(network.hiddenService.onionAddress, network.peerId.id) + this.logger('Local peer', localPeerAddress) + return filterAndSortPeers(peers, stats, localPeerAddress) + } else { return filterAndSortPeers(peers, stats) } - const localPeerAddress = createLibp2pAddress(community.hiddenService.onionAddress, community.peerId.id) - this.logger('Local peer', localPeerAddress) - return filterAndSortPeers(peers, stats, localPeerAddress) } - public async putOwnerOrbitDbIdentity(id: string): Promise { - this.put(LocalDBKeys.OWNER_ORBIT_DB_IDENTITY, id) + public async setCommunity(community: Community) { + let communities = await this.get(LocalDBKeys.COMMUNITIES) + if (!communities) { + communities = {} + } + communities[community.id] = community + await this.put(LocalDBKeys.COMMUNITIES, communities) + } + + public async setCurrentCommunityId(communityId: string) { + await this.put(LocalDBKeys.CURRENT_COMMUNITY_ID, communityId) } - public async getOwnerOrbitDbIdentity(): Promise { - return this.get(LocalDBKeys.OWNER_ORBIT_DB_IDENTITY) + public async getCommunities(): Promise> { + return await this.get(LocalDBKeys.COMMUNITIES) + } + + public async getCurrentCommunity(): Promise { + const currentCommunityId = await this.get(LocalDBKeys.CURRENT_COMMUNITY_ID) + const communities = await this.get(LocalDBKeys.COMMUNITIES) + + return communities?.[currentCommunityId] + } + + public async communityExists(communityId: string): Promise { + return communityId in ((await this.getCommunities()) ?? {}) + } + + // These are potentially temporary functions to help us migrate data to the + // backend. Currently this information lives under the COMMUNITY key in + // LevelDB, but on the frontend this data lives in the Identity model. So we + // may want to keep this data in the Identity model in LevelDB (when we + // migrate it from the frontend) and have getIdentity/setIdentity functions. + public async setNetworkInfo(network: NetworkInfo) { + await this.put(LocalDBKeys.COMMUNITY, network) + } + + // These are potentially temporary functions to help us migrate data to the + // backend. Currently this information lives under the COMMUNITY key in + // LevelDB, but on the frontend this data lives in the Identity model. So we + // may want to keep this data in the Identity model in LevelDB (when we + // migrate it from the frontend) and have getIdentity/setIdentity functions. + public async getNetworkInfo(): Promise { + const initCommunityPayload = await this.get(LocalDBKeys.COMMUNITY) + + return initCommunityPayload + ? { peerId: initCommunityPayload.peerId, hiddenService: initCommunityPayload.hiddenService } + : undefined } } diff --git a/packages/backend/src/nest/local-db/local-db.types.ts b/packages/backend/src/nest/local-db/local-db.types.ts index aded572cec..f3ea58ed9c 100644 --- a/packages/backend/src/nest/local-db/local-db.types.ts +++ b/packages/backend/src/nest/local-db/local-db.types.ts @@ -1,8 +1,19 @@ export enum LocalDBKeys { - COMMUNITY = 'community', - REGISTRAR = 'registrar', + // Record of Community objects + COMMUNITIES = 'communities', + // ID of current community + CURRENT_COMMUNITY_ID = 'currentCommunityId', + // Record of peer details PEERS = 'peers', + + // TODO: Deprecate this once we move the Identity data model to the backend + // (and delete the data from LevelDB). + COMMUNITY = 'community', + // TODO: Deprecate this soon (and delete the data from LevelDB). This data + // exists in the Community object. PSK = 'psk', + // TODO: Deprecate this soon (and delete the data from LevelDB). This data + // exists in the Community object. OWNER_ORBIT_DB_IDENTITY = 'ownerOrbitDbIdentity', } export type LocalDbStatus = 'opening' | 'open' | 'closing' | 'closed' diff --git a/packages/backend/src/nest/registration/registration.service.ts b/packages/backend/src/nest/registration/registration.service.ts index cb74153492..9c04e2bb56 100644 --- a/packages/backend/src/nest/registration/registration.service.ts +++ b/packages/backend/src/nest/registration/registration.service.ts @@ -1,7 +1,14 @@ import { Injectable, OnModuleInit } from '@nestjs/common' import { EventEmitter } from 'events' import { extractPendingCsrs, issueCertificate } from './registration.functions' -import { ErrorCodes, ErrorMessages, PermsData, RegisterOwnerCertificatePayload, SocketActionTypes } from '@quiet/types' +import { + ErrorCodes, + ErrorMessages, + PermsData, + RegisterOwnerCertificatePayload, + type SavedOwnerCertificatePayload, + SocketActionTypes, +} from '@quiet/types' import { RegistrationEvents } from './registration.types' import Logger from '../common/logger' import { StorageService } from '../storage/storage.service' @@ -93,21 +100,22 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { ) } - public async registerOwnerCertificate(payload: RegisterOwnerCertificatePayload): Promise { + // TODO: This doesn't save the owner's certificate in OrbitDB, so perhaps we + // should rename it or look into refactoring so that it does save to OrbitDB. + // However, currently, this is called before the storage service is + // initialized. + public async registerOwnerCertificate( + payload: RegisterOwnerCertificatePayload + ): Promise { this.permsData = payload.permsData const result = await issueCertificate(payload.userCsr.userCsr, this.permsData) if (result?.cert) { - this.emit(SocketActionTypes.OWNER_CERTIFICATE_ISSUED, { + return { communityId: payload.communityId, network: { certificate: result.cert }, - }) + } } else { - this.emit(SocketActionTypes.ERROR, { - type: SocketActionTypes.REGISTER_OWNER_CERTIFICATE, - code: ErrorCodes.SERVER_ERROR, - message: ErrorMessages.REGISTRATION_FAILED, - community: payload.communityId, - }) + throw new Error('Failed to register owner certificate') } } diff --git a/packages/backend/src/nest/socket/socket.service.spec.ts b/packages/backend/src/nest/socket/socket.service.spec.ts index 79089e4410..56d30c1275 100644 --- a/packages/backend/src/nest/socket/socket.service.spec.ts +++ b/packages/backend/src/nest/socket/socket.service.spec.ts @@ -59,7 +59,6 @@ describe('SocketService', () => { SocketActionTypes.CREATE_NETWORK.valueOf(), SocketActionTypes.CREATE_COMMUNITY.valueOf(), SocketActionTypes.LAUNCH_COMMUNITY.valueOf(), - SocketActionTypes.REGISTER_OWNER_CERTIFICATE.valueOf(), SocketActionTypes.REGISTER_USER_CERTIFICATE.valueOf(), SocketActionTypes.ADD_CSR.valueOf(), SocketActionTypes.SET_COMMUNITY_METADATA.valueOf(), diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts index 0057cc0e51..32d99659fd 100644 --- a/packages/backend/src/nest/socket/socket.service.ts +++ b/packages/backend/src/nest/socket/socket.service.ts @@ -143,28 +143,27 @@ export class SocketService extends EventEmitter implements OnModuleInit { this.emit(SocketActionTypes.ADD_CSR, payload) }) - socket.on(SocketActionTypes.REGISTER_OWNER_CERTIFICATE, async (payload: RegisterOwnerCertificatePayload) => { - this.logger(`Registering owner certificate (${payload.communityId})`) - - this.emit(SocketActionTypes.REGISTER_OWNER_CERTIFICATE, payload) - this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.REGISTERING_OWNER_CERTIFICATE) - }) - // ====== Community ====== - socket.on(SocketActionTypes.CREATE_COMMUNITY, async (payload: InitCommunityPayload) => { - this.logger(`Creating community ${payload.id}`) - this.emit(SocketActionTypes.CREATE_COMMUNITY, payload) - }) + socket.on( + SocketActionTypes.CREATE_COMMUNITY, + async (payload: InitCommunityPayload, callback: (response: Community | undefined) => void) => { + this.logger(`Creating community ${payload.id}`) + this.emit(SocketActionTypes.CREATE_COMMUNITY, payload, callback) + } + ) - socket.on(SocketActionTypes.LAUNCH_COMMUNITY, async (payload: InitCommunityPayload) => { - this.logger(`Launching community ${payload.id} for ${payload.peerId.id}`) - this.emit(SocketActionTypes.LAUNCH_COMMUNITY, payload) - this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.LAUNCHING_COMMUNITY) - }) + socket.on( + SocketActionTypes.LAUNCH_COMMUNITY, + async (payload: InitCommunityPayload, callback: (response: Community | undefined) => void) => { + this.logger(`Launching community ${payload.id} for ${payload.peerId.id}`) + this.emit(SocketActionTypes.LAUNCH_COMMUNITY, payload, callback) + this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.LAUNCHING_COMMUNITY) + } + ) socket.on( SocketActionTypes.CREATE_NETWORK, - async (communityId: string, callback: (response?: NetworkInfo) => void) => { + async (communityId: string, callback: (response: NetworkInfo | undefined) => void) => { this.logger(`Creating network for community ${communityId}`) this.emit(SocketActionTypes.CREATE_NETWORK, communityId, callback) } @@ -182,7 +181,7 @@ export class SocketService extends EventEmitter implements OnModuleInit { socket.on( SocketActionTypes.SET_COMMUNITY_METADATA, - (payload: CommunityMetadata, callback: (response?: CommunityMetadata) => void) => { + (payload: CommunityMetadata, callback: (response: CommunityMetadata | undefined) => void) => { this.emit(SocketActionTypes.SET_COMMUNITY_METADATA, payload, callback) } ) @@ -196,6 +195,12 @@ export class SocketService extends EventEmitter implements OnModuleInit { socket.on(SocketActionTypes.SET_USER_PROFILE, (profile: UserProfile) => { this.emit(SocketActionTypes.SET_USER_PROFILE, profile) }) + + // ====== Misc ====== + + socket.on(SocketActionTypes.LOAD_MIGRATION_DATA, async (data: Record) => { + this.emit(SocketActionTypes.LOAD_MIGRATION_DATA, data) + }) }) } diff --git a/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.spec.ts b/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.spec.ts index fd7b7d568f..6c49a5604d 100644 --- a/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.spec.ts +++ b/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.spec.ts @@ -40,9 +40,13 @@ describe('CommmunityMetadataStore', () => { let community: Community const mockLocalDbService = { - putOwnerOrbitDbIdentity: jest.fn(), - // @ts-ignore - OrbitDB's type definition doesn't include identity - getOwnerOrbitDbIdentity: jest.fn(() => orbitDbService.orbitDb.identity.id), + setCommunity: jest.fn(), + getCurrentCommunity: jest.fn(() => { + return { + // @ts-ignore - OrbitDB's type definition doesn't include identity + ownerOrbitDbIdentity: orbitDbService.orbitDb.identity.id, + } + }), } beforeAll(async () => { diff --git a/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.ts b/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.ts index 65ce085150..9acbb661be 100644 --- a/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.ts +++ b/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.ts @@ -123,9 +123,14 @@ export class CommunityMetadataStore extends EventEmitter { ownerOrbitDbIdentity, } - // putOwnerOrbitDbIdentity goes before store.put because the - // store's KeyValueIndex calls getOwnerOrbitDbIdentity - this.localDbService.putOwnerOrbitDbIdentity(ownerOrbitDbIdentity) + // Updating this here before store.put because the store's KeyValueIndex + // then uses the updated Community object. + const community = await this.localDbService.getCurrentCommunity() + if (community) { + await this.localDbService.setCommunity({ ...community, ownerOrbitDbIdentity }) + } else { + throw new Error('Current community missing') + } // FIXME: I think potentially there is a subtle developer // experience bug here. Internally OrbitDB will call @@ -170,7 +175,8 @@ export class CommunityMetadataStore extends EventEmitter { return false } - const ownerOrbitDbIdentity = await localDbService.getOwnerOrbitDbIdentity() + const community = await localDbService.getCurrentCommunity() + const ownerOrbitDbIdentity = community?.ownerOrbitDbIdentity if (!ownerOrbitDbIdentity) { logger.error('Failed to verify community metadata entry:', entry.hash, 'owner identity is invalid') return false diff --git a/packages/backend/src/nest/storage/storage.service.spec.ts b/packages/backend/src/nest/storage/storage.service.spec.ts index 7c0abb0594..0cfdef6888 100644 --- a/packages/backend/src/nest/storage/storage.service.spec.ts +++ b/packages/backend/src/nest/storage/storage.service.spec.ts @@ -164,7 +164,8 @@ describe('StorageService', () => { expect(ipfsService.ipfsInstance).not.toBeNull() expect(localDbService.getStatus()).toEqual('open') - await localDbService.put(LocalDBKeys.COMMUNITY, community) + await localDbService.setCommunity(community) + await localDbService.setCurrentCommunityId(community.id) }) afterEach(async () => { diff --git a/packages/backend/src/nest/storage/storage.service.ts b/packages/backend/src/nest/storage/storage.service.ts index 53988be1ff..6ff620570d 100644 --- a/packages/backend/src/nest/storage/storage.service.ts +++ b/packages/backend/src/nest/storage/storage.service.ts @@ -316,13 +316,16 @@ export class StorageService extends EventEmitter { const users = this.getAllUsers() const peers = users.map(peer => createLibp2pAddress(peer.onionAddress, peer.peerId)) console.log('updatePeersList, peers count:', peers.length) - const community = await this.localDbService.get(LocalDBKeys.COMMUNITY) + + const community = await this.localDbService.getCurrentCommunity() + if (!community) return + const sortedPeers = await this.localDbService.getSortedPeers(peers) if (sortedPeers.length > 0) { - community.peers = sortedPeers - await this.localDbService.put(LocalDBKeys.COMMUNITY, community) + community.peerList = sortedPeers + await this.localDbService.setCommunity(community) } - this.emit(StorageEvents.UPDATE_PEERS_LIST, { communityId: community.id, peerList: peers }) + this.emit(StorageEvents.COMMUNITY_UPDATED, community) } public async loadAllCertificates() { @@ -500,22 +503,28 @@ export class StorageService extends EventEmitter { db.events.on('replicated', async address => { this.logger('Replicated.', address) const ids = this.getAllEventLogEntries(db).map(msg => msg.id) - const community = await this.localDbService.get(LocalDBKeys.COMMUNITY) - this.emit(StorageEvents.MESSAGE_IDS_STORED, { - ids, - channelId: channelData.id, - communityId: community.id, - }) + const community = await this.localDbService.getCurrentCommunity() + + if (community) { + this.emit(StorageEvents.MESSAGE_IDS_STORED, { + ids, + channelId: channelData.id, + communityId: community.id, + }) + } }) db.events.on('ready', async () => { const ids = this.getAllEventLogEntries(db).map(msg => msg.id) - const community = await this.localDbService.get(LocalDBKeys.COMMUNITY) - this.emit(StorageEvents.MESSAGE_IDS_STORED, { - ids, - channelId: channelData.id, - communityId: community.id, - }) + const community = await this.localDbService.getCurrentCommunity() + + if (community) { + this.emit(StorageEvents.MESSAGE_IDS_STORED, { + ids, + channelId: channelData.id, + communityId: community.id, + }) + } }) await db.load() diff --git a/packages/backend/src/nest/storage/storage.types.ts b/packages/backend/src/nest/storage/storage.types.ts index 599739059f..40949570c6 100644 --- a/packages/backend/src/nest/storage/storage.types.ts +++ b/packages/backend/src/nest/storage/storage.types.ts @@ -1,8 +1,6 @@ import { Certificates } from '@quiet/types' export enum StorageEvents { - // Peers - UPDATE_PEERS_LIST = 'updatePeersList', // Public Channels CHANNEL_SUBSCRIBED = 'channelSubscribed', CHANNELS_STORED = 'channelsStored', @@ -22,15 +20,7 @@ export enum StorageEvents { USER_PROFILES_STORED = 'userProfilesStored', // Community COMMUNITY_METADATA_STORED = 'communityMetadataStored', -} - -export interface InitStorageParams { - communityId: string - peerId: any - onionAddress: string - targetPort: number - peers?: string[] - certs: Certificates + COMMUNITY_UPDATED = 'communityUpdated', } export interface CsrReplicatedPromiseValues { diff --git a/packages/desktop/src/renderer/Root.tsx b/packages/desktop/src/renderer/Root.tsx index 8cc8cb6089..32c931de55 100644 --- a/packages/desktop/src/renderer/Root.tsx +++ b/packages/desktop/src/renderer/Root.tsx @@ -35,9 +35,11 @@ import DuplicateModalContainer from './components/widgets/userLabel/duplicate/Du import UsernameTakenModalContainer from './components/widgets/usernameTakenModal/UsernameTakenModal.container' import PossibleImpersonationAttackModalContainer from './components/widgets/possibleImpersonationAttackModal/PossibleImpersonationAttackModal.container' import BreakingChangesWarning from './containers/widgets/breakingChangesWarning/BreakingChangesWarning' +import { communities } from '@quiet/state-manager' // Trigger lerna export const persistor = persistStore(store) + export default () => { return ( diff --git a/packages/desktop/src/renderer/store/reducers.ts b/packages/desktop/src/renderer/store/reducers.ts index 14a778fcd0..2cea0e75a8 100644 --- a/packages/desktop/src/renderer/store/reducers.ts +++ b/packages/desktop/src/renderer/store/reducers.ts @@ -2,7 +2,7 @@ import { AnyAction, combineReducers } from '@reduxjs/toolkit' import ElectronStore from 'electron-store' import createElectronStorage from 'redux-persist-electron-storage' import path from 'path' -import { persistReducer } from 'redux-persist' +import { createMigrate, persistReducer } from 'redux-persist' import stateManagerReducers, { storeKeys as StateManagerStoreKeys, @@ -14,6 +14,7 @@ import stateManagerReducers, { ConnectionTransform, resetStateAndSaveTorConnectionData, UsersTransform, + storeMigrations, } from '@quiet/state-manager' import { StoreType } from './handlers/types' @@ -45,6 +46,7 @@ const reduxStorage = createElectronStorage({ electronStore: store }) const persistConfig = { key: 'root', + version: 0, storage: reduxStorage, throttle: 1000, whitelist: [ @@ -66,6 +68,7 @@ const persistConfig = { ConnectionTransform, UsersTransform, ], + migrate: createMigrate(storeMigrations, { debug: true }), } export const reducers = { diff --git a/packages/desktop/src/rtl-tests/channel.main.test.tsx b/packages/desktop/src/rtl-tests/channel.main.test.tsx index 514c485fc5..924111e61b 100644 --- a/packages/desktop/src/rtl-tests/channel.main.test.tsx +++ b/packages/desktop/src/rtl-tests/channel.main.test.tsx @@ -767,7 +767,7 @@ describe('Channel', () => { const uploadingDelay = 100 - jest.spyOn(socket, 'emit').mockImplementation(async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { + const mockEmitImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.LAUNCH_COMMUNITY) { const data = input[1] as InitCommunityPayload @@ -816,7 +816,11 @@ describe('Channel', () => { peerId: alice.peerId.id, }) } - }) + } + + jest.spyOn(socket, 'emit').mockImplementation(mockEmitImpl) + // @ts-ignore + socket.emitWithAck = mockEmitImpl const { store, runSaga } = await prepareStore( initialState.getState(), @@ -899,7 +903,7 @@ describe('Channel', () => { const community: Community = await factory.create< ReturnType['payload'] - >('Community', { rootCa: 'rootCa', privateKey: 'privateKey' }) + >('Community', { rootCa: 'rootCa' }) const alice = await factory.create['payload']>('Identity', { id: community.id, @@ -964,7 +968,7 @@ describe('Channel', () => { }) ) - jest.spyOn(socket, 'emit').mockImplementation(async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { + const mockEmitImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.LAUNCH_COMMUNITY) { const data = input[1] as InitCommunityPayload @@ -983,7 +987,11 @@ describe('Channel', () => { path: `${__dirname}/test-image.jpeg`, }) } - }) + } + + jest.spyOn(socket, 'emit').mockImplementation(mockEmitImpl) + // @ts-ignore + socket.emitWithAck = mockEmitImpl const { store, runSaga } = await prepareStore( initialState.getState(), diff --git a/packages/desktop/src/rtl-tests/community.create.test.tsx b/packages/desktop/src/rtl-tests/community.create.test.tsx index e7ba494763..44653b7730 100644 --- a/packages/desktop/src/rtl-tests/community.create.test.tsx +++ b/packages/desktop/src/rtl-tests/community.create.test.tsx @@ -14,10 +14,15 @@ import { CreateCommunityDictionary } from '../renderer/components/CreateJoinComm import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' import { socketEventData } from '../renderer/testUtils/socket' -import { Community, type NetworkInfo, SavedOwnerCertificatePayload, SocketActionTypes } from '@quiet/types' +import { + Community, + type InitCommunityPayload, + type NetworkInfo, + SavedOwnerCertificatePayload, + SocketActionTypes, +} from '@quiet/types' import { ChannelsReplicatedPayload, - InitCommunityPayload, publicChannels, RegisterOwnerCertificatePayload, ResponseLaunchCommunityPayload, @@ -70,8 +75,6 @@ describe('User', () => { const mockEmitImpl = (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.CREATE_NETWORK) { - const data = input[1] as Community - const payload = { ...data, privateKey: 'privateKey' } return { hiddenService: { onionAddress: 'onionAddress', @@ -82,23 +85,11 @@ describe('User', () => { }, } } - if (action === SocketActionTypes.REGISTER_OWNER_CERTIFICATE) { - const payload = input[1] as RegisterOwnerCertificatePayload - socket.socketClient.emit(SocketActionTypes.OWNER_CERTIFICATE_ISSUED, { - communityId: payload.communityId, - network: { - certificate: payload.permsData.certificate, - }, - }) - } if (action === SocketActionTypes.CREATE_COMMUNITY) { const payload = input[1] as InitCommunityPayload socket.socketClient.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { id: payload.id, }) - socket.socketClient.emit(SocketActionTypes.COMMUNITY_CREATED, { - id: payload.id, - }) socket.socketClient.emit(SocketActionTypes.CHANNELS_STORED, { channels: { @@ -111,6 +102,8 @@ describe('User', () => { }, }, }) + + return { id: payload.id, ownerCertificate: 'cert' } } } @@ -173,22 +166,21 @@ describe('User', () => { "Network/setLoadingPanelType", "Modals/openModal", "Identity/registerCertificate", - "Communities/updateCommunity", - "Identity/storeUserCertificate", - "Identity/savedOwnerCertificate", - "Communities/updateCommunityData", + "Communities/createCommunity", "Communities/sendCommunityCaData", "Files/checkForMissingFiles", "Network/addInitializedCommunity", "Communities/clearInvitationCodes", + "PublicChannels/channelsReplicated", + "Communities/updateCommunityData", + "PublicChannels/addChannel", + "Identity/storeUserCertificate", + "Messages/addPublicChannelsMessagesBase", "Communities/sendCommunityMetadata", "PublicChannels/createGeneralChannel", - "Identity/saveUserCsr", - "PublicChannels/channelsReplicated", "PublicChannels/createChannel", - "PublicChannels/addChannel", + "Identity/saveUserCsr", "PublicChannels/setCurrentChannel", - "Messages/addPublicChannelsMessagesBase", "PublicChannels/clearUnreadChannel", "Modals/closeModal", "Messages/lazyLoading", diff --git a/packages/desktop/src/rtl-tests/community.join.test.tsx b/packages/desktop/src/rtl-tests/community.join.test.tsx index 9ddf17c4e7..26a5c6ea57 100644 --- a/packages/desktop/src/rtl-tests/community.join.test.tsx +++ b/packages/desktop/src/rtl-tests/community.join.test.tsx @@ -17,7 +17,6 @@ import { socketEventData } from '../renderer/testUtils/socket' import { communities, RegisterUserCertificatePayload, - InitCommunityPayload, ErrorCodes, ErrorMessages, getFactory, @@ -32,6 +31,7 @@ import { ChannelSubscribedPayload, Community, ErrorPayload, + type InitCommunityPayload, type NetworkInfo, ResponseLaunchCommunityPayload, SocketActionTypes, @@ -85,10 +85,9 @@ describe('User', () => { const factory = await getFactory(store) - const mockImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { + const mockEmitImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.CREATE_NETWORK) { - const payload = input[1] as Community return { hiddenService: { onionAddress: 'onionAddress', @@ -123,9 +122,9 @@ describe('User', () => { } } - jest.spyOn(socket, 'emit').mockImplementation(mockImpl) + jest.spyOn(socket, 'emit').mockImplementation(mockEmitImpl) // @ts-ignore - socket.emitWithAck = mockImpl + socket.emitWithAck = mockEmitImpl // Log all the dispatched actions in order const actions: AnyAction[] = [] @@ -171,13 +170,12 @@ describe('User', () => { expect(actions).toMatchInlineSnapshot(` Array [ "Communities/createNetwork", - "Communities/setInvitationCodes", - "Communities/savePSK", "Communities/addNewCommunity", "Communities/setCurrentCommunity", + "Communities/setInvitationCodes", + "Identity/addNewIdentity", "Modals/closeModal", "Modals/openModal", - "Identity/addNewIdentity", "Identity/registerUsername", "Network/setLoadingPanelType", "Modals/openModal", @@ -228,11 +226,10 @@ describe('User', () => { store ) - jest.spyOn(socket, 'emit').mockImplementation(async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { + const mockEmitImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.CREATE_NETWORK) { - const payload = input[1] as Community - return socket.socketClient.emit(SocketActionTypes.NETWORK_CREATED, { + return { hiddenService: { onionAddress: 'onionAddress', privateKey: 'privKey', @@ -240,7 +237,7 @@ describe('User', () => { peerId: { id: 'peerId', }, - }) + } } if (action === SocketActionTypes.REGISTER_USER_CERTIFICATE) { const payload = input[1] as RegisterUserCertificatePayload @@ -253,7 +250,11 @@ describe('User', () => { community: community?.id, }) } - }) + } + + jest.spyOn(socket, 'emit').mockImplementation(mockEmitImpl) + // @ts-ignore + socket.emitWithAck = mockEmitImpl // Log all the dispatched actions in order const actions: AnyAction[] = [] @@ -313,11 +314,10 @@ describe('User', () => { store ) - jest.spyOn(socket, 'emit').mockImplementation(async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { + const mockEmitImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.CREATE_NETWORK) { - const payload = input[1] as Community - return socket.socketClient.emit(SocketActionTypes.NETWORK_CREATED, { + return { hiddenService: { onionAddress: 'onionAddress', privateKey: 'privKey', @@ -325,9 +325,12 @@ describe('User', () => { peerId: { id: 'peerId', }, - }) + } } - }) + } + + // @ts-ignore + socket.emitWithAck = mockEmitImpl // Log all the dispatched actions in order const actions: AnyAction[] = [] diff --git a/packages/desktop/src/rtl-tests/customProtocol.test.tsx b/packages/desktop/src/rtl-tests/customProtocol.test.tsx index 9cacc8ece9..33a3e363bf 100644 --- a/packages/desktop/src/rtl-tests/customProtocol.test.tsx +++ b/packages/desktop/src/rtl-tests/customProtocol.test.tsx @@ -29,8 +29,6 @@ describe('Opening app through custom protocol', () => { rootCa: '', peerList: [], onionAddress: '', - privateKey: '', - port: 0, ownerCertificate: '', } diff --git a/packages/desktop/src/rtl-tests/deep.linking.test.tsx b/packages/desktop/src/rtl-tests/deep.linking.test.tsx index 0fc27b8317..653029d1b1 100644 --- a/packages/desktop/src/rtl-tests/deep.linking.test.tsx +++ b/packages/desktop/src/rtl-tests/deep.linking.test.tsx @@ -51,10 +51,9 @@ describe('Deep linking', () => { Array [ "Communities/customProtocol", "Communities/createNetwork", - "Communities/setInvitationCodes", - "Communities/savePSK", "Communities/addNewCommunity", "Communities/setCurrentCommunity", + "Communities/setInvitationCodes", "Communities/customProtocol", ] `) diff --git a/packages/desktop/src/rtl-tests/generalChannel.create.test.tsx b/packages/desktop/src/rtl-tests/generalChannel.create.test.tsx deleted file mode 100644 index 858ce482c6..0000000000 --- a/packages/desktop/src/rtl-tests/generalChannel.create.test.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react' -import { act } from 'react-dom/test-utils' -import '@testing-library/jest-dom/extend-expect' -import { apply, fork, take } from 'typed-redux-saga' -import { renderComponent } from '../renderer/testUtils/renderComponent' -import { prepareStore } from '../renderer/testUtils/prepareStore' -import MockedSocket from 'socket.io-mock' -import { ioMock } from '../shared/setupTests' -import { socketEventData } from '../renderer/testUtils/socket' -import { AnyAction } from 'redux' -import { identity, publicChannels, getFactory, ChannelsReplicatedPayload } from '@quiet/state-manager' -import { SocketActionTypes } from '@quiet/types' -import Channel from '../renderer/components/Channel/Channel' - -jest.setTimeout(20_000) - -describe('General channel', () => { - let socket: MockedSocket - let communityId: string - - beforeEach(() => { - socket = new MockedSocket() - ioMock.mockImplementation(() => socket) - window.ResizeObserver = jest.fn().mockImplementation(() => ({ - observe: jest.fn(), - unobserve: jest.fn(), - disconnect: jest.fn(), - })) - }) - - it('create automatically along with creating community', async () => { - const { store, runSaga } = await prepareStore( - {}, - socket // Fork state manager's sagas - ) - - window.HTMLElement.prototype.scrollTo = jest.fn() - - renderComponent( - <> - - , - store - ) - - const factory = await getFactory(store) - - await factory.create['payload']>('Identity', { - nickname: 'alice', - }) - - const mockImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { - const action = input[0] - if (action === SocketActionTypes.CREATE_CHANNEL) { - const payload = input[1] as ChannelsReplicatedPayload - expect(payload.channels.channel?.name).toEqual('general') - } - } - - jest.spyOn(socket, 'emit').mockImplementation(mockImpl) - // @ts-ignore - socket.emitWithAck = mockImpl - - // Log all the dispatched actions in order - const actions: AnyAction[] = [] - runSaga(function* (): Generator { - while (true) { - const action = yield* take() - actions.push(action.type) - } - }) - - await act(async () => { - await runSaga(testCreateGeneralChannelSaga).toPromise() - }) - - function* mockNewCommunityEvent(): Generator { - yield* apply(socket.socketClient, socket.socketClient.emit, [ - SocketActionTypes.COMMUNITY_CREATED, - { - id: communityId, - }, - ]) - } - - function* testCreateGeneralChannelSaga(): Generator { - yield* fork(mockNewCommunityEvent) - yield* take(publicChannels.actions.createChannel) - yield* take(publicChannels.actions.setCurrentChannel) - } - - expect(actions).toMatchInlineSnapshot(` - Array [ - "Communities/sendCommunityMetadata", - "PublicChannels/createGeneralChannel", - "Identity/saveUserCsr", - "PublicChannels/createChannel", - "PublicChannels/setCurrentChannel", - "PublicChannels/clearUnreadChannel", - "Messages/lazyLoading", - "Messages/resetCurrentPublicChannelCache", - ] - `) - }) -}) diff --git a/packages/integration-tests/src/integrationTests/appActions.ts b/packages/integration-tests/src/integrationTests/appActions.ts index bcbff3e67e..dd789afdb7 100644 --- a/packages/integration-tests/src/integrationTests/appActions.ts +++ b/packages/integration-tests/src/integrationTests/appActions.ts @@ -252,7 +252,7 @@ export const getCommunityOwnerData = (ownerStore: Store) => { communityId: community.id, ownerPeerId: ownerIdentityState.identities.entities[ownerIdentityState.identities.ids[0]].peerId.id, ownerRootCA: community.rootCa, - registrarPort: community.port, + registrarPort: 0, } } diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts index b019eb86c7..80d718cd7e 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts @@ -31,8 +31,6 @@ describe('deepLinkSaga', () => { rootCa: '', peerList: [], onionAddress: '', - privateKey: '', - port: 0, ownerCertificate: '', } diff --git a/packages/mobile/src/store/store.ts b/packages/mobile/src/store/store.ts index 7d2ede15b5..106867a42f 100644 --- a/packages/mobile/src/store/store.ts +++ b/packages/mobile/src/store/store.ts @@ -1,7 +1,7 @@ import FilesystemStorage from 'redux-persist-filesystem-storage' import RNFetchBlob from 'react-native-blob-util' import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit' -import { persistReducer, persistStore } from 'redux-persist' +import { createMigrate, persistReducer, persistStore } from 'redux-persist' import createSagaMiddleware from 'redux-saga' import { NodeEnv } from '../utils/const/NodeEnv.enum' @@ -16,6 +16,8 @@ import { FilesTransform, ConnectionTransform, UsersTransform, + communities, + storeMigrations, } from '@quiet/state-manager' import { StoreKeys } from './store.keys' @@ -31,6 +33,7 @@ FilesystemStorage.config({ const persistedReducer = persistReducer( { key: 'persistedReducer', + version: 0, storage: FilesystemStorage, whitelist: [ StateManagerStoreKeys.Identity, @@ -51,6 +54,7 @@ const persistedReducer = persistReducer( ConnectionTransform, UsersTransform, ], + migrate: createMigrate(storeMigrations, { debug: true }), }, rootReducer ) diff --git a/packages/mobile/src/tests/deep.linking.test.tsx b/packages/mobile/src/tests/deep.linking.test.tsx index 7bcc553f93..3b0f5650a2 100644 --- a/packages/mobile/src/tests/deep.linking.test.tsx +++ b/packages/mobile/src/tests/deep.linking.test.tsx @@ -16,6 +16,8 @@ describe('Deep linking', () => { beforeEach(async () => { socket = new MockedSocket() + // @ts-ignore + socket.emitWithAck = jest.fn() ioMock.mockImplementation(() => socket) }) @@ -51,11 +53,10 @@ describe('Deep linking', () => { "Init/deepLink", "Init/resetDeepLink", "Communities/createNetwork", - "Communities/setInvitationCodes", - "Navigation/replaceScreen", - "Communities/savePSK", "Communities/addNewCommunity", + "Navigation/replaceScreen", "Communities/setCurrentCommunity", + "Communities/setInvitationCodes", "Init/deepLink", ] `) diff --git a/packages/state-manager/src/index.ts b/packages/state-manager/src/index.ts index a67b2ed234..21809eecb6 100644 --- a/packages/state-manager/src/index.ts +++ b/packages/state-manager/src/index.ts @@ -53,6 +53,7 @@ export { LoadingPanelType } from './sagas/network/network.types' export type { Store } from './sagas/store.types' export type { TestStore, TestStoreState } from './utils/tests/types' export { StoreKeys } from './sagas/store.keys' +export { storeMigrations } from './sagas/store.migrations' export { prepareStore } from './utils/tests/prepareStore' export { useIO } from './sagas/socket/startConnection/startConnection.saga' diff --git a/packages/state-manager/src/sagas/app/app.master.saga.ts b/packages/state-manager/src/sagas/app/app.master.saga.ts index f905fdf992..07dca44027 100644 --- a/packages/state-manager/src/sagas/app/app.master.saga.ts +++ b/packages/state-manager/src/sagas/app/app.master.saga.ts @@ -3,10 +3,12 @@ import { all, takeEvery, takeLeading } from 'typed-redux-saga' import { appActions } from './app.slice' import { closeServicesSaga } from './closeServices.saga' import { stopBackendSaga } from './stopBackend/stopBackend.saga' +import { loadMigrationDataSaga } from './loadMigrationData/loadMigrationData.saga' export function* appMasterSaga(socket: Socket): Generator { yield* all([ takeLeading(appActions.closeServices.type, closeServicesSaga, socket), takeEvery(appActions.stopBackend.type, stopBackendSaga, socket), + takeEvery(appActions.loadMigrationData.type, loadMigrationDataSaga, socket), ]) } diff --git a/packages/state-manager/src/sagas/app/app.slice.ts b/packages/state-manager/src/sagas/app/app.slice.ts index e4ba4f2ef8..4b69d07204 100644 --- a/packages/state-manager/src/sagas/app/app.slice.ts +++ b/packages/state-manager/src/sagas/app/app.slice.ts @@ -1,4 +1,4 @@ -import { createSlice } from '@reduxjs/toolkit' +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' import { StoreKeys } from '../store.keys' // eslint-disable-next-line @typescript-eslint/no-extraneous-class @@ -10,6 +10,7 @@ export const appSlice = createSlice({ reducers: { closeServices: state => state, stopBackend: state => state, + loadMigrationData: (state, action: PayloadAction) => state, }, }) diff --git a/packages/state-manager/src/sagas/app/loadMigrationData/loadMigrationData.saga.ts b/packages/state-manager/src/sagas/app/loadMigrationData/loadMigrationData.saga.ts new file mode 100644 index 0000000000..74893658fa --- /dev/null +++ b/packages/state-manager/src/sagas/app/loadMigrationData/loadMigrationData.saga.ts @@ -0,0 +1,33 @@ +import { type PayloadAction } from '@reduxjs/toolkit' +import { apply, select } from 'typed-redux-saga' + +import { SocketActionTypes } from '@quiet/types' + +import { type appActions } from '../app.slice' +import { type Socket, applyEmitParams } from '../../../types' +import { communitiesSelectors } from '../../communities/communities.selectors' +import { identitySelectors } from '../../identity/identity.selectors' + +export function* loadMigrationDataSaga( + socket: Socket, + action: PayloadAction['payload']> +): Generator { + const keys = action.payload + const data: Record = {} + + for (const key of keys) { + if (key === 'communities') { + data[key] = yield* select(communitiesSelectors.selectEntities) + } + + if (key === 'currentCommunityId') { + data[key] = yield* select(communitiesSelectors.currentCommunityId) + } + + if (key === 'identities') { + data[key] = yield* select(identitySelectors.selectEntities) + } + } + + yield* apply(socket, socket.emit, applyEmitParams(SocketActionTypes.LOAD_MIGRATION_DATA, data)) +} diff --git a/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts b/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts index 3f298c92ea..f646ea2fd8 100644 --- a/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts +++ b/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts @@ -112,9 +112,9 @@ describe('communitiesSelectors', () => { const ownerOrbitDbIdentity = 'testOwnerOrbitDbIdentity' await factory.create['payload']>('Community', { peerList, + psk, ownerOrbitDbIdentity, }) - store.dispatch(communitiesActions.savePSK(psk)) const selectorInvitationUrl = connectionSelectors.invitationUrl(store.getState()) const expectedUrl = invitationShareUrl(peerList, psk, ownerOrbitDbIdentity) expect(expectedUrl).not.toEqual('') diff --git a/packages/state-manager/src/sagas/communities/communities.master.saga.ts b/packages/state-manager/src/sagas/communities/communities.master.saga.ts index 4188795afb..599e9eeee1 100644 --- a/packages/state-manager/src/sagas/communities/communities.master.saga.ts +++ b/packages/state-manager/src/sagas/communities/communities.master.saga.ts @@ -2,20 +2,18 @@ import { type Socket } from '../../types' import { all, takeEvery } from 'typed-redux-saga' import { communitiesActions } from './communities.slice' import { connectionActions } from '../appConnection/connection.slice' -import { updateCommunitySaga } from './updateCommunity/updateCommunity.saga' +import { createCommunitySaga } from './createCommunity/createCommunity.saga' import { initCommunities, launchCommunitySaga } from './launchCommunity/launchCommunity.saga' import { createNetworkSaga } from './createNetwork/createNetwork.saga' -import { saveCommunityMetadataSaga } from './saveCommunityMetadata/saveCommunityMetadata.saga' import { sendCommunityMetadataSaga } from './updateCommunityMetadata/updateCommunityMetadata.saga' import { sendCommunityCaDataSaga } from './sendCommunityCaData/sendCommunityCaData.saga' export function* communitiesMasterSaga(socket: Socket): Generator { yield all([ takeEvery(communitiesActions.createNetwork.type, createNetworkSaga, socket), - takeEvery(communitiesActions.updateCommunity.type, updateCommunitySaga), takeEvery(connectionActions.torBootstrapped.type, initCommunities), + takeEvery(communitiesActions.createCommunity.type, createCommunitySaga, socket), takeEvery(communitiesActions.launchCommunity.type, launchCommunitySaga, socket), - takeEvery(communitiesActions.saveCommunityMetadata.type, saveCommunityMetadataSaga, socket), takeEvery(communitiesActions.sendCommunityMetadata.type, sendCommunityMetadataSaga, socket), takeEvery(communitiesActions.sendCommunityCaData.type, sendCommunityCaDataSaga, socket), ]) diff --git a/packages/state-manager/src/sagas/communities/communities.selectors.test.ts b/packages/state-manager/src/sagas/communities/communities.selectors.test.ts index 1cccbaa515..16ee02a6cb 100644 --- a/packages/state-manager/src/sagas/communities/communities.selectors.test.ts +++ b/packages/state-manager/src/sagas/communities/communities.selectors.test.ts @@ -41,7 +41,7 @@ describe('communitiesSelectors', () => { it('select current community', () => { const community = communitiesSelectors.currentCommunity(store.getState()) - expect(community).toEqual({ ...communityAlpha }) + expect(community).toEqual({ ...communityAlpha, ownerCertificate: identity.userCertificate }) }) it('returns proper ownerNickname - ownerCertificate exist', async () => { diff --git a/packages/state-manager/src/sagas/communities/communities.selectors.ts b/packages/state-manager/src/sagas/communities/communities.selectors.ts index 2c324ce148..1e01a4c16c 100644 --- a/packages/state-manager/src/sagas/communities/communities.selectors.ts +++ b/packages/state-manager/src/sagas/communities/communities.selectors.ts @@ -36,8 +36,8 @@ export const invitationCodes = createSelector(communitiesSlice, reducerState => return reducerState.invitationCodes }) -export const psk = createSelector(communitiesSlice, reducerState => { - return reducerState.psk +export const psk = createSelector(currentCommunity, currentCommunity => { + return currentCommunity?.psk }) export const ownerCertificate = createSelector(currentCommunity, currentCommunity => { diff --git a/packages/state-manager/src/sagas/communities/communities.slice.ts b/packages/state-manager/src/sagas/communities/communities.slice.ts index 188c75ca6b..c8b49e072b 100644 --- a/packages/state-manager/src/sagas/communities/communities.slice.ts +++ b/packages/state-manager/src/sagas/communities/communities.slice.ts @@ -15,7 +15,6 @@ export class CommunitiesState { public invitationCodes: InvitationPair[] = [] public currentCommunity = '' public communities: EntityState = communitiesAdapter.getInitialState() - public psk: string | undefined } export const communitiesSlice = createSlice({ @@ -28,7 +27,6 @@ export const communitiesSlice = createSlice({ addNewCommunity: (state, action: PayloadAction) => { communitiesAdapter.addOne(state.communities, action.payload) }, - updateCommunity: (state, _action: PayloadAction) => state, updateCommunityData: (state, action: PayloadAction) => { communitiesAdapter.updateOne(state.communities, { id: action.payload.id, @@ -40,15 +38,8 @@ export const communitiesSlice = createSlice({ sendCommunityCaData: state => state, sendCommunityMetadata: state => state, createNetwork: (state, _action: PayloadAction) => state, - storePeerList: (state, action: PayloadAction) => { - communitiesAdapter.updateOne(state.communities, { - id: action.payload.communityId, - changes: { - ...action.payload, - }, - }) - }, resetApp: (state, _action) => state, + createCommunity: (state, _action: PayloadAction) => state, launchCommunity: (state, _action: PayloadAction) => state, customProtocol: (state, _action: PayloadAction) => state, setInvitationCodes: (state, action: PayloadAction) => { @@ -57,10 +48,6 @@ export const communitiesSlice = createSlice({ clearInvitationCodes: state => { state.invitationCodes = [] }, - saveCommunityMetadata: (state, _action: PayloadAction) => state, - savePSK: (state, action: PayloadAction) => { - state.psk = action.payload - }, }, }) diff --git a/packages/state-manager/src/sagas/communities/communities.types.ts b/packages/state-manager/src/sagas/communities/communities.types.ts index 8ac9db480d..1789c00362 100644 --- a/packages/state-manager/src/sagas/communities/communities.types.ts +++ b/packages/state-manager/src/sagas/communities/communities.types.ts @@ -18,14 +18,6 @@ export interface Certificates { CA: string[] } -export interface InitCommunityPayload { - id: string - peerId: PeerId - hiddenService: HiddenService - certs: Certificates - peers?: string[] -} - export interface StorePeerListPayload { communityId: string peerList: string[] diff --git a/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts b/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts new file mode 100644 index 0000000000..2c7768a47c --- /dev/null +++ b/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts @@ -0,0 +1,66 @@ +import { type Socket, applyEmitParams } from '../../../types' +import { select, apply, put } from 'typed-redux-saga' +import { type PayloadAction } from '@reduxjs/toolkit' +import { identityActions } from '../../identity/identity.slice' +import { communitiesSelectors } from '../communities.selectors' +import { communitiesActions } from '../communities.slice' +import { identitySelectors } from '../../identity/identity.selectors' +import { publicChannelsActions } from '../../publicChannels/publicChannels.slice' +import { type Community, type InitCommunityPayload, SocketActionTypes } from '@quiet/types' + +export function* createCommunitySaga( + socket: Socket, + action: PayloadAction['payload']> +): Generator { + let communityId: string = action.payload + + if (!communityId) { + communityId = yield* select(communitiesSelectors.currentCommunityId) + } + + const community = yield* select(communitiesSelectors.selectById(communityId)) + const identity = yield* select(identitySelectors.selectById(communityId)) + + if (!identity) return + + const payload: InitCommunityPayload = { + id: communityId, + name: community?.name, + peerId: identity.peerId, + hiddenService: identity.hiddenService, + CA: community?.CA, + rootCa: community?.rootCa, + // Type mismatch between `userCsr | null` in Identity and `ownerCsr?` in + // InitCommunityPayload + ownerCsr: identity.userCsr ?? undefined, + } + + const createdCommunity: Community | undefined = yield* apply( + socket, + socket.emitWithAck, + applyEmitParams(SocketActionTypes.CREATE_COMMUNITY, payload) + ) + + if (!createdCommunity || !createdCommunity.ownerCertificate) return + + yield* put(communitiesActions.updateCommunityData(createdCommunity)) + + yield* put( + identityActions.storeUserCertificate({ + communityId: createdCommunity.id, + userCertificate: createdCommunity.ownerCertificate, + }) + ) + + // TODO: Community metadata should already exist on the backend after creating + // the community. + yield* put(communitiesActions.sendCommunityMetadata()) + yield* put(publicChannelsActions.createGeneralChannel()) + // TODO: We can likely refactor this a bit. Currently, we issue the owner's + // certificate before creating the community, but then we add the owner's CSR + // to the OrbitDB store after creating the community (in the following saga). + // We can likely add the owner's CSR when creating the community or decouple + // community creation from CSR/certificate creation and create the community + // first and then add the owner's CSR and issue their certificate. + yield* put(identityActions.saveUserCsr()) +} diff --git a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts index 69a80947ac..ed84a06819 100644 --- a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts +++ b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts @@ -14,6 +14,13 @@ export function* createNetworkSaga( action: PayloadAction['payload']> ) { console.log('create network saga') + + // Community IDs are only local identifiers + const id = yield* call(generateId) + + const network = yield* apply(socket, socket.emitWithAck, applyEmitParams(SocketActionTypes.CREATE_NETWORK, id)) + + // TODO: Move CA generation to backend when creating Community let CA: null | { rootCertString: string rootKeyString: string @@ -31,7 +38,6 @@ export function* createNetworkSaga( ) } - const id = yield* call(generateId) const community: Community = { id, name: action.payload.name, @@ -41,26 +47,17 @@ export function* createNetworkSaga( ownerOrbitDbIdentity: action.payload.ownerOrbitDbIdentity, } + yield* put(communitiesActions.addNewCommunity(community)) + yield* put(communitiesActions.setCurrentCommunity(id)) + const invitationPeers = action.payload.peers if (invitationPeers) { yield* put(communitiesActions.setInvitationCodes(invitationPeers)) } - const psk = action.payload.psk - if (psk) { - console.log('create network saga: saving PSK') - yield* put(communitiesActions.savePSK(psk)) - } - - yield* put(communitiesActions.addNewCommunity(community)) - yield* put(communitiesActions.setCurrentCommunity(id)) - - const network = yield* apply( - socket, - socket.emitWithAck, - applyEmitParams(SocketActionTypes.CREATE_NETWORK, community.id) - ) const dmKeys = yield* call(generateDmKeyPair) + + // Identities are tied to communities for now const identity: Identity = { id: community.id, nickname: '', diff --git a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.test.ts b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.test.ts index 79c41e8435..8a69c7b4db 100644 --- a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.test.ts +++ b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.test.ts @@ -59,7 +59,7 @@ describe('launchCommunity', () => { }) test('launch certain community instead of current community', async () => { - const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket + const socket = { emit: jest.fn(), emitWithAck: jest.fn(), on: jest.fn() } as unknown as Socket const community = await factory.create['payload']>('Community') @@ -76,14 +76,9 @@ describe('launchCommunity', () => { id: community.id, peerId: identity.peerId, hiddenService: identity.hiddenService, - certs: { - // @ts-expect-error - certificate: identity.userCertificate, - // @ts-expect-error - key: identity.userCsr.userKey, - CA: [communityWithRootCa.rootCa], - }, peers: community.peerList, + psk: undefined, + ownerOrbitDbIdentity: undefined, } await expectSaga(launchCommunitySaga, socket, communitiesActions.launchCommunity(community.id)) @@ -108,22 +103,12 @@ describe('launchCommunity', () => { }, } ) - .apply(socket, socket.emit, [ - SocketActionTypes.LAUNCH_COMMUNITY, - { - id: launchCommunityPayload.id, - peerId: launchCommunityPayload.peerId, - hiddenService: launchCommunityPayload.hiddenService, - peers: launchCommunityPayload.peers, - psk: undefined, - ownerOrbitDbIdentity: undefined, - }, - ]) + .apply(socket, socket.emitWithAck, [SocketActionTypes.LAUNCH_COMMUNITY, launchCommunityPayload]) .run() }) test('launch current community', async () => { - const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket + const socket = { emit: jest.fn(), emitWithAck: jest.fn(), on: jest.fn() } as unknown as Socket const community = await factory.create['payload']>('Community') @@ -140,14 +125,9 @@ describe('launchCommunity', () => { id: community.id, peerId: identity.peerId, hiddenService: identity.hiddenService, - certs: { - // @ts-expect-error - certificate: identity.userCertificate, - // @ts-expect-error - key: identity.userCsr.userKey, - CA: [communityWithRootCa.rootCa], - }, peers: community.peerList, + psk: undefined, + ownerOrbitDbIdentity: undefined, } await expectSaga(launchCommunitySaga, socket, communitiesActions.launchCommunity(community.id)) @@ -172,22 +152,12 @@ describe('launchCommunity', () => { }, } ) - .apply(socket, socket.emit, [ - SocketActionTypes.LAUNCH_COMMUNITY, - { - id: launchCommunityPayload.id, - peerId: launchCommunityPayload.peerId, - hiddenService: launchCommunityPayload.hiddenService, - peers: launchCommunityPayload.peers, - psk: undefined, - ownerOrbitDbIdentity: undefined, - }, - ]) + .apply(socket, socket.emitWithAck, [SocketActionTypes.LAUNCH_COMMUNITY, launchCommunityPayload]) .run() }) test('do not launch current community if it does not have rootCa', async () => { - const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket + const socket = { emit: jest.fn(), emitWithAck: jest.fn(), on: jest.fn() } as unknown as Socket const community = await factory.create['payload']>('Community') @@ -201,14 +171,6 @@ describe('launchCommunity', () => { id: community.id, peerId: identity.peerId, hiddenService: identity.hiddenService, - certs: { - // @ts-expect-error - certificate: identity.userCertificate, - // @ts-expect-error - key: identity.userCsr.userKey, - // @ts-expect-error - CA: [community.rootCa], - }, peers: community.peerList, } @@ -234,18 +196,7 @@ describe('launchCommunity', () => { }, } ) - .not.apply(socket, socket.emit, [ - SocketActionTypes.LAUNCH_COMMUNITY, - { - id: launchCommunityPayload.id, - peerId: launchCommunityPayload.peerId, - hiddenService: launchCommunityPayload.hiddenService, - certs: launchCommunityPayload.certs, - peers: launchCommunityPayload.peers, - psk: undefined, - ownerOrbitDbIdentity: undefined, - }, - ]) + .not.apply(socket, socket.emitWithAck, [SocketActionTypes.LAUNCH_COMMUNITY, launchCommunityPayload]) .run() }) diff --git a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts index 303545f750..278a7d22e2 100644 --- a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts +++ b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts @@ -9,9 +9,8 @@ import { getCurrentTime } from '../../messages/utils/message.utils' import { connectionSelectors } from '../../appConnection/connection.selectors' import { networkSelectors } from '../../network/network.selectors' import { pairsToP2pAddresses } from '@quiet/common' -import { type InitCommunityPayload, SocketActionTypes } from '@quiet/types' +import { type Community, type InitCommunityPayload, SocketActionTypes } from '@quiet/types' -// TODO: Remove if unused export function* initCommunities(): Generator { const joinedCommunities = yield* select(identitySelectors.joinedCommunities) @@ -31,11 +30,17 @@ export function* launchCommunitySaga( action: PayloadAction['payload']> ): Generator { const communityId = action.payload + + if (!communityId) { + console.error('Could not launch community, missing community ID') + return + } + const community = yield* select(communitiesSelectors.selectById(communityId)) const identity = yield* select(identitySelectors.selectById(communityId)) - if (!identity?.userCsr?.userKey) { - console.error('Could not launch community, No identity private key') + if (!community || !identity?.userCsr?.userKey) { + console.error('Could not launch community, missing community or user private key') return } @@ -53,9 +58,9 @@ export function* launchCommunitySaga( peerId: identity.peerId, hiddenService: identity.hiddenService, peers: peerList, - psk: community?.psk, - ownerOrbitDbIdentity: community?.ownerOrbitDbIdentity, + psk: community.psk, + ownerOrbitDbIdentity: community.ownerOrbitDbIdentity, } - yield* apply(socket, socket.emit, applyEmitParams(SocketActionTypes.LAUNCH_COMMUNITY, payload)) + yield* apply(socket, socket.emitWithAck, applyEmitParams(SocketActionTypes.LAUNCH_COMMUNITY, payload)) } diff --git a/packages/state-manager/src/sagas/communities/saveCommunityMetadata/saveCommunityMetadata.saga.ts b/packages/state-manager/src/sagas/communities/saveCommunityMetadata/saveCommunityMetadata.saga.ts deleted file mode 100644 index baadddbbb1..0000000000 --- a/packages/state-manager/src/sagas/communities/saveCommunityMetadata/saveCommunityMetadata.saga.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { type PayloadAction } from '@reduxjs/toolkit' -import { put, select } from 'typed-redux-saga' -import { type Socket } from '../../../types' -import { publicChannelsActions } from '../../publicChannels/publicChannels.slice' -import { communitiesSelectors } from '../communities.selectors' -import { communitiesActions } from '../communities.slice' - -export function* saveCommunityMetadataSaga( - socket: Socket, - action: PayloadAction['payload']> -): Generator { - const communityId = yield* select(communitiesSelectors.currentCommunityId) - console.log('save community metadata', action.payload) - yield* put( - communitiesActions.updateCommunity({ - id: communityId, - rootCa: action.payload.rootCa, - ownerOrbitDbIdentity: action.payload.ownerOrbitDbIdentity, - ownerCertificate: action.payload.ownerCertificate, - }) - ) -} diff --git a/packages/state-manager/src/sagas/communities/updateCommunity/updateCommunity.saga.test.ts b/packages/state-manager/src/sagas/communities/updateCommunity/updateCommunity.saga.test.ts deleted file mode 100644 index 6473949a8f..0000000000 --- a/packages/state-manager/src/sagas/communities/updateCommunity/updateCommunity.saga.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { combineReducers } from '@reduxjs/toolkit' -import { expectSaga } from 'redux-saga-test-plan' -import { setupCrypto } from '@quiet/identity' -import { prepareStore } from '../../../utils/tests/prepareStore' -import { getFactory } from '../../../utils/tests/factories' -import { communitiesActions } from '../communities.slice' -import { updateCommunitySaga } from './updateCommunity.saga' -import { reducers } from '../../reducers' -import { type Community } from '@quiet/types' - -describe('updateCommunitySaga', () => { - test('update community', async () => { - setupCrypto() - - const store = prepareStore().store - - const factory = await getFactory(store) - - const community: Community = - await factory.create['payload']>('Community') - - const rootCa = - 'MIIBYDCCAQagAwIBAgIBATAKBggqhkjOPQQDAjAZMRcwFQYDVQQDEw5xdWlldGNvbW11bml0eTAeFw0xMDEyMjgxMDEwMTBaFw0zMDEyMjgxMDEwMTBaMBkxFzAVBgNVBAMTDnF1aWV0Y29tbXVuaXR5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQhderWYwXEfJ/SY7BLkPlgrqj6I1nwICH7TUCCZ+YD2j2m7WqJ2HfWMxjF//hInzIcSwZLJEDPCJroS13tn37KM/MD0wDwYDVR0TBAgwBgEB/wIBAzALBgNVHQ8EBAMCAIYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAoGCCqGSM49BAMCA0gAMEUCIGZqjtn6WU65HtCjIi6sWnwsre1HdGfBQYV1z8tCSsVpAiEAjQ04iRlFT7UZOxn5Y9j1jLHtp/KvhcW7g5vgbnY0EIQ=' - - const reducer = combineReducers(reducers) - await expectSaga( - updateCommunitySaga, - communitiesActions.updateCommunity({ - id: community.id, - rootCa, - }) - ) - .withReducer(reducer) - .withState(store.getState()) - .put( - communitiesActions.updateCommunityData({ - id: community.id, - rootCa, - name: 'quietcommunity', - }) - ) - .run() - }) -}) diff --git a/packages/state-manager/src/sagas/communities/updateCommunity/updateCommunity.saga.ts b/packages/state-manager/src/sagas/communities/updateCommunity/updateCommunity.saga.ts deleted file mode 100644 index 651f4f1499..0000000000 --- a/packages/state-manager/src/sagas/communities/updateCommunity/updateCommunity.saga.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { call, put } from 'typed-redux-saga' -import { type Certificate } from 'pkijs' - -import { CertFieldsTypes, getCertFieldValue, loadCertificate } from '@quiet/identity' -import { type PayloadAction } from '@reduxjs/toolkit' - -import { communitiesActions } from '../communities.slice' - -export function* updateCommunitySaga( - action: PayloadAction['payload']> -): Generator { - let rootCa: Certificate - let communityName: string | null = null - - if (action.payload.rootCa) { - rootCa = loadCertificate(action.payload.rootCa) - communityName = yield* call(getCertFieldValue, rootCa, CertFieldsTypes.commonName) - - if (!communityName) { - console.error(`Could not retrieve ${CertFieldsTypes.commonName} from rootca`) - } - } - - const payload: { - id: string - name?: string - rootCa?: string - ownerCertificate?: string - ownerOrbitDbIdentity?: string - } = { - id: action.payload.id, - } - - if (communityName) { - payload.name = communityName - } - - if (action.payload.rootCa) { - payload.rootCa = action.payload.rootCa - } - - if (action.payload.ownerOrbitDbIdentity) { - payload.ownerOrbitDbIdentity = action.payload.ownerOrbitDbIdentity - } - - if (action.payload.ownerCertificate) { - payload.ownerCertificate = action.payload.ownerCertificate - } - - yield* put(communitiesActions.updateCommunityData(payload)) -} diff --git a/packages/state-manager/src/sagas/communities/updateCommunityMetadata/updateCommunityMetadata.saga.ts b/packages/state-manager/src/sagas/communities/updateCommunityMetadata/updateCommunityMetadata.saga.ts index 494a8b67ff..b3efe67cde 100644 --- a/packages/state-manager/src/sagas/communities/updateCommunityMetadata/updateCommunityMetadata.saga.ts +++ b/packages/state-manager/src/sagas/communities/updateCommunityMetadata/updateCommunityMetadata.saga.ts @@ -35,13 +35,9 @@ export function* sendCommunityMetadataSaga( rootCa: community.rootCa, } - const meta = yield* apply( + yield* apply( socket, socket.emitWithAck, applyEmitParams(SocketActionTypes.SET_COMMUNITY_METADATA, communityMetadataPayload) ) - - if (meta) { - yield* put(communitiesActions.saveCommunityMetadata(meta)) - } } diff --git a/packages/state-manager/src/sagas/identity/identity.master.saga.ts b/packages/state-manager/src/sagas/identity/identity.master.saga.ts index a2488685bc..471398a152 100644 --- a/packages/state-manager/src/sagas/identity/identity.master.saga.ts +++ b/packages/state-manager/src/sagas/identity/identity.master.saga.ts @@ -5,7 +5,6 @@ import { registerCertificateSaga } from './registerCertificate/registerCertifica import { registerUsernameSaga } from './registerUsername/registerUsername.saga' import { verifyJoinTimestampSaga } from './verifyJoinTimestamp/verifyJoinTimestamp.saga' import { saveUserCsrSaga } from './saveUserCsr/saveUserCsr.saga' -import { savedOwnerCertificateSaga } from './savedOwnerCertificate/savedOwnerCertificate.saga' import { usersActions } from '../users/users.slice' import { updateCertificateSaga } from './updateCertificate/updateCertificate.saga' import { checkLocalCsrSaga } from './checkLocalCsr/checkLocalCsr.saga' @@ -14,7 +13,6 @@ export function* identityMasterSaga(socket: Socket): Generator { yield all([ takeEvery(identityActions.registerUsername.type, registerUsernameSaga, socket), takeEvery(identityActions.registerCertificate.type, registerCertificateSaga, socket), - takeEvery(identityActions.savedOwnerCertificate.type, savedOwnerCertificateSaga, socket), takeEvery(identityActions.verifyJoinTimestamp.type, verifyJoinTimestampSaga), takeEvery(identityActions.checkLocalCsr.type, checkLocalCsrSaga), takeEvery(identityActions.saveUserCsr.type, saveUserCsrSaga, socket), diff --git a/packages/state-manager/src/sagas/identity/identity.slice.ts b/packages/state-manager/src/sagas/identity/identity.slice.ts index 2692933611..ec7eb25502 100644 --- a/packages/state-manager/src/sagas/identity/identity.slice.ts +++ b/packages/state-manager/src/sagas/identity/identity.slice.ts @@ -30,7 +30,6 @@ export const identitySlice = createSlice({ }) }, createUserCsr: (state, _action: PayloadAction) => state, - savedOwnerCertificate: (state, _action: PayloadAction) => state, registerUsername: (state, _action: PayloadAction) => state, registerCertificate: (state, action: PayloadAction) => { identityAdapter.updateOne(state.identities, { diff --git a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts b/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts index ad829c8650..43da4a2ade 100644 --- a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts +++ b/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts @@ -11,7 +11,7 @@ import { registerCertificateSaga } from './registerCertificate.saga' import { type CertData, type RegisterCertificatePayload, SocketActionTypes, type UserCsr } from '@quiet/types' describe('registerCertificateSaga', () => { - it('request certificate registration when user is community owner', async () => { + it('create community when user is community owner', async () => { setupCrypto() const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket const store = prepareStore().store @@ -35,17 +35,7 @@ describe('registerCertificateSaga', () => { await expectSaga(registerCertificateSaga, socket, identityActions.registerCertificate(registerCertificatePayload)) .withReducer(reducer) .withState(store.getState()) - .apply(socket, socket.emit, [ - SocketActionTypes.REGISTER_OWNER_CERTIFICATE, - { - communityId: community.id, - userCsr: identity.userCsr, - permsData: { - certificate: community.CA?.rootCertString, - privKey: community.CA?.rootKeyString, - }, - }, - ]) + .put(communitiesActions.createCommunity(community.id)) .not.apply(socket, socket.emit, [SocketActionTypes.REGISTER_USER_CERTIFICATE]) .run() }) @@ -67,8 +57,6 @@ describe('registerCertificateSaga', () => { rootCa: 'rootCa', peerList: [], onionAddress: '', - privateKey: '', - port: 0, } ) @@ -98,7 +86,7 @@ describe('registerCertificateSaga', () => { await expectSaga(registerCertificateSaga, socket, identityActions.registerCertificate(registerCertificatePayload)) .withReducer(reducer) .withState(store.getState()) - .not.apply(socket, socket.emit, [SocketActionTypes.REGISTER_OWNER_CERTIFICATE]) + .not.put(communitiesActions.createCommunity(community.id)) .put(communitiesActions.launchCommunity(community.id)) .run() }) @@ -120,8 +108,6 @@ describe('registerCertificateSaga', () => { rootCa: 'rootCa', peerList: [], onionAddress: '', - privateKey: '', - port: 0, } ) @@ -152,7 +138,7 @@ describe('registerCertificateSaga', () => { await expectSaga(registerCertificateSaga, socket, identityActions.registerCertificate(registerCertificatePayload)) .withReducer(reducer) .withState(store.getState()) - .not.apply(socket, socket.emit, [SocketActionTypes.REGISTER_OWNER_CERTIFICATE]) + .not.put(communitiesActions.createCommunity(community.id)) .not.put(communitiesActions.launchCommunity(community.id)) .put(identityActions.saveUserCsr()) .run() diff --git a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts b/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts index aef5910547..31a46349ee 100644 --- a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts +++ b/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts @@ -16,22 +16,14 @@ export function* registerCertificateSaga( ): Generator { const currentCommunity = yield* select(communitiesSelectors.currentCommunity) const isUsernameTaken = action.payload.isUsernameTaken + if (!currentCommunity) { console.error('Could not register certificate, no current community') return } if (currentCommunity.CA?.rootCertString) { - const payload: RegisterOwnerCertificatePayload = { - communityId: action.payload.communityId, - userCsr: action.payload.userCsr, - permsData: { - certificate: currentCommunity.CA.rootCertString, - privKey: currentCommunity.CA.rootKeyString, - }, - } - - yield* apply(socket, socket.emit, applyEmitParams(SocketActionTypes.REGISTER_OWNER_CERTIFICATE, payload)) + yield* put(communitiesActions.createCommunity(action.payload.communityId)) } else { if (!isUsernameTaken) { yield* put(communitiesActions.launchCommunity(action.payload.communityId)) diff --git a/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.test.ts b/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.test.ts index ac86b7c5da..112d724cd1 100644 --- a/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.test.ts +++ b/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.test.ts @@ -54,8 +54,6 @@ describe('registerUsernameSaga', () => { } const reducer = combineReducers(reducers) - const psk = '12345' - store.dispatch(communitiesActions.savePSK(psk)) await expectSaga(registerUsernameSaga, socket, identityActions.registerUsername({ nickname: 'nickname' })) .withReducer(reducer) .withState(store.getState()) @@ -219,8 +217,6 @@ describe('registerUsernameSaga', () => { rootCa: 'rootCa', peerList: [], onionAddress: '', - privateKey: '', - port: 0, } ) diff --git a/packages/state-manager/src/sagas/identity/savedOwnerCertificate/savedOwnerCertificate.saga.ts b/packages/state-manager/src/sagas/identity/savedOwnerCertificate/savedOwnerCertificate.saga.ts deleted file mode 100644 index 20ba7793fe..0000000000 --- a/packages/state-manager/src/sagas/identity/savedOwnerCertificate/savedOwnerCertificate.saga.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { type Socket, applyEmitParams } from '../../../types' -import { select, apply } from 'typed-redux-saga' -import { type PayloadAction } from '@reduxjs/toolkit' -import { type identityActions } from '../identity.slice' -import { communitiesSelectors } from '../../communities/communities.selectors' -import { identitySelectors } from '../identity.selectors' -import { type InitCommunityPayload, SocketActionTypes } from '@quiet/types' - -export function* savedOwnerCertificateSaga( - socket: Socket, - action: PayloadAction['payload']> -): Generator { - let communityId: string = action.payload - - if (!communityId) { - communityId = yield* select(communitiesSelectors.currentCommunityId) - } - - const community = yield* select(communitiesSelectors.selectById(communityId)) - const identity = yield* select(identitySelectors.selectById(communityId)) - if (!identity?.userCertificate || !identity?.userCsr || !community?.rootCa) return - - const payload: InitCommunityPayload = { - id: communityId, - peerId: identity.peerId, - hiddenService: identity.hiddenService, - certs: { - certificate: identity.userCertificate, - key: identity.userCsr.userKey, - CA: [community.rootCa], - }, - psk: community?.psk, - ownerOrbitDbIdentity: community?.ownerOrbitDbIdentity, - } - - yield* apply(socket, socket.emit, applyEmitParams(SocketActionTypes.CREATE_COMMUNITY, payload)) -} diff --git a/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts b/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts index a43b8f034a..b900613ae6 100644 --- a/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts +++ b/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts @@ -2,6 +2,7 @@ import { eventChannel } from 'redux-saga' import { type Socket } from '../../../types' import { all, call, fork, put, takeEvery } from 'typed-redux-saga' import logger from '../../../utils/logger' +import { appActions } from '../../app/app.slice' import { appMasterSaga } from '../../app/app.master.saga' import { connectionActions } from '../../appConnection/connection.slice' import { communitiesMasterSaga } from '../../communities/communities.master.saga' @@ -26,6 +27,7 @@ import { type ResponseLaunchCommunityPayload, type ChannelMessageIdsResponse, type ChannelsReplicatedPayload, + type Community, type CommunityId, type DownloadStatus, type ErrorPayload, @@ -63,11 +65,10 @@ export function subscribe(socket: Socket) { | ReturnType | ReturnType | ReturnType - | ReturnType | ReturnType - | ReturnType - | ReturnType + | ReturnType | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType @@ -83,11 +84,10 @@ export function subscribe(socket: Socket) { | ReturnType | ReturnType | ReturnType - | ReturnType | ReturnType - | ReturnType | ReturnType | ReturnType + | ReturnType >(emit => { // UPDATE FOR APP socket.on(SocketActionTypes.TOR_INITIALIZED, () => { @@ -105,6 +105,9 @@ export function subscribe(socket: Socket) { emit(networkActions.removeConnectedPeer(payload.peer)) emit(connectionActions.updateNetworkData(payload)) }) + socket.on(SocketActionTypes.MIGRATION_DATA_REQUIRED, (keys: string[]) => { + emit(appActions.loadMigrationData(keys)) + }) // Files socket.on(SocketActionTypes.MESSAGE_MEDIA_UPDATED, (payload: FileMetadata) => { emit(filesActions.updateMessageMedia(payload)) @@ -136,24 +139,6 @@ export function subscribe(socket: Socket) { // Community - socket.on(SocketActionTypes.COMMUNITY_CREATED, async (payload: ResponseCreateCommunityPayload) => { - log(`${SocketActionTypes.COMMUNITY_CREATED}: ${payload}`) - // We can also set community metadata when we register the - // owner's certificate. I think the only issue is that we - // register the owner's certificate before initializing the - // community and thus the storage service. - emit(communitiesActions.sendCommunityMetadata()) - emit(publicChannelsActions.createGeneralChannel()) - // We also save the owner's CSR after registering their - // certificate. It works, but it might make more sense to get - // all the backend services up and running and then save the - // CSR, register the owner's certificate and set community - // metadata. - emit(identityActions.saveUserCsr()) - }) - socket.on(SocketActionTypes.PEER_LIST, (payload: StorePeerListPayload) => { - emit(communitiesActions.storePeerList(payload)) - }) socket.on(SocketActionTypes.COMMUNITY_LAUNCHED, (payload: ResponseLaunchCommunityPayload) => { console.log('Hunting for heisenbug: Community event received in state-manager') // TODO: We can send this once when creating the community and @@ -163,6 +148,11 @@ export function subscribe(socket: Socket) { emit(networkActions.addInitializedCommunity(payload.id)) emit(communitiesActions.clearInvitationCodes()) }) + + socket.on(SocketActionTypes.COMMUNITY_UPDATED, (payload: Community) => { + emit(communitiesActions.updateCommunityData(payload)) + }) + // Errors socket.on(SocketActionTypes.ERROR, (payload: ErrorPayload) => { // FIXME: It doesn't look like log errors have the red error @@ -182,30 +172,6 @@ export function subscribe(socket: Socket) { socket.on(SocketActionTypes.CERTIFICATES_STORED, (payload: SendCertificatesResponse) => { emit(usersActions.responseSendCertificates(payload)) }) - socket.on(SocketActionTypes.OWNER_CERTIFICATE_ISSUED, (payload: SavedOwnerCertificatePayload) => { - log(`${SocketActionTypes.OWNER_CERTIFICATE_ISSUED}: ${payload.communityId}`) - emit( - communitiesActions.updateCommunity({ - id: payload.communityId, - ownerCertificate: payload.network.certificate, - }) - ) - emit( - identityActions.storeUserCertificate({ - userCertificate: payload.network.certificate, - communityId: payload.communityId, - }) - ) - emit(identityActions.savedOwnerCertificate(payload.communityId)) - }) - socket.on(SocketActionTypes.COMMUNITY_METADATA_STORED, (payload: CommunityMetadata) => { - log(`${SocketActionTypes.COMMUNITY_METADATA_STORED}: ${payload}`) - emit(communitiesActions.saveCommunityMetadata(payload)) - }) - socket.on(SocketActionTypes.LIBP2P_PSK_STORED, (payload: { psk: string }) => { - log(`${SocketActionTypes.LIBP2P_PSK_STORED}`) - emit(communitiesActions.savePSK(payload.psk)) - }) // User Profile diff --git a/packages/state-manager/src/sagas/store.migrations.ts b/packages/state-manager/src/sagas/store.migrations.ts new file mode 100644 index 0000000000..f25b189fae --- /dev/null +++ b/packages/state-manager/src/sagas/store.migrations.ts @@ -0,0 +1,39 @@ +import { StoreKeys } from './store.keys' +import { CommunitiesState } from './communities/communities.slice' + +// TODO: It might be easier to run migrations at a higher level that this (e.g. +// once the store has already been rehydrated), so that we have access to the +// entity API and slice reducers. +// +// TODO: Note that any migration here needs to be idempotent since these will +// also run on fresh Quiet installs. We can probably fix this if we want. See: +// https://github.com/rt2zz/redux-persist/blob/d8b01a085e3679db43503a3858e8d4759d6f22fa/src/createMigrate.ts#L21-L24 +export const storeMigrations = { + // MIGRATION: Move CommunitiesState.psk to Community.psk + 0: (state: any) => { + // Removing psk from the CommunitiesState class causes type errors. Below + // is one solution. Another alternative is making CommunitiesState a union + // type, e.g. CommunitiesStateV1 | CommunitiesStateV2, or simply leaving + // the psk field in CommunitiesState and marking it deprecated in a + // comment. + const prevState = state[StoreKeys.Communities] as CommunitiesState & { psk?: string | undefined } + + if (prevState.psk) { + // At this time we only have a single community + const currentCommunity = prevState.communities.entities[prevState.currentCommunity] + + if (currentCommunity) { + currentCommunity.psk = prevState.psk + } + } + + return state + }, + // TODO: Uncomment this migration after the previous migration has been + // released. + // 1: (state: any) => { + // const prevState = state[StoreKeys.Communities] as CommunitiesState & { psk?: string | undefined } + + // delete prevState.psk + // } +} diff --git a/packages/state-manager/src/sagas/store.types.ts b/packages/state-manager/src/sagas/store.types.ts index 0679a2610d..3c96a4e0bf 100644 --- a/packages/state-manager/src/sagas/store.types.ts +++ b/packages/state-manager/src/sagas/store.types.ts @@ -1,6 +1,7 @@ import { configureStore, combineReducers, getDefaultMiddleware } from '@reduxjs/toolkit' import { reducers } from './reducers' import createSagaMiddleware from 'redux-saga' + const rootReducer = combineReducers(reducers) const sagaMiddleware = createSagaMiddleware() const store = configureStore({ diff --git a/packages/state-manager/src/sagas/users/users.slice.test.ts b/packages/state-manager/src/sagas/users/users.slice.test.ts index 9658716a61..ccfa6bd128 100644 --- a/packages/state-manager/src/sagas/users/users.slice.test.ts +++ b/packages/state-manager/src/sagas/users/users.slice.test.ts @@ -20,8 +20,6 @@ describe('users reducer', () => { rootCa: '', peerList: [], onionAddress: '', - privateKey: '', - port: 0, ownerCertificate: '', } diff --git a/packages/state-manager/src/types.ts b/packages/state-manager/src/types.ts index d356b88e2a..f0d3ba0499 100644 --- a/packages/state-manager/src/types.ts +++ b/packages/state-manager/src/types.ts @@ -32,14 +32,13 @@ interface EventsMap { type EmitEvent void> = (payload: Payload, callback?: Callback) => void export interface EmitEvents { - [SocketActionTypes.LAUNCH_COMMUNITY]: EmitEvent + [SocketActionTypes.LAUNCH_COMMUNITY]: EmitEvent void> [SocketActionTypes.DOWNLOAD_FILE]: EmitEvent [SocketActionTypes.SEND_MESSAGE]: EmitEvent [SocketActionTypes.CANCEL_DOWNLOAD]: EmitEvent [SocketActionTypes.UPLOAD_FILE]: EmitEvent - [SocketActionTypes.REGISTER_OWNER_CERTIFICATE]: EmitEvent [SocketActionTypes.REGISTER_USER_CERTIFICATE]: EmitEvent - [SocketActionTypes.CREATE_COMMUNITY]: EmitEvent + [SocketActionTypes.CREATE_COMMUNITY]: EmitEvent void> [SocketActionTypes.GET_MESSAGES]: EmitEvent void> [SocketActionTypes.CREATE_CHANNEL]: EmitEvent void> [SocketActionTypes.DELETE_CHANNEL]: EmitEvent< @@ -49,11 +48,15 @@ export interface EmitEvents { [SocketActionTypes.DELETE_FILES_FROM_CHANNEL]: EmitEvent [SocketActionTypes.CLOSE]: () => void [SocketActionTypes.LEAVE_COMMUNITY]: () => void - [SocketActionTypes.CREATE_NETWORK]: EmitEvent void> + [SocketActionTypes.CREATE_NETWORK]: EmitEvent void> [SocketActionTypes.ADD_CSR]: EmitEvent - [SocketActionTypes.SET_COMMUNITY_METADATA]: EmitEvent void> + [SocketActionTypes.SET_COMMUNITY_METADATA]: EmitEvent< + CommunityMetadata, + (response: CommunityMetadata | undefined) => void + > [SocketActionTypes.SET_COMMUNITY_CA_DATA]: EmitEvent [SocketActionTypes.SET_USER_PROFILE]: EmitEvent + [SocketActionTypes.LOAD_MIGRATION_DATA]: EmitEvent> } export type Socket = IOSocket diff --git a/packages/state-manager/src/utils/tests/factories.ts b/packages/state-manager/src/utils/tests/factories.ts index 2eeec6cdae..e73b7f84a1 100644 --- a/packages/state-manager/src/utils/tests/factories.ts +++ b/packages/state-manager/src/utils/tests/factories.ts @@ -149,7 +149,7 @@ export const getFactory = async (store: Store) => { if (!community.ownerCertificate) { store.dispatch( - communities.actions.updateCommunity({ + communities.actions.updateCommunityData({ id: community.id, ownerCertificate: action.payload.userCertificate, }) diff --git a/packages/types/src/community.ts b/packages/types/src/community.ts index d952fecfbb..607a00fe1a 100644 --- a/packages/types/src/community.ts +++ b/packages/types/src/community.ts @@ -1,4 +1,4 @@ -import { type HiddenService, type PeerId, type Identity } from './identity' +import { type HiddenService, type PeerId, type Identity, type UserCsr } from './identity' import { InvitationPair } from './network' export interface Community { @@ -11,8 +11,6 @@ export interface Community { rootCa?: string peerList?: string[] onionAddress?: string - privateKey?: string - port?: number ownerCertificate?: string psk?: string ownerOrbitDbIdentity?: string @@ -44,12 +42,18 @@ export interface Certificates { export interface InitCommunityPayload { id: string + name?: string peerId: PeerId hiddenService: HiddenService - certs?: Certificates + CA?: null | { + rootCertString: string + rootKeyString: string + } + rootCa?: string peers?: string[] psk?: string ownerOrbitDbIdentity?: string + ownerCsr?: UserCsr } export interface StorePeerListPayload { diff --git a/packages/types/src/connection.ts b/packages/types/src/connection.ts index 899c6c64cd..afee30991b 100644 --- a/packages/types/src/connection.ts +++ b/packages/types/src/connection.ts @@ -2,6 +2,9 @@ export type CommunityId = string export type ConnectedPeers = string[] +// FIXME: We can rename this to something like PeerConnInfo or +// PeerDisconnectedPayload if it's only used for the PEER_DISCONNECTED +// event. export interface NetworkDataPayload { peer: string connectionDuration: number diff --git a/packages/types/src/socket.ts b/packages/types/src/socket.ts index 5888f41141..45495c829a 100644 --- a/packages/types/src/socket.ts +++ b/packages/types/src/socket.ts @@ -2,17 +2,22 @@ * Backend API event types. Currently, these are divided into two * groups: pure events and actions. Pure events are emitted from the * backend to notify the frontend of something and are generally named - * with the past tense (e.g. COMMUNITY_CREATED), while actions are - * emitted from the frontend in order to invoke the backend to do - * something on it's behalf and are generally named as a command (e.g. - * CREATE_COMMUNITY). + * with the past tense (e.g. COMMUNITY_LAUNCHED) or as a noun (e.g. + * CONNECTION_PROCESS_INFO), while actions are emitted from the + * frontend in order to invoke the backend to do something on it's + * behalf and are generally named as a command (e.g. + * CREATE_COMMUNITY). Events generally don't expect a response, while + * actions tend to have a callback for returning data (using Socket.IO + * acknowledgements feature to reduce the amount of events like + * EVENT_REQUEST/EVENT_RESPONSE). + * + * NOTE: I've been adding docstrings to document the events here. */ export enum SocketActionTypes { // ====== Community ====== - COMMUNITY_CREATED = 'communityCreated', COMMUNITY_LAUNCHED = 'communityLaunched', - COMMUNITY_METADATA_STORED = 'communityMetadataStored', + COMMUNITY_UPDATED = 'communityUpdated', CREATE_COMMUNITY = 'createCommunity', LAUNCH_COMMUNITY = 'launchCommunity', LEAVE_COMMUNITY = 'leaveCommunity', @@ -55,9 +60,7 @@ export enum SocketActionTypes { ADD_CSR = 'addCsr', CERTIFICATES_STORED = 'certificatesStored', CSRS_STORED = 'csrsStored', - OWNER_CERTIFICATE_ISSUED = 'ownerCertificateIssued', REGISTER_USER_CERTIFICATE = 'registerUserCertificate', - REGISTER_OWNER_CERTIFICATE = 'registerOwnerCertificate', // ====== Network ====== @@ -67,14 +70,23 @@ export enum SocketActionTypes { CONNECTION_PROCESS_INFO = 'connectionProcess', CREATE_NETWORK = 'createNetwork', LIBP2P_PSK_STORED = 'libp2pPskStored', - NETWORK_CREATED = 'networkCreated', PEER_CONNECTED = 'peerConnected', PEER_DISCONNECTED = 'peerDisconnected', - PEER_LIST = 'peerList', TOR_INITIALIZED = 'torInitialized', // ====== Misc ====== + /** + * For moving data from the frontend to the backend. Load migration + * data into the backend. + */ + LOAD_MIGRATION_DATA = 'loadMigrationData', + /** + * For moving data from the frontend to the backend. The backend may + * require frontend data for migrations when loading an existing + * community from storage. + */ + MIGRATION_DATA_REQUIRED = 'migrationDataRequired', PUSH_NOTIFICATION = 'pushNotification', ERROR = 'error', }