From abd9101f84149ae4ec1db3038fca31880334cdf3 Mon Sep 17 00:00:00 2001 From: Lucas Leblow Date: Mon, 29 Apr 2024 07:58:05 -0600 Subject: [PATCH] fix: Various fixes related to peers, CSRs and backend startup (#2455) Fixes for the following issues: - Peers can be deleted if CSRs don't sync - Backend starting before the frontend is ready, resulting in missed events - Adding duplicate CSRs --- .../connections-manager.service.ts | 3 +- .../src/nest/local-db/local-db.service.ts | 18 +-- .../nest/registration/registration.service.ts | 10 +- .../backend/src/nest/socket/socket.service.ts | 14 +- .../certificatesRequestsStore.ts | 12 +- .../certificates/certificates.store.spec.ts | 3 - .../certificates/certificates.store.ts | 5 +- .../src/nest/storage/storage.service.spec.ts | 49 +++++- .../src/nest/storage/storage.service.ts | 91 ++++++++--- .../src/renderer/sagas/socket/socket.saga.ts | 8 +- .../src/rtl-tests/channel.main.test.tsx | 1 + .../src/rtl-tests/community.create.test.tsx | 2 +- .../src/rtl-tests/community.join.test.tsx | 3 +- packages/identity/src/extractPubKey.ts | 1 + .../src/integrationTests/appActions.ts | 3 +- .../src/screens/Channel/Channel.screen.tsx | 2 +- .../startConnection/startConnection.saga.ts | 7 +- .../createCommunity/createCommunity.saga.ts | 10 +- .../launchCommunity/launchCommunity.saga.ts | 6 +- .../checkLocalCsr/checkLocalCsr.saga.test.ts | 103 ------------ .../checkLocalCsr/checkLocalCsr.saga.ts | 45 ------ .../sagas/identity/identity.master.saga.ts | 4 - .../src/sagas/identity/identity.slice.ts | 5 +- .../registerCertificate.saga.test.ts | 146 ------------------ .../registerCertificate.saga.ts | 34 ---- .../registerUsername.saga.test.ts | 8 +- .../registerUsername/registerUsername.saga.ts | 26 +++- .../identity/saveUserCsr/saveUserCsr.saga.ts | 2 + .../src/sagas/messages/messages.slice.ts | 15 +- .../messages/sendMessage/sendMessage.saga.ts | 46 +++--- .../publicChannels.selectors.ts | 2 + .../startConnection/startConnection.saga.ts | 3 - .../src/sagas/users/users.selectors.ts | 2 +- packages/state-manager/src/types.ts | 1 + packages/types/src/socket.ts | 6 + 35 files changed, 256 insertions(+), 440 deletions(-) delete mode 100644 packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.test.ts delete mode 100644 packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.ts delete mode 100644 packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts delete mode 100644 packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts 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 7800d75b4c..0489557f08 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -209,7 +209,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI const network = await this.localDbService.getNetworkInfo() if (community && network) { - const sortedPeers = await this.localDbService.getSortedPeers(community.peerList) + const sortedPeers = await this.localDbService.getSortedPeers(community.peerList ?? []) this.logger('launchCommunityFromStorage - sorted peers', sortedPeers) if (sortedPeers.length > 0) { community.peerList = sortedPeers @@ -558,6 +558,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI agent: this.socksProxyAgent, localAddress: this.libp2pService.createLibp2pAddress(onionAddress, peerId.toString()), targetPort: this.ports.libp2pHiddenService, + // Ignore local address peers: peers ? peers.slice(1) : [], psk: Libp2pService.generateLibp2pPSK(community.psk).fullKey, } 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 ba3d796ed1..e3a94b4be9 100644 --- a/packages/backend/src/nest/local-db/local-db.service.ts +++ b/packages/backend/src/nest/local-db/local-db.service.ts @@ -95,21 +95,9 @@ export class LocalDbService { } } - public async getSortedPeers( - peers?: string[] | undefined, - includeLocalPeerAddress: boolean = true - ): Promise { - if (!peers) { - const currentCommunity = await this.getCurrentCommunity() - if (!currentCommunity) { - throw new Error('No peers were provided and no community was found to extract peers from') - } - peers = currentCommunity.peerList - if (!peers) { - throw new Error('No peers provided and no peers found on current stored community') - } - } - + // I think we can move this into StorageService (keep this service + // focused on CRUD). + public async getSortedPeers(peers: string[], includeLocalPeerAddress: boolean = true): Promise { const peersStats = (await this.get(LocalDBKeys.PEERS)) || {} const stats: NetworkStats[] = Object.values(peersStats) const network = await this.getNetworkInfo() diff --git a/packages/backend/src/nest/registration/registration.service.ts b/packages/backend/src/nest/registration/registration.service.ts index 87afbada72..17cd8ff838 100644 --- a/packages/backend/src/nest/registration/registration.service.ts +++ b/packages/backend/src/nest/registration/registration.service.ts @@ -44,14 +44,14 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { } public async tryIssueCertificates() { - this.logger('Trying to issue certificates', this.registrationEventInProgress, this.registrationEvents) + this.logger('Trying to process registration event') // Process only a single registration event at a time so that we // do not register two certificates with the same name. if (!this.registrationEventInProgress) { // Get the next event. const event = this.registrationEvents.shift() if (event) { - this.logger('Issuing certificates', event) + this.logger('Processing registration event', event) // Event processing in progress this.registrationEventInProgress = true @@ -62,6 +62,7 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { certificates: (await this.storageService?.loadAllCertificates()) as string[], }) + this.logger('Finished processing registration event') // Event processing finished this.registrationEventInProgress = false @@ -70,6 +71,8 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { setTimeout(this.tryIssueCertificates.bind(this), 0) } } + } else { + this.logger('Registration event processing already in progress') } } @@ -90,12 +93,13 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { } const pendingCsrs = await extractPendingCsrs(payload) + this.logger(`Issuing certificates`) await Promise.all( pendingCsrs.map(async csr => { await this.registerUserCertificate(csr) }) ) - this.logger('Finished issuing certificates') + this.logger('Total certificates issued:', pendingCsrs.length) } // TODO: This doesn't save the owner's certificate in OrbitDB, so perhaps we diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts index bd5b5ce408..e0df314509 100644 --- a/packages/backend/src/nest/socket/socket.service.ts +++ b/packages/backend/src/nest/socket/socket.service.ts @@ -49,36 +49,40 @@ export class SocketService extends EventEmitter implements OnModuleInit { } async onModuleInit() { - this.logger('init:started') + this.logger('init: Started') this.attachListeners() await this.init() - this.logger('init:finished') + this.logger('init: Finished') } public async init() { const connection = new Promise(resolve => { this.serverIoProvider.io.on(SocketActionTypes.CONNECTION, socket => { - this.logger('init: connection') - resolve() + socket.on(SocketActionTypes.START, async () => { + resolve() + }) }) }) await this.listen() + this.logger('init: Waiting for frontend to connect') await connection + this.logger('init: Frontend connected') } private readonly attachListeners = (): void => { // Attach listeners here this.serverIoProvider.io.on(SocketActionTypes.CONNECTION, socket => { - this.logger('socket connection') + this.logger('Socket connection') // On websocket connection, update presentation service with network data this.emit(SocketActionTypes.CONNECTION) socket.on(SocketActionTypes.CLOSE, async () => { + this.logger('Socket connection closed') this.emit(SocketActionTypes.CLOSE) }) diff --git a/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts b/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts index 6f415738a6..be2d6c54b5 100644 --- a/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts +++ b/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts @@ -29,7 +29,6 @@ export class CertificatesRequestsStore extends EventEmitter { write: ['*'], }, }) - await this.store.load() this.store.events.on('write', async (_address, entry) => { this.logger('Added CSR to database') @@ -41,8 +40,8 @@ export class CertificatesRequestsStore extends EventEmitter { this.loadedCertificateRequests() }) - // TODO: Load CSRs in case the owner closes the app before issuing - // certificates + // @ts-ignore + await this.store.load({ fetchEntryTimeout: 15000 }) this.logger('Initialized') } @@ -77,7 +76,7 @@ export class CertificatesRequestsStore extends EventEmitter { await parsedCsr.verify() await this.validateCsrFormat(csr) } catch (err) { - console.error('Failed to validate user csr:', csr, err?.message) + console.error('Failed to validate user CSR:', csr, err?.message) return false } return true @@ -100,6 +99,7 @@ export class CertificatesRequestsStore extends EventEmitter { .map(e => { return e.payload.value }) + this.logger('Total CSRs:', allEntries.length) const allCsrsUnique = [...new Set(allEntries)] await Promise.all( @@ -119,7 +119,9 @@ export class CertificatesRequestsStore extends EventEmitter { filteredCsrsMap.set(pubKey, csr) }) ) - return [...filteredCsrsMap.values()] + const validCsrs = [...filteredCsrsMap.values()] + this.logger('Valid CSRs:', validCsrs.length) + return validCsrs } public clean() { diff --git a/packages/backend/src/nest/storage/certificates/certificates.store.spec.ts b/packages/backend/src/nest/storage/certificates/certificates.store.spec.ts index 104c1a19bc..bf0a1b1d54 100644 --- a/packages/backend/src/nest/storage/certificates/certificates.store.spec.ts +++ b/packages/backend/src/nest/storage/certificates/certificates.store.spec.ts @@ -135,7 +135,6 @@ describe('CertificatesStore', () => { await certificatesStore.addCertificate(certificate) - // @ts-expect-error - getCertificates is protected const certificates = await certificatesStore.getCertificates() expect(certificates).toContain(certificate) @@ -149,7 +148,6 @@ describe('CertificatesStore', () => { await certificatesStore.addCertificate(certificate) - // @ts-expect-error - getCertificates is protected const certificates = await certificatesStore.getCertificates() expect(certificates).not.toContain(certificate) @@ -161,7 +159,6 @@ describe('CertificatesStore', () => { certificatesStore.updateMetadata(communityMetadata) - // @ts-expect-error - getCertificates is protected jest.spyOn(certificatesStore, 'getCertificates').mockResolvedValue([certificate1, certificate2]) const certificates = await certificatesStore.loadAllCertificates() diff --git a/packages/backend/src/nest/storage/certificates/certificates.store.ts b/packages/backend/src/nest/storage/certificates/certificates.store.ts index e9c833b8d2..6341d51d0e 100644 --- a/packages/backend/src/nest/storage/certificates/certificates.store.ts +++ b/packages/backend/src/nest/storage/certificates/certificates.store.ts @@ -146,7 +146,7 @@ export class CertificatesStore extends EventEmitter { * as specified in the comment section of * https://github.com/TryQuiet/quiet/issues/1899 */ - protected async getCertificates() { + public async getCertificates(): Promise { if (!this.store) { return [] } @@ -189,7 +189,8 @@ export class CertificatesStore extends EventEmitter { const validCerts = validCertificates.filter(i => i != undefined) this.logger(`Valid certificates: ${validCerts.length}`) - return validCerts + // TODO: Why doesn't TS infer this properly? + return validCerts as string[] } public async getCertificateUsername(pubkey: string) { diff --git a/packages/backend/src/nest/storage/storage.service.spec.ts b/packages/backend/src/nest/storage/storage.service.spec.ts index c07906efe5..9893376b79 100644 --- a/packages/backend/src/nest/storage/storage.service.spec.ts +++ b/packages/backend/src/nest/storage/storage.service.spec.ts @@ -514,17 +514,52 @@ describe('StorageService', () => { describe('Users', () => { it('gets all users from db', async () => { - await storageService.init(peerId) - const mockGetCsrs = jest.fn() - // @ts-ignore - Property 'getAllEventLogEntries' is protected - storageService.getAllEventLogEntries = mockGetCsrs - mockGetCsrs.mockReturnValue([ + const certs = [ + // b + 'MIICITCCAcegAwIBAgIGAY8GkBEVMAoGCCqGSM49BAMCMAwxCjAIBgNVBAMTAWEwHhcNMjQwNDIyMTYwNzM1WhcNMzAwMjAxMDcwMDAwWjBJMUcwRQYDVQQDEz56Z2hpZGV4czdxdDI0aXZ1M2pvYmpxZHR6end0eWF1NGxwcG51bng1cGtpZjc2cGtweXA3cWNpZC5vbmlvbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDG8SNnoS1BYoV72jcyQFVlsrwvd2Bb9/9L13Tc4SHJwitTUB3F+y/7pk0tAPrZi2qasU2PO9lTwUxXYcAfpCRSjgdcwgdQwCQYDVR0TBAIwADALBgNVHQ8EBAMCAIAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBEGCisGAQQBg4wbAgEEAxMBYjA9BgkrBgECAQ8DAQEEMBMuUW1lUGJCMjVoMWZYN1dBRk42ckZSNGFWRFdVRlFNU3RSSEdERFM0UlFaUTRZcTBJBgNVHREEQjBAgj56Z2hpZGV4czdxdDI0aXZ1M2pvYmpxZHR6end0eWF1NGxwcG51bng1cGtpZjc2cGtweXA3cWNpZC5vbmlvbjAKBggqhkjOPQQDAgNIADBFAiBkTZo6/D0YgNMPcDpuf7n+rDEQls6cMVxEVw/H8vxbhwIhAM+e6we9YP4JeNgOGgd0iZNEpq8N7dla4XO+YVWrh0YG', + + // c + 'MIICITCCAcegAwIBAgIGAY8Glf+pMAoGCCqGSM49BAMCMAwxCjAIBgNVBAMTAWEwHhcNMjQwNDIyMTYxNDA0WhcNMzAwMjAxMDcwMDAwWjBJMUcwRQYDVQQDEz5uaGxpdWpuNjZlMzQ2ZXZ2dnhlYWY0cW1hN3Bxc2hjZ2J1NnQ3d3Nlb2FubmMyZnk0Y25zY3J5ZC5vbmlvbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABP1WBKQdMz5yMpv5hWj6j+auIsnfiJE8dtuxeeM4N03K1An61F0o47CWg04DydwmoPn5gwefEv8t9Cz9nv/VUGejgdcwgdQwCQYDVR0TBAIwADALBgNVHQ8EBAMCAIAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBEGCisGAQQBg4wbAgEEAxMBYzA9BgkrBgECAQ8DAQEEMBMuUW1WY1hRTXVmRWNZS0R0d3NFSlRIUGJzc3BCeU02U0hUYlJHR2VEdkVFdU1RQTBJBgNVHREEQjBAgj5uaGxpdWpuNjZlMzQ2ZXZ2dnhlYWY0cW1hN3Bxc2hjZ2J1NnQ3d3Nlb2FubmMyZnk0Y25zY3J5ZC5vbmlvbjAKBggqhkjOPQQDAgNIADBFAiEAgMCBxF3oK4ituEWcAK6uawMCludZu4YujIpBIR+v2LICIBhMHXrBy1KWc70t6idB+5XkInsRZz5nw1vwgRJ4mw98', + ] + + const csrs = [ + // c + 'MIIB4TCCAYgCAQAwSTFHMEUGA1UEAxM+emdoaWRleHM3cXQyNGl2dTNqb2JqcWR0enp3dHlhdTRscHBudW54NXBraWY3NnBrcHlwN3FjaWQub25pb24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQxvEjZ6EtQWKFe9o3MkBVZbK8L3dgW/f/S9d03OEhycIrU1Adxfsv+6ZNLQD62YtqmrFNjzvZU8FMV2HAH6QkUoIHcMC4GCSqGSIb3DQEJDjEhMB8wHQYDVR0OBBYEFG1W6vJTK/uPuRK2LPaVZyebVVc+MA8GCSqGSIb3DQEJDDECBAAwEQYKKwYBBAGDjBsCATEDEwFiMD0GCSsGAQIBDwMBATEwEy5RbWVQYkIyNWgxZlg3V0FGTjZyRlI0YVZEV1VGUU1TdFJIR0REUzRSUVpRNFlxMEcGA1UdETFAEz56Z2hpZGV4czdxdDI0aXZ1M2pvYmpxZHR6end0eWF1NGxwcG51bng1cGtpZjc2cGtweXA3cWNpZC5vbmlvbjAKBggqhkjOPQQDAgNHADBEAiAjxneoJZtCzkd75HTT+pcj+objG3S04omjeMMw1N+B/wIgAaJRgifnWEnWFYm614UmPw9un2Uwk1gVhN2tSwJ65sM=', + + // o 'MIIDHjCCAsMCAQAwSTFHMEUGA1UEAxM+NnZ1MmJ4a2k3NzdpdDNjcGF5djZmcTZ2cGw0a2Uza3pqN2d4aWNmeWdtNTVkaGh0cGh5ZmR2eWQub25pb24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATMpfp2hSfWFL26OZlZKZEWG9fyAM1ndlEzO0kLxT0pA/7/fs+a5X/s4TkzqCVVQSzhas/84q0WE99ScAcM1LQJoIICFjAuBgkqhkiG9w0BCQ4xITAfMB0GA1UdDgQWBBR6VRzktP1pzZxsGUaJivNUrtgSrzCCAUcGCSqGSIb3DQEJDDGCATgEggE0KZq9s6HEViRfplVgYkulg6XV411ZRe4U1UjfXTf1pRaygfcenGbT6RRagPtZzjuq5hHdYhqDjRzZhnbn8ZASYTgBM7qcseUq5UpS1pE08DI2jePKqatp3Pzm6a/MGSziESnREx784JlKfwKMjJl33UA8lQm9nhSeAIHyBx3c4Lf8IXdW2n3rnhbVfjpBMAxwh6lt+e5agtGXy+q/xAESUeLPfUgRYWctlLgt8Op+WTpLyBkZsVFoBvJrMt2XdM0RI32YzTRr56GXFa4VyQmY5xXwlQSPgidAP7jPkVygNcoeXvAz2ZCk3IR1Cn3mX8nMko53MlDNaMYldUQA0ug28/S7BlSlaq2CDD4Ol3swTq7C4KGTxKrI36ruYUZx7NEaQDF5V7VvqPCZ0fZoTIJuSYTQ67gwEQYKKwYBBAGDjBsCATEDEwFvMD0GCSsGAQIBDwMBATEwEy5RbVhSWTRyaEF4OE11cThkTUdrcjlxa25KZEU2VUhaRGRHYURSVFFFYndGTjViMEcGA1UdETFAEz42dnUyYnhraTc3N2l0M2NwYXl2NmZxNnZwbDRrZTNremo3Z3hpY2Z5Z201NWRoaHRwaHlmZHZ5ZC5vbmlvbjAKBggqhkjOPQQDAgNJADBGAiEAt+f1u/bchg5AZHv6NTGNoXeejTRWUhX3ioGwW6TGg84CIQCHqKNzDh2JjS/hUHx5PApAmfNnQTSf19X6LnNHQweU1g==', + + // o 'MIIDHTCCAsMCAQAwSTFHMEUGA1UEAxM+eTd5Y3ptdWdsMnRla2FtaTdzYmR6NXBmYWVtdng3YmFod3RocmR2Y2J6dzV2ZXgyY3JzcjI2cWQub25pb24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATMq0l4bCmjdb0grtzpwtDVLM9E1IQpL9vrB4+lD9OBZzlrx2365jV7shVu9utas8w8fxtKoBZSnT5+32ZMFTB4oIICFjAuBgkqhkiG9w0BCQ4xITAfMB0GA1UdDgQWBBSoDQpTZdEvi1/Rr/muVXT1clyKRDCCAUcGCSqGSIb3DQEJDDGCATgEggE0BQvyvkiiXEf/PLKnsR1Ba9AhYsVO8o56bnftUnoVzBlRZgUzLJvOSroPk/EmbVz+okhMrcYNgCWHvxrAqHVVq0JRP6bi98BtCUotx6OPFHp5K5QCL60hod1uAnhKocyJG9tsoM9aS+krn/k+g4RCBjiPZ25cC7QG/UNr6wyIQ8elBho4MKm8iOp7EShSsZOV1f6xrnXYCC/zyUc85GEuycLzVImgAQvPATbdMzY4zSGnNLHxkvSUNxaR9LnEWf+i1jeqcOiXOvmdyU5Be3ZqhGKvvBg/5vyLQiCIfeapjZemnLqFHQBitglDm2xnKL6HzMyfZoAHPV7YcWYR4spU9Ju8Q8aqSeAryx7sx55eSR4GO5UQTo5DrQn6xtkwOZ/ytsOknFthF8jcA9uTAMDKA2TylCUwEQYKKwYBBAGDjBsCATEDEwFvMD0GCSsGAQIBDwMBATEwEy5RbVQxOFV2blVCa3NlTWMzU3FuZlB4cEh3TjhuekxySmVOU0xadGM4ckFGWGh6MEcGA1UdETFAEz55N3ljem11Z2wydGVrYW1pN3NiZHo1cGZhZW12eDdiYWh3dGhyZHZjYnp3NXZleDJjcnNyMjZxZC5vbmlvbjAKBggqhkjOPQQDAgNIADBFAiEAoFrAglxmk7ciD6AHQOB1qEoLu0NARcxgwmIry8oeTHwCICyXp5NJQ9Z8vReIAQNng2H2+/XjHifZEWzhoN0VkcBx', - ]) - const allUsers = storageService.getAllUsers() + ] + + await storageService.init(peerId) + // @ts-ignore + storageService.certificatesRequestsStore = { + getCsrs: jest.fn(() => { + return csrs + }), + } + // @ts-ignore + storageService.certificatesStore = { + getCertificates: jest.fn(() => { + return certs + }), + } + + const allUsers = await storageService.getAllUsers() expect(allUsers).toStrictEqual([ + { + onionAddress: 'zghidexs7qt24ivu3jobjqdtzzwtyau4lppnunx5pkif76pkpyp7qcid.onion', + peerId: 'QmePbB25h1fX7WAFN6rFR4aVDWUFQMStRHGDDS4RQZQ4Yq', + username: 'b', + }, + { + onionAddress: 'nhliujn66e346evvvxeaf4qma7pqshcgbu6t7wseoannc2fy4cnscryd.onion', + peerId: 'QmVcXQMufEcYKDtwsEJTHPbsspByM6SHTbRGGeDvEEuMQA', + username: 'c', + }, { onionAddress: '6vu2bxki777it3cpayv6fq6vpl4ke3kzj7gxicfygm55dhhtphyfdvyd.onion', peerId: 'QmXRY4rhAx8Muq8dMGkr9qknJdE6UHZDdGaDRTQEbwFN5b', diff --git a/packages/backend/src/nest/storage/storage.service.ts b/packages/backend/src/nest/storage/storage.service.ts index 7c5121a81d..14df293e8a 100644 --- a/packages/backend/src/nest/storage/storage.service.ts +++ b/packages/backend/src/nest/storage/storage.service.ts @@ -7,6 +7,7 @@ import { parseCertificationRequest, getCertFieldValue, getReqFieldValue, + keyFromCertificate, } from '@quiet/identity' import type { IPFS } from 'ipfs-core' import EventStore from 'orbit-db-eventstore' @@ -286,11 +287,15 @@ export class StorageService extends EventEmitter { this.certificatesStore.on(StorageEvents.CERTIFICATES_STORED, async payload => { this.emit(StorageEvents.CERTIFICATES_STORED, payload) await this.updatePeersList() + // TODO: Shouldn't we also dial new peers or at least add them + // to the peer store for the auto-dialer to handle? }) this.certificatesRequestsStore.on(StorageEvents.CSRS_STORED, async (payload: { csrs: string[] }) => { this.emit(StorageEvents.CSRS_STORED, payload) await this.updatePeersList() + // TODO: Shouldn't we also dial new peers or at least add them + // to the peer store for the auto-dialer to handle? }) this.communityMetadataStore.on(StorageEvents.COMMUNITY_METADATA_STORED, (meta: CommunityMetadata) => { @@ -312,18 +317,30 @@ export class StorageService extends EventEmitter { } public async updatePeersList() { - const users = this.getAllUsers() - const peers = Array.from(new Set(users.map(peer => createLibp2pAddress(peer.onionAddress, peer.peerId)))) - console.log('updatePeersList, peers count:', peers.length) - const community = await this.localDbService.getCurrentCommunity() - if (!community) return + if (!community) { + throw new Error('Failed to update peers list - community missing') + } + + // Always include existing peers. Otherwise, if CSRs or + // certificates do not replicate, then this could remove peers. + const existingPeers = community.peerList ?? [] + this.logger('Existing peers count:', existingPeers.length) + const users = await this.getAllUsers() + const peers = Array.from( + new Set([...existingPeers, ...users.map(user => createLibp2pAddress(user.onionAddress, user.peerId))]) + ) const sortedPeers = await this.localDbService.getSortedPeers(peers) - if (sortedPeers.length > 0) { - community.peerList = sortedPeers - await this.localDbService.setCommunity(community) + + // This should never happen, but just in case + if (sortedPeers.length === 0) { + throw new Error('Failed to update peers list - no peers') } + + this.logger('Updating community peer list. Peers count:', sortedPeers.length) + community.peerList = sortedPeers + await this.localDbService.setCommunity(community) this.emit(StorageEvents.COMMUNITY_UPDATED, community) } @@ -728,18 +745,56 @@ export class StorageService extends EventEmitter { return result } - public getAllUsers(): UserData[] { - const csrs = this.getAllEventLogEntries(this.certificatesRequestsStore.store) - this.logger('csrs count:', csrs.length) - const allUsers: UserData[] = [] + /** + * Retrieve all users (using certificates and CSRs to determine users) + */ + public async getAllUsers(): Promise { + const csrs = await this.certificatesRequestsStore.getCsrs() + const certs = await this.certificatesStore.getCertificates() + const allUsersByKey: Record = {} + + this.logger(`Retrieving all users. CSRs count: ${csrs.length} Certificates count: ${certs.length}`) + + for (const cert of certs) { + const parsedCert = parseCertificate(cert) + const pubKey = keyFromCertificate(parsedCert) + const onionAddress = getCertFieldValue(parsedCert, CertFieldsTypes.commonName) + const peerId = getCertFieldValue(parsedCert, CertFieldsTypes.peerId) + const username = getCertFieldValue(parsedCert, CertFieldsTypes.nickName) + + // TODO: This validation should go in CertificatesStore + if (!pubKey || !onionAddress || !peerId || !username) { + this.logger.error( + `Received invalid certificate. onionAddress: ${onionAddress} peerId: ${peerId} username: ${username}` + ) + continue + } + + allUsersByKey[pubKey] = { onionAddress, peerId, username } + } + for (const csr of csrs) { - const parsedCert = parseCertificationRequest(csr) - const onionAddress = getReqFieldValue(parsedCert, CertFieldsTypes.commonName) - const peerId = getReqFieldValue(parsedCert, CertFieldsTypes.peerId) - const username = getReqFieldValue(parsedCert, CertFieldsTypes.nickName) - if (!onionAddress || !peerId || !username) continue - allUsers.push({ onionAddress, peerId, username }) + const parsedCsr = parseCertificationRequest(csr) + const pubKey = keyFromCertificate(parsedCsr) + const onionAddress = getReqFieldValue(parsedCsr, CertFieldsTypes.commonName) + const peerId = getReqFieldValue(parsedCsr, CertFieldsTypes.peerId) + const username = getReqFieldValue(parsedCsr, CertFieldsTypes.nickName) + + // TODO: This validation should go in CertificatesRequestsStore + if (!pubKey || !onionAddress || !peerId || !username) { + this.logger.error(`Received invalid CSR. onionAddres: ${onionAddress} peerId: ${peerId} username: ${username}`) + continue + } + + if (!(pubKey in allUsersByKey)) { + allUsersByKey[pubKey] = { onionAddress, peerId, username } + } } + + const allUsers = Object.values(allUsersByKey) + + this.logger(`All users count: ${allUsers.length}`) + return allUsers } diff --git a/packages/desktop/src/renderer/sagas/socket/socket.saga.ts b/packages/desktop/src/renderer/sagas/socket/socket.saga.ts index beaec5d543..2268578ac9 100644 --- a/packages/desktop/src/renderer/sagas/socket/socket.saga.ts +++ b/packages/desktop/src/renderer/sagas/socket/socket.saga.ts @@ -1,5 +1,5 @@ import { io } from 'socket.io-client' -import { all, fork, takeEvery, call, put, cancel, FixedTask, select, take } from 'typed-redux-saga' +import { all, fork, takeEvery, call, put, cancel, FixedTask, select, take, delay, apply } from 'typed-redux-saga' import { PayloadAction } from '@reduxjs/toolkit' import { socket as stateManager, messages, connection, Socket } from '@quiet/state-manager' import { socketActions } from './socket.slice' @@ -7,6 +7,7 @@ import { eventChannel } from 'redux-saga' import { displayMessageNotificationSaga } from '../notifications/notifications.saga' import logger from '../../logger' import { encodeSecret } from '@quiet/common' +import { SocketActionTypes } from '@quiet/types' const log = logger('socket') @@ -27,6 +28,7 @@ export function* startConnectionSaga( if (!socketIOSecret) return + log('Connecting to backend') const token = encodeSecret(socketIOSecret) const socket = yield* call(io, `http://127.0.0.1:${dataPort}`, { withCredentials: true, @@ -43,6 +45,10 @@ export function* startConnectionSaga( function* setConnectedSaga(socket: Socket): Generator { const root = yield* fork(stateManager.useIO, socket) const observers = yield* fork(initObservers) + + console.log('Frontend is ready. Starting backend...') + yield* apply(socket, socket.emit, [SocketActionTypes.START]) + // Handle suspending current connection yield all([ takeEvery(socketActions.suspendConnection, cancelRootSaga, root), diff --git a/packages/desktop/src/rtl-tests/channel.main.test.tsx b/packages/desktop/src/rtl-tests/channel.main.test.tsx index c4148b6ac0..4e59b48dde 100644 --- a/packages/desktop/src/rtl-tests/channel.main.test.tsx +++ b/packages/desktop/src/rtl-tests/channel.main.test.tsx @@ -1029,6 +1029,7 @@ describe('Channel', () => { "Messages/lazyLoading", "Messages/resetCurrentPublicChannelCache", "Messages/resetCurrentPublicChannelCache", + "Identity/saveUserCsr", "Files/updateMessageMedia", "Messages/addMessages", "Messages/addMessageVerificationStatus", diff --git a/packages/desktop/src/rtl-tests/community.create.test.tsx b/packages/desktop/src/rtl-tests/community.create.test.tsx index 9d225ae667..467c422725 100644 --- a/packages/desktop/src/rtl-tests/community.create.test.tsx +++ b/packages/desktop/src/rtl-tests/community.create.test.tsx @@ -159,7 +159,7 @@ describe('User', () => { "Identity/registerUsername", "Network/setLoadingPanelType", "Modals/openModal", - "Identity/registerCertificate", + "Identity/addCsr", "Communities/createCommunity", "Files/checkForMissingFiles", "Network/addInitializedCommunity", diff --git a/packages/desktop/src/rtl-tests/community.join.test.tsx b/packages/desktop/src/rtl-tests/community.join.test.tsx index 0f62b173eb..6e699d288e 100644 --- a/packages/desktop/src/rtl-tests/community.join.test.tsx +++ b/packages/desktop/src/rtl-tests/community.join.test.tsx @@ -175,7 +175,7 @@ describe('User', () => { "Identity/registerUsername", "Network/setLoadingPanelType", "Modals/openModal", - "Identity/registerCertificate", + "Identity/addCsr", "Communities/launchCommunity", "Files/checkForMissingFiles", "Network/addInitializedCommunity", @@ -191,6 +191,7 @@ describe('User', () => { "Messages/lazyLoading", "Messages/resetCurrentPublicChannelCache", "Messages/resetCurrentPublicChannelCache", + "Identity/saveUserCsr", "Messages/addMessagesSendingStatus", "Messages/addMessageVerificationStatus", "Messages/addMessages", diff --git a/packages/identity/src/extractPubKey.ts b/packages/identity/src/extractPubKey.ts index d4bb0230c0..6f66224818 100644 --- a/packages/identity/src/extractPubKey.ts +++ b/packages/identity/src/extractPubKey.ts @@ -8,6 +8,7 @@ import config from './config' import { getAlgorithmParameters, Certificate, CertificationRequest, getCrypto } from 'pkijs' import { NoCryptoEngineError } from '@quiet/types' +// FIXME: This is a duplicate of loadCertificate export const parseCertificate = (pem: string): Certificate => { let certificateBuffer = new ArrayBuffer(0) certificateBuffer = stringToArrayBuffer(fromBase64(pem)) diff --git a/packages/integration-tests/src/integrationTests/appActions.ts b/packages/integration-tests/src/integrationTests/appActions.ts index dd789afdb7..1404683cb1 100644 --- a/packages/integration-tests/src/integrationTests/appActions.ts +++ b/packages/integration-tests/src/integrationTests/appActions.ts @@ -162,7 +162,8 @@ export async function sendCsr(store: Store) { userCsr, } - store.dispatch(identity.actions.registerCertificate(csr)) + store.dispatch(identity.actions.addCsr(csr)) + store.dispatch(identity.actions.saveUserCsr()) } export async function joinCommunity(payload: JoinCommunity) { diff --git a/packages/mobile/src/screens/Channel/Channel.screen.tsx b/packages/mobile/src/screens/Channel/Channel.screen.tsx index c399e8d8d1..010e8e0289 100644 --- a/packages/mobile/src/screens/Channel/Channel.screen.tsx +++ b/packages/mobile/src/screens/Channel/Channel.screen.tsx @@ -110,7 +110,7 @@ export const ChannelScreen: FC = () => { return updatedExistingFiles }) - //User Label + // User Label const duplicatedUsernameHandleBack = useCallback(() => { dispatch( diff --git a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts index 8b19e75084..a0abd3022a 100644 --- a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts +++ b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts @@ -1,11 +1,12 @@ import { io } from 'socket.io-client' -import { select, put, call, cancel, fork, takeEvery, delay, FixedTask } from 'typed-redux-saga' +import { select, put, call, cancel, fork, takeEvery, FixedTask, delay, apply } from 'typed-redux-saga' import { PayloadAction } from '@reduxjs/toolkit' import { socket as stateManager, Socket } from '@quiet/state-manager' import { encodeSecret } from '@quiet/common' import { initSelectors } from '../init.selectors' import { initActions, WebsocketConnectionPayload } from '../init.slice' import { eventChannel } from 'redux-saga' +import { SocketActionTypes } from '@quiet/types' export function* startConnectionSaga( action: PayloadAction['payload']> @@ -38,6 +39,7 @@ export function* startConnectionSaga( return } + console.log('Connecting to backend') const token = encodeSecret(socketIOSecret) const socket = yield* call(io, `http://127.0.0.1:${_dataPort}`, { withCredentials: true, @@ -55,6 +57,9 @@ function* setConnectedSaga(socket: Socket): Generator { console.log('WEBSOCKET', 'Forking state-manager sagas', task) // Handle suspending current connection yield* takeEvery(initActions.suspendWebsocketConnection, cancelRootTaskSaga, task) + console.log('Frontend is ready. Starting backend...') + // @ts-ignore - Why is this broken? + yield* apply(socket, socket.emit, [SocketActionTypes.START]) } function* handleSocketLifecycleActions(socket: Socket, socketIOData: WebsocketConnectionPayload): Generator { diff --git a/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts b/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts index c790bed4de..5b9fc25fff 100644 --- a/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts +++ b/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts @@ -25,7 +25,10 @@ export function* createCommunitySaga( const community = yield* select(communitiesSelectors.selectById(communityId)) const identity = yield* select(identitySelectors.selectById(communityId)) - if (!identity) return + if (!identity) { + console.error('Failed to create community - identity missing') + return + } const payload: InitCommunityPayload = { id: communityId, @@ -45,7 +48,10 @@ export function* createCommunitySaga( applyEmitParams(SocketActionTypes.CREATE_COMMUNITY, payload) ) - if (!createdCommunity || !createdCommunity.ownerCertificate) return + if (!createdCommunity || !createdCommunity.ownerCertificate) { + console.error('Failed to create community - invalid response from backend') + return + } yield* put(communitiesActions.updateCommunityData(createdCommunity)) 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 9260ded78c..df08376b55 100644 --- a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts +++ b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts @@ -1,6 +1,7 @@ import { apply, select, put, call } from 'typed-redux-saga' import { type PayloadAction } from '@reduxjs/toolkit' import { applyEmitParams, type Socket } from '../../../types' +import { identityActions } from '../../identity/identity.slice' import { identitySelectors } from '../../identity/identity.selectors' import { communitiesSelectors } from '../communities.selectors' import { communitiesActions } from '../communities.slice' @@ -29,7 +30,8 @@ export function* launchCommunitySaga( socket: Socket, action: PayloadAction['payload']> ): Generator { - console.log('LAUNCH COMMUNITY SAGA') + console.log('Launching community') + const communityId = action.payload if (!communityId) { @@ -65,4 +67,6 @@ export function* launchCommunitySaga( } yield* apply(socket, socket.emitWithAck, applyEmitParams(SocketActionTypes.LAUNCH_COMMUNITY, payload)) + + yield* put(identityActions.saveUserCsr()) } diff --git a/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.test.ts b/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.test.ts deleted file mode 100644 index 7804f2fc5c..0000000000 --- a/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { createUserCsr, getPubKey, loadPrivateKey, pubKeyFromCsr, setupCrypto } from '@quiet/identity' -import { FactoryGirl } from 'factory-girl' -import { getFactory } from '../../../utils/tests/factories' -import { prepareStore, reducers } from '../../../utils/tests/prepareStore' -import { Store, combineReducers } from 'redux' -import { communitiesActions } from '../../communities/communities.slice' -import { identityActions } from '../identity.slice' -import { checkLocalCsrSaga } from './checkLocalCsr.saga' -import { CreateUserCsrPayload, SendCsrsResponse } from '@quiet/types' -import { expectSaga } from 'redux-saga-test-plan' -import { usersActions } from '../../users/users.slice' - -describe('checkLocalCsr', () => { - let store: Store - let factory: FactoryGirl - - beforeEach(async () => { - setupCrypto() - store = prepareStore().store - factory = await getFactory(store) - }) - - test('saves user csr if absent from the database', async () => { - const community = - await factory.create['payload']>('Community') - - const identity = await factory.create['payload']>('Identity', { - id: community.id, - nickname: 'john', - }) - - const payload: SendCsrsResponse = { - csrs: [], - } - - const reducer = combineReducers(reducers) - await expectSaga(checkLocalCsrSaga, usersActions.storeCsrs(payload)) - .withReducer(reducer) - .withState(store.getState()) - .put(identityActions.saveUserCsr()) - .run() - }) - - test('saves user csr if local and stored one differs', async () => { - const community = - await factory.create['payload']>('Community') - - const identity = await factory.create['payload']>('Identity', { - id: community.id, - nickname: 'john', - }) - - const _pubKey = pubKeyFromCsr(identity.userCsr!.userCsr) - - const privateKey = await loadPrivateKey(identity.userCsr!.userKey, 'ECDSA') - const publicKey = await getPubKey(_pubKey) - - const existingKeyPair: CryptoKeyPair = { privateKey, publicKey } - - const createUserCsrPayload: CreateUserCsrPayload = { - nickname: 'alice', - commonName: identity.hiddenService.onionAddress, - peerId: identity.peerId.id, - signAlg: 'ECDSA', - hashAlg: 'sha-256', - existingKeyPair, - } - - const csr = await createUserCsr(createUserCsrPayload) - - const payload: SendCsrsResponse = { - csrs: [csr.userCsr], - } - - const reducer = combineReducers(reducers) - await expectSaga(checkLocalCsrSaga, usersActions.storeCsrs(payload)) - .withReducer(reducer) - .withState(store.getState()) - .put(identityActions.saveUserCsr()) - .run() - }) - - test('skips if stored csr equals local one', async () => { - const community = - await factory.create['payload']>('Community') - - const identity = await factory.create['payload']>('Identity', { - id: community.id, - nickname: 'john', - }) - - const payload: SendCsrsResponse = { - csrs: [identity.userCsr!.userCsr], - } - - const reducer = combineReducers(reducers) - await expectSaga(checkLocalCsrSaga, usersActions.storeCsrs(payload)) - .withReducer(reducer) - .withState(store.getState()) - .not.put(identityActions.saveUserCsr()) - .run() - }) -}) diff --git a/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.ts b/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.ts deleted file mode 100644 index 2e57894e01..0000000000 --- a/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { select, call, put } from 'typed-redux-saga' -import { PayloadAction } from '@reduxjs/toolkit' -import { identityActions } from '../identity.slice' -import { identitySelectors } from '../identity.selectors' -import { CertFieldsTypes, getReqFieldValue, loadCSR, pubKeyFromCsr } from '@quiet/identity' - -export function* checkLocalCsrSaga( - action: PayloadAction['payload']> -): Generator { - console.log('Checking local CSR', action.payload.csrs) - - const { csrs } = action.payload - - const identity = yield* select(identitySelectors.currentIdentity) - - if (!identity) { - console.error('Could not check local csr, no identity.') - return - } - - if (!identity.userCsr) { - console.warn("Identity doesn't have userCsr.") - return - } - - const pubKey = yield* call(pubKeyFromCsr, identity.userCsr.userCsr) - - const storedCsr = csrs.find(csr => csr === identity.userCsr?.userCsr) - - if (storedCsr) { - console.log('Stored CSR with the same public key found, checking for username integirty.', pubKey) - - const parsedCsr = yield* call(loadCSR, storedCsr) - const nickname = yield* call(getReqFieldValue, parsedCsr, CertFieldsTypes.nickName) - - if (nickname == identity.nickname) { - console.log('Stored CSR is equal to the local one, skipping.') - return - } - } - - console.log('Stored CSR differs or missing, saving local one.') - - yield* put(identityActions.saveUserCsr()) -} 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 471398a152..cf3c116db7 100644 --- a/packages/state-manager/src/sagas/identity/identity.master.saga.ts +++ b/packages/state-manager/src/sagas/identity/identity.master.saga.ts @@ -1,20 +1,16 @@ import { type Socket } from '../../types' import { all, takeEvery } from 'typed-redux-saga' import { identityActions } from './identity.slice' -import { registerCertificateSaga } from './registerCertificate/registerCertificate.saga' import { registerUsernameSaga } from './registerUsername/registerUsername.saga' import { verifyJoinTimestampSaga } from './verifyJoinTimestamp/verifyJoinTimestamp.saga' import { saveUserCsrSaga } from './saveUserCsr/saveUserCsr.saga' import { usersActions } from '../users/users.slice' import { updateCertificateSaga } from './updateCertificate/updateCertificate.saga' -import { checkLocalCsrSaga } from './checkLocalCsr/checkLocalCsr.saga' export function* identityMasterSaga(socket: Socket): Generator { yield all([ takeEvery(identityActions.registerUsername.type, registerUsernameSaga, socket), - takeEvery(identityActions.registerCertificate.type, registerCertificateSaga, socket), takeEvery(identityActions.verifyJoinTimestamp.type, verifyJoinTimestampSaga), - takeEvery(identityActions.checkLocalCsr.type, checkLocalCsrSaga), takeEvery(identityActions.saveUserCsr.type, saveUserCsrSaga, socket), takeEvery(usersActions.responseSendCertificates.type, updateCertificateSaga), ]) diff --git a/packages/state-manager/src/sagas/identity/identity.slice.ts b/packages/state-manager/src/sagas/identity/identity.slice.ts index ec7eb25502..6915b1edb4 100644 --- a/packages/state-manager/src/sagas/identity/identity.slice.ts +++ b/packages/state-manager/src/sagas/identity/identity.slice.ts @@ -29,9 +29,8 @@ export const identitySlice = createSlice({ changes: action.payload, }) }, - createUserCsr: (state, _action: PayloadAction) => state, registerUsername: (state, _action: PayloadAction) => state, - registerCertificate: (state, action: PayloadAction) => { + addCsr: (state, action: PayloadAction) => { identityAdapter.updateOne(state.identities, { id: action.payload.communityId, changes: { @@ -49,7 +48,6 @@ export const identitySlice = createSlice({ }, }) }, - checkLocalCsr: (state, _action: PayloadAction) => state, saveUserCsr: state => state, verifyJoinTimestamp: state => state, updateJoinTimestamp: (state, action: PayloadAction) => { @@ -60,7 +58,6 @@ export const identitySlice = createSlice({ }, }) }, - throwIdentityError: (state, _action: PayloadAction) => state, }, }) 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 deleted file mode 100644 index 43da4a2ade..0000000000 --- a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { expectSaga } from 'redux-saga-test-plan' -import { type Socket } from '../../../types' -import { setupCrypto } from '@quiet/identity' -import { prepareStore } from '../../../utils/tests/prepareStore' -import { getFactory } from '../../../utils/tests/factories' -import { combineReducers } from '@reduxjs/toolkit' -import { reducers } from '../../reducers' -import { communitiesActions } from '../../communities/communities.slice' -import { identityActions } from '../identity.slice' -import { registerCertificateSaga } from './registerCertificate.saga' -import { type CertData, type RegisterCertificatePayload, SocketActionTypes, type UserCsr } from '@quiet/types' - -describe('registerCertificateSaga', () => { - 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 - - const factory = await getFactory(store) - - const community = - await factory.create['payload']>('Community') - - const identity = await factory.create['payload']>('Identity', { - id: community.id, - }) - expect(identity.userCsr).not.toBeNull() - const registerCertificatePayload: RegisterCertificatePayload = { - communityId: community.id, - nickname: identity.nickname, - // @ts-expect-error - userCsr: identity.userCsr, - } - const reducer = combineReducers(reducers) - await expectSaga(registerCertificateSaga, socket, identityActions.registerCertificate(registerCertificatePayload)) - .withReducer(reducer) - .withState(store.getState()) - .put(communitiesActions.createCommunity(community.id)) - .not.apply(socket, socket.emit, [SocketActionTypes.REGISTER_USER_CERTIFICATE]) - .run() - }) - - it('launch community when user is not community owner', async () => { - setupCrypto() - const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket - - const store = prepareStore().store - - const factory = await getFactory(store) - - const community = await factory.create['payload']>( - 'Community', - { - id: '1', - name: 'rockets', - CA: null, - rootCa: 'rootCa', - peerList: [], - onionAddress: '', - } - ) - - const userCsr: UserCsr = { - userCsr: 'userCsr', - userKey: 'userKey', - pkcs10: jest.fn() as unknown as CertData, - } - - const identity = ( - await factory.build('Identity', { - id: community.id, - }) - ).payload - - identity.userCsr = userCsr - - store.dispatch(identityActions.addNewIdentity(identity)) - - const registerCertificatePayload: RegisterCertificatePayload = { - communityId: community.id, - nickname: identity.nickname, - userCsr: identity.userCsr, - } - - const reducer = combineReducers(reducers) - await expectSaga(registerCertificateSaga, socket, identityActions.registerCertificate(registerCertificatePayload)) - .withReducer(reducer) - .withState(store.getState()) - .not.put(communitiesActions.createCommunity(community.id)) - .put(communitiesActions.launchCommunity(community.id)) - .run() - }) - - it('launch community when user is not community owner and he used username which was taken', async () => { - setupCrypto() - const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket - - const store = prepareStore().store - - const factory = await getFactory(store) - - const community = await factory.create['payload']>( - 'Community', - { - id: '1', - name: 'rockets', - CA: null, - rootCa: 'rootCa', - peerList: [], - onionAddress: '', - } - ) - - const userCsr: UserCsr = { - userCsr: 'userCsr', - userKey: 'userKey', - pkcs10: jest.fn() as unknown as CertData, - } - - const identity = ( - await factory.build('Identity', { - id: community.id, - }) - ).payload - - identity.userCsr = userCsr - - store.dispatch(identityActions.addNewIdentity(identity)) - - const registerCertificatePayload: RegisterCertificatePayload = { - communityId: community.id, - nickname: identity.nickname, - userCsr: identity.userCsr, - isUsernameTaken: true, - } - - const reducer = combineReducers(reducers) - await expectSaga(registerCertificateSaga, socket, identityActions.registerCertificate(registerCertificatePayload)) - .withReducer(reducer) - .withState(store.getState()) - .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 deleted file mode 100644 index 31a46349ee..0000000000 --- a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { applyEmitParams, type Socket } from '../../../types' -import { type PayloadAction } from '@reduxjs/toolkit' -import { apply, select, put } from 'typed-redux-saga' -import { communitiesSelectors } from '../../communities/communities.selectors' -import { identityActions } from '../identity.slice' -import { - type RegisterOwnerCertificatePayload, - type RegisterUserCertificatePayload, - SocketActionTypes, -} from '@quiet/types' -import { communitiesActions } from '../../communities/communities.slice' - -export function* registerCertificateSaga( - socket: Socket, - action: PayloadAction['payload']> -): 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) { - yield* put(communitiesActions.createCommunity(action.payload.communityId)) - } else { - if (!isUsernameTaken) { - yield* put(communitiesActions.launchCommunity(action.payload.communityId)) - } else { - yield* put(identityActions.saveUserCsr()) - } - } -} 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 5acf2b3e7b..327b7cb698 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 @@ -59,7 +59,7 @@ describe('registerUsernameSaga', () => { .provide([[call.fn(createUserCsr), userCsr]]) .call(createUserCsr, createUserCsrPayload) .put( - identityActions.registerCertificate({ + identityActions.addCsr({ communityId: community.id, nickname: 'nickname', userCsr, @@ -131,7 +131,7 @@ describe('registerUsernameSaga', () => { .call(getPubKey, pubKey) .call(createUserCsr, createUserCsrPayload) .put( - identityActions.registerCertificate({ + identityActions.addCsr({ communityId: community.id, nickname: newNickname, userCsr, @@ -187,7 +187,7 @@ describe('registerUsernameSaga', () => { ]) .dispatch(identityActions.addNewIdentity(identity)) .put( - identityActions.registerCertificate({ + identityActions.addCsr({ communityId: community.id, nickname: identity.nickname, userCsr, @@ -272,7 +272,7 @@ describe('registerUsernameSaga', () => { .dispatch(identityActions.addNewIdentity(identity)) .call(createUserCsr, createUserCsrPayload) .put( - identityActions.registerCertificate({ + identityActions.addCsr({ communityId: community.id, nickname: 'nickname', userCsr, diff --git a/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts b/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts index ae91990777..e6714f430c 100644 --- a/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts +++ b/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts @@ -13,35 +13,35 @@ export function* registerUsernameSaga( socket: Socket, action: PayloadAction['payload']> ): Generator { + console.log('Registering username') + // Nickname can differ between saga calls const { nickname, isUsernameTaken = false } = action.payload let community = yield* select(communitiesSelectors.currentCommunity) - if (!community) { + console.warn('Community missing, waiting...') yield* take(communitiesActions.addNewCommunity) } - community = yield* select(communitiesSelectors.currentCommunity) - if (!community) { console.error('Could not register username, no community data') return } + console.log('Found community') let identity = yield* select(identitySelectors.currentIdentity) - if (!identity) { + console.warn('Identity missing, waiting...') yield* take(identityActions.addNewIdentity) } - identity = yield* select(identitySelectors.currentIdentity) - if (!identity) { console.error('Could not register username, no identity') return } + console.log('Found identity') let userCsr = identity.userCsr @@ -87,12 +87,24 @@ export function* registerUsernameSaga( } } + // TODO: Can rename this type const payload: RegisterCertificatePayload = { communityId: community.id, nickname, userCsr, + // TODO: Remove isUsernameTaken, } - yield* put(identityActions.registerCertificate(payload)) + yield* put(identityActions.addCsr(payload)) + + if (community.CA?.rootCertString) { + yield* put(communitiesActions.createCommunity(community.id)) + } else { + if (!isUsernameTaken) { + yield* put(communitiesActions.launchCommunity(community.id)) + } else { + yield* put(identityActions.saveUserCsr()) + } + } } diff --git a/packages/state-manager/src/sagas/identity/saveUserCsr/saveUserCsr.saga.ts b/packages/state-manager/src/sagas/identity/saveUserCsr/saveUserCsr.saga.ts index 8a595c0fa1..05c371bef0 100644 --- a/packages/state-manager/src/sagas/identity/saveUserCsr/saveUserCsr.saga.ts +++ b/packages/state-manager/src/sagas/identity/saveUserCsr/saveUserCsr.saga.ts @@ -4,6 +4,8 @@ import { apply, select } from 'typed-redux-saga' import { identitySelectors } from '../identity.selectors' export function* saveUserCsrSaga(socket: Socket): Generator { + console.log('Saving user CSR') + const identity = yield* select(identitySelectors.currentIdentity) if (!identity?.userCsr) { console.error('Cannot save user csr to backend, no userCsr') diff --git a/packages/state-manager/src/sagas/messages/messages.slice.ts b/packages/state-manager/src/sagas/messages/messages.slice.ts index c279efc8b2..ab7bfc098a 100644 --- a/packages/state-manager/src/sagas/messages/messages.slice.ts +++ b/packages/state-manager/src/sagas/messages/messages.slice.ts @@ -78,8 +78,14 @@ export const messagesSlice = createSlice({ addMessages: (state, action: PayloadAction) => { const { messages } = action.payload for (const message of messages) { - if (!instanceOfChannelMessage(message)) return - if (!state.publicChannelsMessagesBase.entities[message.channelId]) return + if (!instanceOfChannelMessage(message)) { + console.error('Failed to add message, object not instance of message') + return + } + if (!state.publicChannelsMessagesBase.entities[message.channelId]) { + console.error('Failed to add message, could not find channel', message.channelId) + return + } let toAdd = message @@ -96,8 +102,11 @@ export const messagesSlice = createSlice({ } const messagesBase = state.publicChannelsMessagesBase.entities[message.channelId] - if (!messagesBase) return + if (!messagesBase) { + throw new Error('Failed to add message, channel went missing') + } + console.log('Upserting message to Redux store') channelMessagesAdapter.upsertOne(messagesBase.messages, toAdd) } }, diff --git a/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts b/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts index 1297c1ea5b..fee94b4d50 100644 --- a/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts +++ b/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts @@ -1,10 +1,11 @@ import { type Socket, applyEmitParams } from '../../../types' import { type PayloadAction } from '@reduxjs/toolkit' import { sign, loadPrivateKey, pubKeyFromCsr } from '@quiet/identity' -import { call, select, apply, put, delay } from 'typed-redux-saga' +import { call, select, apply, put, delay, take } from 'typed-redux-saga' import { arrayBufferToString } from 'pvutils' import { config } from '../../users/const/certFieldTypes' import { identitySelectors } from '../../identity/identity.selectors' +import { publicChannelsActions } from '../../publicChannels/publicChannels.slice' import { publicChannelsSelectors } from '../../publicChannels/publicChannels.selectors' import { messagesActions } from '../messages.slice' import { generateMessageId, getCurrentTime } from '../utils/message.utils' @@ -14,28 +15,30 @@ export function* sendMessageSaga( socket: Socket, action: PayloadAction['payload']> ): Generator { - const identity = yield* select(identitySelectors.currentIdentity) - if (!identity?.userCsr) return - - const pubKey = yield* call(pubKeyFromCsr, identity.userCsr.userCsr) - const keyObject = yield* call(loadPrivateKey, identity.userCsr.userKey, config.signAlg) - const signatureArrayBuffer = yield* call(sign, action.payload.message, keyObject) - const signature = yield* call(arrayBufferToString, signatureArrayBuffer) - - const currentChannelId = yield* select(publicChannelsSelectors.currentChannelId) - - const createdAt = yield* call(getCurrentTime) - const generatedMessageId = yield* call(generateMessageId) - const id = action.payload.id || generatedMessageId + const identity = yield* select(identitySelectors.currentIdentity) + if (!identity?.userCsr) { + console.error(`Failed to send message ${id} - user CSR is missing`) + return + } + + const currentChannelId = yield* select(publicChannelsSelectors.currentChannelId) const channelId = action.payload.channelId || currentChannelId if (!channelId) { - console.error(`Could not send message with id ${id}, no channel id`) + console.error(`Failed to send message ${id} - channel ID is missing`) return } + console.log(`Sending message ${id} to channel ${channelId}`) + + const pubKey = yield* call(pubKeyFromCsr, identity.userCsr.userCsr) + const keyObject = yield* call(loadPrivateKey, identity.userCsr.userKey, config.signAlg) + const signatureArrayBuffer = yield* call(sign, action.payload.message, keyObject) + const signature = yield* call(arrayBufferToString, signatureArrayBuffer) + const createdAt = yield* call(getCurrentTime) + const message: ChannelMessage = { id, type: action.payload.type || MessageType.Basic, @@ -48,6 +51,7 @@ export function* sendMessageSaga( } // Grey out message until saved in db + console.log('Adding pending message status') yield* put( messagesActions.addMessagesSendingStatus({ message: message, @@ -65,6 +69,7 @@ export function* sendMessageSaga( ) // Display sent message immediately, to improve user experience + console.log('Adding message to Redux store') yield* put( messagesActions.addMessages({ messages: [message], @@ -73,7 +78,10 @@ export function* sendMessageSaga( ) const isUploadingFileMessage = action.payload.media?.cid?.includes('uploading') - if (isUploadingFileMessage) return // Do not broadcast message until file is uploaded + if (isUploadingFileMessage) { + console.log(`Failed to send message ${id} - file upload is in progress`) + return // Do not broadcast message until file is uploaded + } // Wait until we have subscribed to the channel // @@ -82,11 +90,13 @@ export function* sendMessageSaga( // (in a durable way). while (true) { const subscribedChannels = yield* select(publicChannelsSelectors.subscribedChannels) + console.log('Subscribed channels', subscribedChannels) if (subscribedChannels.includes(channelId)) { + console.log(`Channel ${channelId} subscribed`) break } - console.error('Failed to send message, channel not subscribed. Retrying...') - yield* delay(500) + console.error(`Failed to send message ${id} - channel not subscribed. Waiting...`) + yield* take(publicChannelsActions.setChannelSubscribed) } yield* apply( diff --git a/packages/state-manager/src/sagas/publicChannels/publicChannels.selectors.ts b/packages/state-manager/src/sagas/publicChannels/publicChannels.selectors.ts index 1481087c42..fb5de0ab36 100644 --- a/packages/state-manager/src/sagas/publicChannels/publicChannels.selectors.ts +++ b/packages/state-manager/src/sagas/publicChannels/publicChannels.selectors.ts @@ -178,6 +178,8 @@ export const displayableCurrentChannelMessages = createSelector( if (user) { // @ts-ignore result.push(displayableMessage(message, user, userProfiles[message.pubKey])) + } else { + console.warn('Received a message from a user that does not exist', message.id, message.pubKey, users) } return result }, []) 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 9c908f13ee..a64a65224c 100644 --- a/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts +++ b/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts @@ -64,8 +64,6 @@ export function subscribe(socket: Socket) { | ReturnType | ReturnType | ReturnType - | ReturnType - | ReturnType | ReturnType | ReturnType | ReturnType @@ -160,7 +158,6 @@ export function subscribe(socket: Socket) { // Certificates socket.on(SocketActionTypes.CSRS_STORED, (payload: SendCsrsResponse) => { log(`${SocketActionTypes.CSRS_STORED}`) - emit(identityActions.checkLocalCsr(payload)) emit(usersActions.storeCsrs(payload)) }) socket.on(SocketActionTypes.CERTIFICATES_STORED, (payload: SendCertificatesResponse) => { diff --git a/packages/state-manager/src/sagas/users/users.selectors.ts b/packages/state-manager/src/sagas/users/users.selectors.ts index 2f21e34b08..4902482597 100644 --- a/packages/state-manager/src/sagas/users/users.selectors.ts +++ b/packages/state-manager/src/sagas/users/users.selectors.ts @@ -77,9 +77,9 @@ export const registeredUsernames = createSelector( mapping => new Set(Object.values(mapping).map(u => u.username)) ) +// TODO: We can move most of this to the backend. export const allUsers = createSelector(csrsMapping, certificatesMapping, (csrs, certs) => { const users: Record = {} - const allUsernames: string[] = Object.values(csrs).map(u => u.username) const duplicatedUsernames: string[] = allUsernames.filter((val, index) => allUsernames.indexOf(val) !== index) diff --git a/packages/state-manager/src/types.ts b/packages/state-manager/src/types.ts index 44260530d5..909ae28c6c 100644 --- a/packages/state-manager/src/types.ts +++ b/packages/state-manager/src/types.ts @@ -52,6 +52,7 @@ export interface EmitEvents { [SocketActionTypes.ADD_CSR]: EmitEvent [SocketActionTypes.SET_USER_PROFILE]: EmitEvent [SocketActionTypes.LOAD_MIGRATION_DATA]: EmitEvent> + [SocketActionTypes.START]: () => void } export type Socket = IOSocket diff --git a/packages/types/src/socket.ts b/packages/types/src/socket.ts index 573fb1a313..c74ba5dd14 100644 --- a/packages/types/src/socket.ts +++ b/packages/types/src/socket.ts @@ -87,4 +87,10 @@ export enum SocketActionTypes { MIGRATION_DATA_REQUIRED = 'migrationDataRequired', PUSH_NOTIFICATION = 'pushNotification', ERROR = 'error', + /** + * Start the backend. Currently, the frontend depends on events + * emitted from the backend, so we wait to start the backend until + * the frontend is connected and listening. + */ + START = 'start', }