diff --git a/CHANGELOG.md b/CHANGELOG.md index dec53b4fbd..e264e503de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ [unreleased] +* Changed registration process - user connects to the libp2p network directly instead of using registrar. Invitation link format changed. User csr is now saved to database. + * Fixed android stucking on username registration screen introduced in previous alpha. * Added creator username to initial channel message. diff --git a/packages/backend/src/nest/common/utils.ts b/packages/backend/src/nest/common/utils.ts index 72fa5f8901..0e271922b1 100644 --- a/packages/backend/src/nest/common/utils.ts +++ b/packages/backend/src/nest/common/utils.ts @@ -12,7 +12,7 @@ import { TestConfig } from '../const' import logger from './logger' import { createCertificatesTestHelper } from './client-server' import { Libp2pNodeParams } from '../libp2p/libp2p.types' -import { createLibp2pAddress, createLibp2pListenAddress } from '../libp2p/libp2p.utils' +import { createLibp2pAddress, createLibp2pListenAddress } from '@quiet/common' const log = logger('test') export interface Ports { @@ -201,9 +201,6 @@ export const libp2pInstanceParams = async (): Promise => { listenAddresses: [createLibp2pListenAddress('localhost')], agent: createHttpsProxyAgent({ port: 1234, host: 'localhost' }), localAddress: createLibp2pAddress('localhost', peerId.toString()), - cert: pems.userCert, - key: pems.userKey, - ca: [pems.ca], targetPort: port, peers: [remoteAddress], } 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 5f71412024..abc3ad923e 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 @@ -1,34 +1,23 @@ +import { jest } from '@jest/globals' +import { LazyModuleLoader } from '@nestjs/core' import { Test, TestingModule } from '@nestjs/testing' +import { getFactory, prepareStore, type Store, type communities, type identity } from '@quiet/state-manager' +import { type Community, type Identity, type InitCommunityPayload, type LaunchRegistrarPayload } from '@quiet/types' +import { type FactoryGirl } from 'factory-girl' +import PeerId from 'peer-id' import { TestModule } from '../common/test.module' +import { libp2pInstanceParams, removeFilesFromDir } from '../common/utils' import { QUIET_DIR, TOR_PASSWORD_PROVIDER } from '../const' -import { ConnectionsManagerModule } from './connections-manager.module' -import { ConnectionsManagerService } from './connections-manager.service' -import PeerId from 'peer-id' -import { libp2pInstanceParams } from '../common/utils' -import { jest } from '@jest/globals' -import { CustomEvent } from '@libp2p/interfaces/events' -import { type communities, getFactory, prepareStore, type identity, type Store } from '@quiet/state-manager' -import { type FactoryGirl } from 'factory-girl' -import { DateTime } from 'luxon' -import waitForExpect from 'wait-for-expect' -import { - type Community, - type Identity, - type InitCommunityPayload, - type LaunchRegistrarPayload, - type NetworkStats, -} from '@quiet/types' -import { LocalDBKeys } from '../local-db/local-db.types' +import { Libp2pModule } from '../libp2p/libp2p.module' +import { Libp2pService } from '../libp2p/libp2p.service' import { LocalDbModule } from '../local-db/local-db.module' import { LocalDbService } from '../local-db/local-db.service' -import { RegistrationService } from '../registration/registration.service' +import { LocalDBKeys } from '../local-db/local-db.types' import { RegistrationModule } from '../registration/registration.module' -import { LazyModuleLoader } from '@nestjs/core' -import { Libp2pService } from '../libp2p/libp2p.service' -import { Libp2pModule } from '../libp2p/libp2p.module' +import { RegistrationService } from '../registration/registration.service' import { SocketModule } from '../socket/socket.module' -import { removeFilesFromDir } from '../common/utils' -import { Libp2pEvents } from '../libp2p/libp2p.types' +import { ConnectionsManagerModule } from './connections-manager.module' +import { ConnectionsManagerService } from './connections-manager.service' describe('ConnectionsManagerService', () => { let module: TestingModule @@ -126,7 +115,7 @@ describe('ConnectionsManagerService', () => { expect(launchCommunitySpy).toHaveBeenCalledWith(launchCommunityPayload) }) - it('launches community and registrar on init if their data exists in local db', async () => { + it('launches community on init if their data exists in local db', async () => { const launchCommunityPayload: InitCommunityPayload = { id: community.id, peerId: userIdentity.peerId, @@ -141,18 +130,7 @@ describe('ConnectionsManagerService', () => { peers: community.peerList, } - const launchRegistrarPayload: LaunchRegistrarPayload = { - id: community.id, - peerId: userIdentity.peerId.id, - // @ts-expect-error - rootCertString: community.CA?.rootCertString, - // @ts-expect-error - rootKeyString: community.CA?.rootKeyString, - privateKey: 'privateKey', - } - await localDbService.put(LocalDBKeys.COMMUNITY, launchCommunityPayload) - await localDbService.put(LocalDBKeys.REGISTRAR, launchRegistrarPayload) const peerAddress = '/dns4/test.onion/tcp/80/ws/p2p/peerid' await localDbService.put(LocalDBKeys.PEERS, { @@ -166,21 +144,17 @@ describe('ConnectionsManagerService', () => { await connectionsManagerService.closeAllServices() const launchCommunitySpy = jest.spyOn(connectionsManagerService, 'launchCommunity').mockResolvedValue() - const launchRegistrarSpy = jest.spyOn(registrationService, 'launchRegistrar').mockResolvedValue() await connectionsManagerService.init() expect(launchCommunitySpy).toHaveBeenCalledWith(Object.assign(launchCommunityPayload, { peers: [peerAddress] })) - expect(launchRegistrarSpy).toHaveBeenCalledWith(launchRegistrarPayload) }) it('does not launch community on init if its data does not exist in local db', async () => { await connectionsManagerService.closeAllServices() await connectionsManagerService.init() const launchCommunitySpy = jest.spyOn(connectionsManagerService, 'launchCommunity') - const launchRegistrarSpy = jest.spyOn(registrationService, 'launchRegistrar') expect(launchCommunitySpy).not.toHaveBeenCalled() - expect(launchRegistrarSpy).not.toHaveBeenCalled() }) // At this moment, that test have to be skipped, because checking statues is called before launchCommunity method @@ -210,7 +184,7 @@ describe('ConnectionsManagerService', () => { expect(launchSpy).toBeCalledTimes(1) }) - it('Bug reproduction - Error on startup - Error: TOR: Connection already established - Trigger launchCommunity and launchRegistrar from backend and state manager', async () => { + 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, peerId: userIdentity.peerId, @@ -225,19 +199,8 @@ describe('ConnectionsManagerService', () => { peers: community.peerList, } - const launchRegistrarPayload: LaunchRegistrarPayload = { - id: community.id, - peerId: userIdentity.peerId.id, - // @ts-expect-error - rootCertString: community.CA?.rootCertString, - // @ts-expect-error - rootKeyString: community.CA?.rootKeyString, - privateKey: '', - } - // await connectionsManager.init() await localDbService.put(LocalDBKeys.COMMUNITY, launchCommunityPayload) - await localDbService.put(LocalDBKeys.REGISTRAR, launchRegistrarPayload) const peerAddress = '/dns4/test.onion/tcp/80/ws/p2p/peerid' await localDbService.put(LocalDBKeys.PEERS, { @@ -251,11 +214,9 @@ describe('ConnectionsManagerService', () => { await connectionsManagerService.closeAllServices() const launchCommunitySpy = jest.spyOn(connectionsManagerService, 'launchCommunity').mockResolvedValue() - const launchRegistrarSpy = jest.spyOn(registrationService, 'launchRegistrar').mockResolvedValue() await connectionsManagerService.init() expect(launchCommunitySpy).toBeCalledTimes(1) - expect(launchRegistrarSpy).toBeCalledTimes(1) }) }) 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 014931a1ca..8c759f8571 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 @@ -30,7 +30,7 @@ import { DateTime } from 'luxon' import waitForExpect from 'wait-for-expect' import { Libp2pEvents } from '../libp2p/libp2p.types' import { sleep } from '../common/sleep' -import { createLibp2pAddress } from '../libp2p/libp2p.utils' +import { createLibp2pAddress } from '@quiet/common' jest.setTimeout(100_000) 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 c62fcc8911..4baec55254 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -42,6 +42,10 @@ import { StorePeerListPayload, UploadFilePayload, PeerId as PeerIdType, + SaveCSRPayload, + SendUserCertificatePayload, + CommunityMetadata, + CommunityMetadataPayload, } from '@quiet/types' import { CONFIG_OPTIONS, QUIET_DIR, SERVER_IO_PROVIDER, SOCKS_PROXY_AGENT } from '../const' import { ConfigOptions, GetPorts, ServerIoProviderTypes } from '../types' @@ -165,17 +169,9 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI if ([ServiceState.LAUNCHING, ServiceState.LAUNCHED].includes(this.communityState)) return this.communityState = ServiceState.LAUNCHING } - const registrarData: LaunchRegistrarPayload = await this.localDbService.get(LocalDBKeys.REGISTRAR) - if (registrarData) { - if ([ServiceState.LAUNCHING, ServiceState.LAUNCHED].includes(this.registrarState)) return - this.registrarState = ServiceState.LAUNCHING - } if (community) { await this.launchCommunity(community) } - if (registrarData) { - await this.registrationService.launchRegistrar(registrarData) - } } public async closeAllServices(options: { saveTor: boolean } = { saveTor: false }) { @@ -273,7 +269,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI community: { ...community, privateKey: network2.hiddenService.privateKey, - registrarUrl: community.registrarUrl || network2.hiddenService.onionAddress.split('.')[0], + registrarUrl: community.registrarUrl || network2.hiddenService.onionAddress.split('.')[0], // TODO: remove }, network, } @@ -281,12 +277,14 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } public async createCommunity(payload: InitCommunityPayload) { + console.log('ConnectionsManager.createCommunity peers:', payload.peers) await this.launchCommunity(payload) this.logger(`Created and launched community ${payload.id}`) this.serverIoProvider.io.emit(SocketActionTypes.NEW_COMMUNITY, { id: payload.id }) } public async launchCommunity(payload: InitCommunityPayload) { + console.log('ConnectionsManager.launchCommunity peers:', payload.peers) this.communityState = ServiceState.LAUNCHING const communityData: InitCommunityPayload = await this.localDbService.get(LocalDBKeys.COMMUNITY) if (!communityData) { @@ -327,7 +325,6 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI const { Libp2pModule } = await import('../libp2p/libp2p.module') const moduleRef = await this.lazyModuleLoader.load(() => Libp2pModule) - this.logger('launchCommunityFromStorage') const { Libp2pService } = await import('../libp2p/libp2p.service') const lazyService = moduleRef.get(Libp2pService) this.libp2pService = lazyService @@ -336,6 +333,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI const _peerId = await peerIdFromKeys(restoredRsa.marshalPubKey(), restoredRsa.marshalPrivKey()) let peers = payload.peers + console.log(`Launching community ${payload.id}, payload peers: ${peers}`) if (!peers || peers.length === 0) { peers = [this.libp2pService.createLibp2pAddress(onionAddress, _peerId.toString())] } @@ -344,9 +342,6 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI peerId: _peerId, listenAddresses: [this.libp2pService.createLibp2pListenAddress(onionAddress)], agent: this.socksProxyAgent, - cert: payload.certs.certificate, - key: payload.certs.key, - ca: payload.certs.CA, localAddress: this.libp2pService.createLibp2pAddress(onionAddress, _peerId.toString()), targetPort: this.ports.libp2pHiddenService, peers, @@ -397,19 +392,9 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.registrationService.on(SocketActionTypes.SAVED_OWNER_CERTIFICATE, payload => { this.serverIoProvider.io.emit(SocketActionTypes.SAVED_OWNER_CERTIFICATE, payload) }) - this.registrationService.on(RegistrationEvents.SPAWN_HS_FOR_REGISTRAR, async payload => { - await this.tor.spawnHiddenService({ - targetPort: payload.port, - privKey: payload.privateKey, - virtPort: payload.targetPort, - }) - }) this.registrationService.on(RegistrationEvents.ERROR, payload => { emitError(this.serverIoProvider.io, payload) }) - this.registrationService.on(SocketActionTypes.SEND_USER_CERTIFICATE, payload => { - this.serverIoProvider.io.emit(SocketActionTypes.SEND_USER_CERTIFICATE, payload) - }) this.registrationService.on(RegistrationEvents.NEW_USER, async payload => { await this.storageService?.saveCertificate(payload) }) @@ -446,18 +431,16 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.communityState = ServiceState.LAUNCHING await this.launchCommunity(args) }) - // Registration this.socketService.on(SocketActionTypes.LAUNCH_REGISTRAR, async (args: LaunchRegistrarPayload) => { + // Event left for setting permsData purposes this.logger(`socketService - ${SocketActionTypes.LAUNCH_REGISTRAR}`) - - const communityData = await this.localDbService.get(LocalDBKeys.REGISTRAR) - if (!communityData) { - await this.localDbService.put(LocalDBKeys.REGISTRAR, args) + this.registrationService.permsData = { + certificate: args.rootCertString, + privKey: args.rootKeyString, } - console.log('this.registrarState', this.registrarState) - if ([ServiceState.LAUNCHING, ServiceState.LAUNCHED].includes(this.registrarState)) return - this.registrarState = ServiceState.LAUNCHING - await this.registrationService.launchRegistrar(args) + }) + this.socketService.on(SocketActionTypes.SEND_COMMUNITY_METADATA, async (payload: CommunityMetadata) => { + await this.storageService?.updateCommunityMetadata(payload) }) this.socketService.on(SocketActionTypes.SAVED_OWNER_CERTIFICATE, async (args: SaveOwnerCertificatePayload) => { const saveCertificatePayload: SaveCertificatePayload = { @@ -466,18 +449,10 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } await this.storageService?.saveCertificate(saveCertificatePayload) }) - this.socketService.on(SocketActionTypes.REGISTER_USER_CERTIFICATE, async (args: RegisterUserCertificatePayload) => { - // if (!this.socksProxyAgent) { - // this.createAgent() - // } - - await this.registrationService.sendCertificateRegistrationRequest( - args.serviceAddress, - args.userCsr, - args.communityId, - 120_000, - this.socksProxyAgent - ) + this.socketService.on(SocketActionTypes.SAVE_USER_CSR, async (payload: SaveCSRPayload) => { + console.log(`On ${SocketActionTypes.SAVE_USER_CSR}: ${payload.csr}`) + await this.storageService?.saveCSR(payload) + this.serverIoProvider.io.emit(SocketActionTypes.SAVED_USER_CSR, payload) }) this.socketService.on( SocketActionTypes.REGISTER_OWNER_CERTIFICATE, @@ -592,5 +567,17 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI console.log('emitting deleted channel event back to state manager') this.serverIoProvider.io.emit(SocketActionTypes.CHANNEL_DELETION_RESPONSE, payload) }) + this.storageService.on(StorageEvents.REPLICATED_CSR, async (payload: { csr: string }) => { + console.log(`On ${StorageEvents.REPLICATED_CSR}`) + this.registrationService.emit(RegistrationEvents.REGISTER_USER_CERTIFICATE, payload.csr) + }) + this.storageService.on(StorageEvents.REPLICATED_COMMUNITY_METADATA, (payload: CommunityMetadata) => { + console.log(`On ${StorageEvents.REPLICATED_COMMUNITY_METADATA}: ${payload}`) + const communityMetadataPayload: CommunityMetadataPayload = { + rootCa: payload.rootCa, + ownerCertificate: payload.ownerCertificate, + } + this.serverIoProvider.io.emit(SocketActionTypes.SAVE_COMMUNITY_METADATA, communityMetadataPayload) + }) } } diff --git a/packages/backend/src/nest/libp2p/libp2p.service.ts b/packages/backend/src/nest/libp2p/libp2p.service.ts index fa98820843..311b8f2b47 100644 --- a/packages/backend/src/nest/libp2p/libp2p.service.ts +++ b/packages/backend/src/nest/libp2p/libp2p.service.ts @@ -15,10 +15,10 @@ import { multiaddr } from '@multiformats/multiaddr' import { ConnectionProcessInfo, PeerId, SocketActionTypes } from '@quiet/types' import { SERVER_IO_PROVIDER, SOCKS_PROXY_AGENT } from '../const' import { ServerIoProviderTypes } from '../types' -import { createLibp2pListenAddress, createLibp2pAddress } from './libp2p.utils' import Logger from '../common/logger' import { webSockets } from '../websocketOverTor' import { all } from '../websocketOverTor/filters' +import { createLibp2pAddress, createLibp2pListenAddress } from '@quiet/common' @Injectable() export class Libp2pService extends EventEmitter { @@ -44,7 +44,7 @@ export class Libp2pService extends EventEmitter { return createLibp2pListenAddress(address) } - public async createInstance(params: Libp2pNodeParams): Promise { + public async createInstance(params: Libp2pNodeParams): Promise { if (this.libp2pInstance) { return this.libp2pInstance } @@ -118,6 +118,7 @@ export class Libp2pService extends EventEmitter { dialInChunks.stop() this.connectedPeers.set(remotePeerId, DateTime.utc().valueOf()) + this.logger(`${this.connectedPeers.size} connected peers`) this.emit(Libp2pEvents.PEER_CONNECTED, { peers: [remotePeerId], @@ -144,6 +145,7 @@ export class Libp2pService extends EventEmitter { const connectionDuration: number = connectionEndTime - connectionStartTime this.connectedPeers.delete(remotePeerId) + this.logger(`${this.connectedPeers.size} connected peers`) this.emit(Libp2pEvents.PEER_DISCONNECTED, { peer: remotePeerId, diff --git a/packages/backend/src/nest/libp2p/libp2p.types.ts b/packages/backend/src/nest/libp2p/libp2p.types.ts index a912015c23..d32cc0230f 100644 --- a/packages/backend/src/nest/libp2p/libp2p.types.ts +++ b/packages/backend/src/nest/libp2p/libp2p.types.ts @@ -11,9 +11,6 @@ export interface Libp2pNodeParams { peerId: any listenAddresses: string[] agent: Agent - cert: string - key: string - ca: string[] localAddress: string targetPort: number peers: string[] diff --git a/packages/backend/src/nest/registration/registration.service.ts b/packages/backend/src/nest/registration/registration.service.ts index c3d8a9b172..f3bb56d8e4 100644 --- a/packages/backend/src/nest/registration/registration.service.ts +++ b/packages/backend/src/nest/registration/registration.service.ts @@ -30,7 +30,7 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { private _server: Server private _port: number public registrationService: any - public certificates: string[] + public certificates: string[] = [] private _permsData: PermsData private _ownerCertificate: string @@ -40,8 +40,17 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { onModuleInit() { this.on(RegistrationEvents.SET_CERTIFICATES, certs => { + console.log('SET CERTIFICATES', certs) this.setCertificates(certs) }) + this.on(RegistrationEvents.REGISTER_USER_CERTIFICATE, async (csr: string) => { + if (!this._permsData) { + console.log('NO PERMS DATA') + return + } + console.log('CSR in registration service', csr) + const response = await this.registerUser(csr) + }) this.setRouting() } @@ -84,6 +93,14 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { }) } + public set permsData(perms: PermsData) { + console.log('Setting owner perms data') + this._permsData = { + certificate: perms.certificate, + privKey: perms.privKey, + } + } + public async registerOwnerCertificate(payload: RegisterOwnerCertificatePayload): Promise { let cert: string try { @@ -123,7 +140,7 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { this.emit(response.eventType, response.data) } - private async registerUser(csr: string): Promise<{ status: number; body: any }> { + public async registerUser(csr: string): Promise<{ status: number; body: any }> { const result = await registerUser(csr, this._permsData, this.certificates, this._ownerCertificate) if (result?.status === 200) { this.emit(RegistrationEvents.NEW_USER, { certificate: result.body.certificate, rootPermsData: this._permsData }) diff --git a/packages/backend/src/nest/registration/registration.types.ts b/packages/backend/src/nest/registration/registration.types.ts index 62e81b59c1..dcffe9d7f9 100644 --- a/packages/backend/src/nest/registration/registration.types.ts +++ b/packages/backend/src/nest/registration/registration.types.ts @@ -4,4 +4,5 @@ export enum RegistrationEvents { NEW_USER = 'newUser', SET_CERTIFICATES = 'setCertificates', REGISTRAR_STATE = 'registrarState', + REGISTER_USER_CERTIFICATE = 'registerUserCertificate', } diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts index 4936e4a32a..bb554a0916 100644 --- a/packages/backend/src/nest/socket/socket.service.ts +++ b/packages/backend/src/nest/socket/socket.service.ts @@ -15,6 +15,8 @@ import { LaunchRegistrarPayload, Community, DeleteFilesFromChannelSocketPayload, + SaveCSRPayload, + CommunityMetadata, } from '@quiet/types' import cors, { CorsOptions } from 'cors' import EventEmitter from 'events' @@ -100,11 +102,11 @@ export class SocketService extends EventEmitter implements OnModuleInit { this.emit(SocketActionTypes.ASK_FOR_MESSAGES, payload) }) - socket.on(SocketActionTypes.REGISTER_USER_CERTIFICATE, async (payload: RegisterUserCertificatePayload) => { - this.logger(`Registering user certificate (${payload.communityId}) on ${payload.serviceAddress}`) - this.emit(SocketActionTypes.REGISTER_USER_CERTIFICATE, payload) + socket.on(SocketActionTypes.SAVE_USER_CSR, async (payload: SaveCSRPayload) => { + this.logger(`SAVING user CSR ${payload.csr}`) + this.emit(SocketActionTypes.SAVE_USER_CSR, payload) await new Promise(resolve => setTimeout(() => resolve(), 2000)) - this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.REGISTERING_USER_CERTIFICATE) + this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.SAVING_USER_CSR) }) socket.on(SocketActionTypes.REGISTER_OWNER_CERTIFICATE, async (payload: RegisterOwnerCertificatePayload) => { this.logger(`Registering owner certificate (${payload.communityId})`) @@ -114,6 +116,13 @@ export class SocketService extends EventEmitter implements OnModuleInit { socket.on(SocketActionTypes.SAVE_OWNER_CERTIFICATE, async (payload: SaveOwnerCertificatePayload) => { this.logger(`Saving owner certificate (${payload.peerId}), community: ${payload.id}`) this.emit(SocketActionTypes.SAVED_OWNER_CERTIFICATE, payload) + const communityMetadataPayload: CommunityMetadata = { + id: payload.id, + ownerCertificate: payload.certificate, + rootCa: payload.permsData.certificate, + } + console.log('meta from state-manager', communityMetadataPayload) + this.emit(SocketActionTypes.SEND_COMMUNITY_METADATA, communityMetadataPayload) }) socket.on(SocketActionTypes.CREATE_COMMUNITY, async (payload: InitCommunityPayload) => { this.logger(`Creating community ${payload.id}`) diff --git a/packages/backend/src/nest/storage/storage.service.spec.ts b/packages/backend/src/nest/storage/storage.service.spec.ts index 8e95303aad..9012cf0785 100644 --- a/packages/backend/src/nest/storage/storage.service.spec.ts +++ b/packages/backend/src/nest/storage/storage.service.spec.ts @@ -255,6 +255,10 @@ describe('StorageService', () => { const channelsDbAddress = storageService.channels?.address // @ts-expect-error 'certificates' is private const certificatesDbAddress = storageService.certificates.address + // @ts-expect-error 'certificatesRequests' is private + const certificatesRequestsDbAddress = storageService.certificatesRequests.address + // @ts-expect-error 'communityMetadata' is private + const communityMetadataDbAddress = storageService.communityMetadata.address expect(channelsDbAddress).not.toBeFalsy() expect(certificatesDbAddress).not.toBeFalsy() expect(subscribeToPubSubSpy).toBeCalledTimes(2) @@ -262,6 +266,8 @@ describe('StorageService', () => { expect(subscribeToPubSubSpy).toHaveBeenNthCalledWith(1, [ StorageService.dbAddress(channelsDbAddress), StorageService.dbAddress(certificatesDbAddress), + StorageService.dbAddress(certificatesRequestsDbAddress), + StorageService.dbAddress(communityMetadataDbAddress), ]) // Creating channel: expect(subscribeToPubSubSpy).toHaveBeenNthCalledWith(2, [StorageService.dbAddress(db.address)]) @@ -540,7 +546,7 @@ describe('StorageService', () => { }) describe('Users', () => { - it('gets all users from db', async () => { + it('gets all registered users from db', async () => { await storageService.init(peerId) const mockGetCertificates = jest.fn() // @ts-ignore - Property 'getAllEventLogEntries' is protected @@ -549,7 +555,7 @@ describe('StorageService', () => { 'MIICWzCCAgGgAwIBAgIGAYKIVrmoMAoGCCqGSM49BAMCMA8xDTALBgNVBAMTBG1haW4wHhcNMjIwODEwMTUxOTIxWhcNMzAwMTMxMjMwMDAwWjBJMUcwRQYDVQQDEz5wM29xZHI1M2RrZ2czbjVudWV6bHp5YXdoeHZpdDVlZnh6bHVudnpwN243bG12YTZmajNpNDNhZC5vbmlvbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCAjxbiV781WC8O5emEdavPaQfR0FD8CaqC+P3R3uRdL9xuzGeUu8f5NIplSJ6abBMnanGgcMs34u82buiFROHqjggENMIIBCTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIAgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwLwYJKoZIhvcNAQkMBCIEICSr5xj+pjBSb+YOZ7TMPQJHYs4KASfnc9TugSpKJUG/MBUGCisGAQQBg4wbAgEEBxMFZGV2dnYwPQYJKwYBAgEPAwEBBDATLlFtVlRrVWFkMkdxM01rQ2E4Z2YxMlIxZ3NXRGZrMnlpVEVxYjZZR1hERzJpUTMwSQYDVR0RBEIwQII+cDNvcWRyNTNka2dnM241bnVlemx6eWF3aHh2aXQ1ZWZ4emx1bnZ6cDduN2xtdmE2ZmozaTQzYWQub25pb24wCgYIKoZIzj0EAwIDSAAwRQIhAIXhkkgs3H6GcZ1GYrSL2qJYDRQcpZlmcbq7YjpJHaORAiBMfkwP75v08R/ud6BPWvdS36corT+596+HzpqFt6bffw==', 'MIICYTCCAgegAwIBAgIGAYKIYnYuMAoGCCqGSM49BAMCMA8xDTALBgNVBAMTBG1haW4wHhcNMjIwODEwMTUzMjEwWhcNMzAwMTMxMjMwMDAwWjBJMUcwRQYDVQQDEz52bnl3dWl5bDdwN2lnMm11cmNzY2R5emtza281M2U0azNkcGRtMnlvb3B2dnUyNXA2d3dqcWJhZC5vbmlvbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABM0cOt7jMJ6YhRvL9nhbDCh42QJPKDet/Zc2PJ9rm6CzYz1IXc5uRUCUNZSnNykVMZknogAavp0FjV+cFXzV8gGjggETMIIBDzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIAgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwLwYJKoZIhvcNAQkMBCIEIIsBwPwIhLSltj9dnkgkMq3sOe3RVha9Mhukop6XOoISMBsGCisGAQQBg4wbAgEEDRMLZHNrZmpia3NmaWcwPQYJKwYBAgEPAwEBBDATLlFtZDJVbjlBeW5va1pyY1pHc011YXFndXBUdGlkSEdRblVrTlZmRkZBZWY5N0MwSQYDVR0RBEIwQII+dm55d3VpeWw3cDdpZzJtdXJjc2NkeXprc2tvNTNlNGszZHBkbTJ5b29wdnZ1MjVwNnd3anFiYWQub25pb24wCgYIKoZIzj0EAwIDSAAwRQIgAiCmGfUuSG010CxLEzu9mAQOgDq//SHI9LkXbmCxaAUCIQC9xzmkRBxq5HmNomYJ9ZAJXaY3J6+VqBYthaVnv0bhMw==', ]) - const allUsers = storageService.getAllUsers() + const allUsers = storageService.getAllRegisteredUsers() expect(allUsers).toStrictEqual([ { onionAddress: 'p3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad.onion', @@ -565,6 +571,34 @@ describe('StorageService', () => { }, ]) }) + 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([ + 'MIIDHjCCAsMCAQAwSTFHMEUGA1UEAxM+NnZ1MmJ4a2k3NzdpdDNjcGF5djZmcTZ2cGw0a2Uza3pqN2d4aWNmeWdtNTVkaGh0cGh5ZmR2eWQub25pb24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATMpfp2hSfWFL26OZlZKZEWG9fyAM1ndlEzO0kLxT0pA/7/fs+a5X/s4TkzqCVVQSzhas/84q0WE99ScAcM1LQJoIICFjAuBgkqhkiG9w0BCQ4xITAfMB0GA1UdDgQWBBR6VRzktP1pzZxsGUaJivNUrtgSrzCCAUcGCSqGSIb3DQEJDDGCATgEggE0KZq9s6HEViRfplVgYkulg6XV411ZRe4U1UjfXTf1pRaygfcenGbT6RRagPtZzjuq5hHdYhqDjRzZhnbn8ZASYTgBM7qcseUq5UpS1pE08DI2jePKqatp3Pzm6a/MGSziESnREx784JlKfwKMjJl33UA8lQm9nhSeAIHyBx3c4Lf8IXdW2n3rnhbVfjpBMAxwh6lt+e5agtGXy+q/xAESUeLPfUgRYWctlLgt8Op+WTpLyBkZsVFoBvJrMt2XdM0RI32YzTRr56GXFa4VyQmY5xXwlQSPgidAP7jPkVygNcoeXvAz2ZCk3IR1Cn3mX8nMko53MlDNaMYldUQA0ug28/S7BlSlaq2CDD4Ol3swTq7C4KGTxKrI36ruYUZx7NEaQDF5V7VvqPCZ0fZoTIJuSYTQ67gwEQYKKwYBBAGDjBsCATEDEwFvMD0GCSsGAQIBDwMBATEwEy5RbVhSWTRyaEF4OE11cThkTUdrcjlxa25KZEU2VUhaRGRHYURSVFFFYndGTjViMEcGA1UdETFAEz42dnUyYnhraTc3N2l0M2NwYXl2NmZxNnZwbDRrZTNremo3Z3hpY2Z5Z201NWRoaHRwaHlmZHZ5ZC5vbmlvbjAKBggqhkjOPQQDAgNJADBGAiEAt+f1u/bchg5AZHv6NTGNoXeejTRWUhX3ioGwW6TGg84CIQCHqKNzDh2JjS/hUHx5PApAmfNnQTSf19X6LnNHQweU1g==', + 'MIIDHTCCAsMCAQAwSTFHMEUGA1UEAxM+eTd5Y3ptdWdsMnRla2FtaTdzYmR6NXBmYWVtdng3YmFod3RocmR2Y2J6dzV2ZXgyY3JzcjI2cWQub25pb24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATMq0l4bCmjdb0grtzpwtDVLM9E1IQpL9vrB4+lD9OBZzlrx2365jV7shVu9utas8w8fxtKoBZSnT5+32ZMFTB4oIICFjAuBgkqhkiG9w0BCQ4xITAfMB0GA1UdDgQWBBSoDQpTZdEvi1/Rr/muVXT1clyKRDCCAUcGCSqGSIb3DQEJDDGCATgEggE0BQvyvkiiXEf/PLKnsR1Ba9AhYsVO8o56bnftUnoVzBlRZgUzLJvOSroPk/EmbVz+okhMrcYNgCWHvxrAqHVVq0JRP6bi98BtCUotx6OPFHp5K5QCL60hod1uAnhKocyJG9tsoM9aS+krn/k+g4RCBjiPZ25cC7QG/UNr6wyIQ8elBho4MKm8iOp7EShSsZOV1f6xrnXYCC/zyUc85GEuycLzVImgAQvPATbdMzY4zSGnNLHxkvSUNxaR9LnEWf+i1jeqcOiXOvmdyU5Be3ZqhGKvvBg/5vyLQiCIfeapjZemnLqFHQBitglDm2xnKL6HzMyfZoAHPV7YcWYR4spU9Ju8Q8aqSeAryx7sx55eSR4GO5UQTo5DrQn6xtkwOZ/ytsOknFthF8jcA9uTAMDKA2TylCUwEQYKKwYBBAGDjBsCATEDEwFvMD0GCSsGAQIBDwMBATEwEy5RbVQxOFV2blVCa3NlTWMzU3FuZlB4cEh3TjhuekxySmVOU0xadGM4ckFGWGh6MEcGA1UdETFAEz55N3ljem11Z2wydGVrYW1pN3NiZHo1cGZhZW12eDdiYWh3dGhyZHZjYnp3NXZleDJjcnNyMjZxZC5vbmlvbjAKBggqhkjOPQQDAgNIADBFAiEAoFrAglxmk7ciD6AHQOB1qEoLu0NARcxgwmIry8oeTHwCICyXp5NJQ9Z8vReIAQNng2H2+/XjHifZEWzhoN0VkcBx', + ]) + const allUsers = storageService.getAllUsers() + + expect(allUsers).toStrictEqual([ + { + onionAddress: '6vu2bxki777it3cpayv6fq6vpl4ke3kzj7gxicfygm55dhhtphyfdvyd.onion', + peerId: 'QmXRY4rhAx8Muq8dMGkr9qknJdE6UHZDdGaDRTQEbwFN5b', + dmPublicKey: + '299abdb3a1c456245fa65560624ba583a5d5e35d5945ee14d548df5d37f5a516b281f71e9c66d3e9145a80fb59ce3baae611dd621a838d1cd98676e7f1901261380133ba9cb1e52ae54a52d69134f032368de3caa9ab69dcfce6e9afcc192ce21129d1131efce0994a7f028c8c9977dd403c9509bd9e149e0081f2071ddce0b7fc217756da7deb9e16d57e3a41300c7087a96df9ee5a82d197cbeabfc4011251e2cf7d481161672d94b82df0ea7e593a4bc81919b1516806f26b32dd9774cd11237d98cd346be7a19715ae15c90998e715f095048f8227403fb8cf915ca035ca1e5ef033d990a4dc84750a7de65fc9cc928e773250cd68c625754400d2e836f3f4bb0654a56aad820c3e0e977b304eaec2e0a193c4aac8dfaaee614671ecd11a40317957b56fa8f099d1f6684c826e4984d0ebb8', + username: 'o', + }, + { + onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd.onion', + peerId: 'QmT18UvnUBkseMc3SqnfPxpHwN8nzLrJeNSLZtc8rAFXhz', + dmPublicKey: + '050bf2be48a25c47ff3cb2a7b11d416bd02162c54ef28e7a6e77ed527a15cc19516605332c9bce4aba0f93f1266d5cfea2484cadc60d802587bf1ac0a87555ab42513fa6e2f7c06d094a2dc7a38f147a792b94022fad21a1dd6e02784aa1cc891bdb6ca0cf5a4be92b9ff93e83844206388f676e5c0bb406fd436beb0c8843c7a5061a3830a9bc88ea7b112852b19395d5feb1ae75d8082ff3c9473ce4612ec9c2f35489a0010bcf0136dd333638cd21a734b1f192f494371691f4b9c459ffa2d637aa70e8973af99dc94e417b766a8462afbc183fe6fc8b4220887de6a98d97a69cba851d0062b609439b6c6728be87cccc9f6680073d5ed8716611e2ca54f49bbc43c6aa49e02bcb1eecc79e5e491e063b95104e8e43ad09fac6d930399ff2b6c3a49c5b6117c8dc03db9300c0ca0364f29425', + username: 'o', + }, + ]) + }) }) describe('Files deletion', () => { diff --git a/packages/backend/src/nest/storage/storage.service.ts b/packages/backend/src/nest/storage/storage.service.ts index 1b2300e9d3..0742523494 100644 --- a/packages/backend/src/nest/storage/storage.service.ts +++ b/packages/backend/src/nest/storage/storage.service.ts @@ -7,6 +7,8 @@ import { parseCertificate, verifySignature, verifyUserCert, + parseCertificationRequest, + getReqFieldValue, } from '@quiet/identity' import type { IPFS } from 'ipfs-core' import OrbitDB from 'orbit-db' @@ -15,18 +17,20 @@ import KeyValueStore from 'orbit-db-kvstore' import path from 'path' import { EventEmitter } from 'events' import PeerId from 'peer-id' -import { getCrypto } from 'pkijs' +import { CertificationRequest, getCrypto } from 'pkijs' import { stringToArrayBuffer } from 'pvutils' import validate from '../validation/validators' import { CID } from 'multiformats/cid' import { ChannelMessage, + CommunityMetadata, ConnectionProcessInfo, DeleteFilesFromChannelSocketPayload, FileMetadata, NoCryptoEngineError, PublicChannel, PushNotificationPayload, + SaveCSRPayload, SaveCertificatePayload, SocketActionTypes, User, @@ -43,7 +47,7 @@ import AccessControllers from 'orbit-db-access-controllers' import { MessagesAccessController } from './MessagesAccessController' import { createChannelAccessController } from './ChannelsAccessController' import Logger from '../common/logger' -import { DirectMessagesRepo, IMessageThread, PublicChannelsRepo } from '../common/types' +import { DirectMessagesRepo, PublicChannelsRepo } from '../common/types' import { removeFiles, removeDirs, createPaths, getUsersAddresses } from '../common/utils' import { StorageEvents } from './storage.types' @@ -55,10 +59,12 @@ interface DBOptions { export class StorageService extends EventEmitter { public channels: KeyValueStore private certificates: EventStore + private certificatesRequests: EventStore public publicChannelsRepos: Map = new Map() public directMessagesRepos: Map = new Map() private publicKeysMap: Map = new Map() private userNamesMap: Map = new Map() + private communityMetadata: KeyValueStore private ipfs: IPFS private orbitDb: OrbitDB private filesManager: IpfsFileManagerService @@ -150,6 +156,12 @@ export class StorageService extends EventEmitter { if (this.certificates?.address) { dbs.push(this.certificates.address) } + if (this.certificatesRequests?.address) { + dbs.push(this.certificatesRequests.address) + } + if (this.communityMetadata?.address) { + dbs.push(this.communityMetadata.address) + } const channels = this.publicChannelsRepos.values() @@ -172,7 +184,7 @@ export class StorageService extends EventEmitter { return } for (const a of addr) { - this.logger(`Pubsub - subscribe to ${addr}`) + this.logger(`Pubsub - subscribe to ${a}`) // @ts-ignore await this.orbitDb._pubsub.subscribe( a, @@ -203,16 +215,53 @@ export class StorageService extends EventEmitter { } public async initDatabases() { - this.logger('1/4') + this.logger('1/5') await this.createDbForChannels() - this.logger('2/4') + this.logger('2/5') await this.createDbForCertificates() - this.logger('3/4') + this.logger('3/5') + await this.createDbForCertificatesRequests() + this.logger('4/5') + await this.createDbForCommunityMetadata() + this.logger('5/5') await this.initAllChannels() - this.logger('4/4') this.logger('Initialized DBs') this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.INITIALIZED_DBS) } + private async createDbForCommunityMetadata() { + this.logger('createDbForCommunityMetadata init') + this.communityMetadata = await this.orbitDb.keyvalue('community-metadata', { + replicate: false, + accessController: { + write: ['*'], + }, + }) + + this.communityMetadata.events.on('write', async (_address, _entry) => { + this.logger('WRITE: communityMetadata') + }) + + this.communityMetadata.events.on('replicated', async () => { + this.logger('Replicated community metadata') + // @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options' + await this.communityMetadata.load({ fetchEntryTimeout: 15000 }) + this.emit(StorageEvents.REPLICATED_COMMUNITY_METADATA, Object.values(this.communityMetadata.all)[0]) + }) + // @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options' + await this.communityMetadata.load({ fetchEntryTimeout: 15000 }) + } + + public async updateCommunityMetadata(communityMetadata: CommunityMetadata) { + this.logger(`Updating community metadata`) + if (!communityMetadata.id) return + const meta = this.communityMetadata.get(communityMetadata.id) + console.log('meta from db', meta) + if (meta?.ownerCertificate && meta?.rootCa) return + await this.communityMetadata.put(communityMetadata.id, { + ...meta, + ...communityMetadata, + }) + } private async __stopOrbitDb() { if (this.orbitDb) { @@ -245,19 +294,25 @@ export class StorageService extends EventEmitter { public async stopOrbitDb() { try { - if (this.channels) { - await this.channels.close() - } + await this.channels?.close() } catch (e) { - this.logger.error('channels', e) + this.logger.error('Error closing channels db', e) } try { - if (this.certificates) { - await this.certificates.close() - } + await this.certificates?.close() } catch (e) { - this.logger.error('certificates', e) + this.logger.error('Error closing certificates db', e) + } + try { + await this.certificatesRequests?.close() + } catch (e) { + this.logger.error('Error closing certificates db', e) + } + try { + await this.communityMetadata?.close() + } catch (e) { + this.logger.error('Error closing community metadata db', e) } await this.__stopOrbitDb() await this.__stopIPFS() @@ -265,7 +320,9 @@ export class StorageService extends EventEmitter { public async updatePeersList() { const allUsers = this.getAllUsers() - const peers = await getUsersAddresses(allUsers) + const registeredUsers = this.getAllRegisteredUsers() + const peers = [...new Set(await getUsersAddresses(allUsers.concat(registeredUsers)))] + console.log('updatePeersList, peers count:', peers.length) const community = await this.localDbService.get(LocalDBKeys.COMMUNITY) this.emit(StorageEvents.UPDATE_PEERS_LIST, { communityId: community.id, peerList: peers }) } @@ -332,6 +389,51 @@ export class StorageService extends EventEmitter { this.logger('STORAGE: Finished createDbForCertificates') } + public async createDbForCertificatesRequests() { + this.logger('certificatesRequests db init') + this.certificatesRequests = await this.orbitDb.log('csrs', { + replicate: false, + accessController: { + write: ['*'], + }, + }) + this.certificatesRequests.events.on('replicate.progress', async (_address, _hash, entry, _progress, _total) => { + const csr = entry.payload.value + this.logger('REPLICATED CSR', csr) + let parsedCSR: CertificationRequest + try { + parsedCSR = parseCertificationRequest(csr) + } catch (e) { + this.logger.error(`csrs replicate.progress: could not parse certificate request`) + return + } + + const username = getReqFieldValue(parsedCSR, CertFieldsTypes.nickName) + if (!username) { + this.logger.error( + `csrs replicate.progress: could not parse certificate request for field type ${CertFieldsTypes.nickName}` + ) + return + } + + this.emit(StorageEvents.REPLICATED_CSR, { csr: csr }) + }) + this.certificatesRequests.events.on('replicated', async () => { + this.logger('REPLICATED: CSRs') + await this.updatePeersList() + }) + this.certificatesRequests.events.on('write', async (_address, entry) => { + this.logger('Saved CSR locally') + await this.updatePeersList() + }) + + // @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options' + await this.certificates.load({ fetchEntryTimeout: 15000 }) + const allcsrs = this.getAllEventLogEntries(this.certificatesRequests) + this.logger('ALL Certificates COUNT:', allcsrs.length) + this.logger('STORAGE: Finished creating certificatesRequests db') + } + public async loadAllChannels() { this.logger('Getting all channels') // @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options' @@ -755,7 +857,30 @@ export class StorageService extends EventEmitter { return true } - public getAllUsers(): User[] { + public async saveCSR(payload: SaveCSRPayload): Promise { + this.logger('About to save csr...') + if (!payload.csr) { + this.logger('CSR is either null or undefined, not saving to db') + return false + } + // TODO: Verify CSR + try { + parseCertificationRequest(payload.csr) + } catch (e) { + this.logger.error(`Cannot save csr ${payload.csr}. Reason: ${e.message}`) + return false + } + + const csrs = this.getAllEventLogEntries(this.certificatesRequests) + + if (csrs.includes(payload.csr)) return false + + this.logger('Saving csr...') + await this.certificatesRequests.add(payload.csr) + return true + } + + public getAllRegisteredUsers(): User[] { const certs = this.getAllEventLogEntries(this.certificates) const allUsers: User[] = [] for (const cert of certs) { @@ -770,6 +895,22 @@ export class StorageService extends EventEmitter { return allUsers } + public getAllUsers(): User[] { + const csrs = this.getAllEventLogEntries(this.certificatesRequests) + console.log('csrs', csrs.length) + const allUsers: User[] = [] + 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) + const dmPublicKey = getReqFieldValue(parsedCert, CertFieldsTypes.dmPublicKey) + if (!onionAddress || !peerId || !username || !dmPublicKey) continue + allUsers.push({ onionAddress, peerId, username, dmPublicKey }) + } + return allUsers + } + public usernameCert(username: string): string | null { /** * Check if given username is already in use diff --git a/packages/backend/src/nest/storage/storage.types.ts b/packages/backend/src/nest/storage/storage.types.ts index 617b944333..84b3286b5d 100644 --- a/packages/backend/src/nest/storage/storage.types.ts +++ b/packages/backend/src/nest/storage/storage.types.ts @@ -4,6 +4,7 @@ export enum StorageEvents { // Peers UPDATE_PEERS_LIST = 'updatePeersList', LOAD_CERTIFICATES = 'loadCertificates', + REPLICATED_CSR = 'replicatedCsr', // Public Channels LOAD_PUBLIC_CHANNELS = 'loadPublicChannels', LOAD_ALL_PRIVATE_CONVERSATIONS = 'loadAllPrivateConversations', @@ -22,6 +23,8 @@ export enum StorageEvents { LOAD_ALL_DIRECT_MESSAGES = 'loadAllDirectMessages', // Misc SEND_PUSH_NOTIFICATION = 'sendPushNotification', + // Community + REPLICATED_COMMUNITY_METADATA = 'replicatedCommunityMetadata', } export interface InitStorageParams { communityId: string diff --git a/packages/backend/src/nest/tor/tor-control.service.ts b/packages/backend/src/nest/tor/tor-control.service.ts index 1c2e6f509b..f833154e87 100644 --- a/packages/backend/src/nest/tor/tor-control.service.ts +++ b/packages/backend/src/nest/tor/tor-control.service.ts @@ -50,7 +50,7 @@ export class TorControl implements OnModuleInit { }) } - private async disconnect() { + private disconnect() { try { this.connection?.end() } catch (e) { @@ -62,11 +62,12 @@ export class TorControl implements OnModuleInit { // eslint-disable-next-line @typescript-eslint/ban-types private async _sendCommand(command: string, resolve: Function, reject: Function) { await this.connect() + const connectionTimeout = setTimeout(() => { reject('TOR: Send command timeout') }, 5000) this.connection?.on('data', async data => { - await this.disconnect() + this.disconnect() const dataArray = data.toString().split(/\r?\n/) if (dataArray[0].startsWith('250')) { resolve({ code: 250, messages: dataArray }) diff --git a/packages/backend/src/nest/websocketOverTor/websocketOverTor.tor.spec.ts b/packages/backend/src/nest/websocketOverTor/websocketOverTor.tor.spec.ts index 560e5a3f2f..1ffde5daa4 100644 --- a/packages/backend/src/nest/websocketOverTor/websocketOverTor.tor.spec.ts +++ b/packages/backend/src/nest/websocketOverTor/websocketOverTor.tor.spec.ts @@ -16,7 +16,7 @@ import { Tor } from '../tor/tor.service' import crypto from 'crypto' import { TorControl } from '../tor/tor-control.service' import { TorControlAuthType } from '../tor/tor.types' -import { createLibp2pAddress } from '../libp2p/libp2p.utils' +import { createLibp2pAddress } from '@quiet/common' jest.setTimeout(120000) describe('websocketOverTor', () => { @@ -278,73 +278,4 @@ describe('websocketOverTor', () => { }) ).rejects.toBeTruthy() }) - - it.skip('rejects connection if server cert is invalid', async () => { - const pems = await createCertificatesTestHelper(`${service1.onionAddress}`, `${service2.onionAddress}`) - const anotherPems = await createCertificatesTestHelper(`${service1.onionAddress}`, `${service2.onionAddress}`) - - const prepareListenerArg: CreateListenerOptions = { - handler: x => x, - upgrader: { - // @ts-expect-error - upgradeOutbound, - // @ts-expect-error - upgradeInbound, - }, - } - - const signal: AbortSignal = { - ...abortSignalOpts, - addEventListener, - removeEventListener, - } - - const peerId1 = 'Qme5NiSQ6V3cc3nyfYVtkkXDPGBSYEVUNCN5sM4DbyYc7s' - const peerId2 = 'QmeCWxba5Yk1ZAKogQJsaHXoAermE7PgFZqpqyKNg65cSN' - - const websocketsOverTorData1 = { - filter: all, - websocket: { - agent, - cert: anotherPems.servCert, - key: anotherPems.servKey, - ca: [pems.ca], - }, - localAddress: createLibp2pAddress(service1.onionAddress, peerId1), - targetPort: port1Target, - createServer, - } - - const websocketsOverTorData2 = { - filter: all, - websocket: { - agent, - cert: pems.servCert, - key: pems.servKey, - ca: [pems.ca], - }, - localAddress: createLibp2pAddress(service2.onionAddress, peerId2), - targetPort: port2Target, - createServer, - } - - const multiAddress = multiaddr(createLibp2pAddress(service1.onionAddress, peerId1)) - - const ws1 = webSockets(websocketsOverTorData1)() - const ws2 = webSockets(websocketsOverTorData2)() - - listener = await ws1.prepareListener(prepareListenerArg) - - await listener.listen(multiAddress) - - const onConnection = jest.fn() - listener.on('connection', onConnection) - - await expect( - ws2.dial(multiAddress, { - signal, - upgrader: prepareListenerArg.upgrader, - }) - ).rejects.toBeTruthy() - }) }) diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 914f45c769..67ce8a7b5b 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -7,3 +7,4 @@ export * from './sortPeers' export * from './channelAddress' export * from './naming' export * from './fileData' +export * from './libp2p' diff --git a/packages/common/src/invitationCode.test.ts b/packages/common/src/invitationCode.test.ts index 78bd1420c1..28254563bc 100644 --- a/packages/common/src/invitationCode.test.ts +++ b/packages/common/src/invitationCode.test.ts @@ -1,24 +1,75 @@ -import { argvInvitationCode, invitationDeepUrl, invitationShareUrl } from './invitationCode' -import { Site } from './static' +import { + argvInvitationCode, + invitationDeepUrl, + invitationShareUrl, + pairsToInvitationShareUrl, + retrieveInvitationCode, +} from './invitationCode' +import { QUIET_JOIN_PAGE } from './static' describe('Invitation code helper', () => { + const peerId1 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA' + const address1 = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' + const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' + const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' + it('retrieves invitation code from argv', () => { + const expectedCodes = [ + { peerId: peerId1, onionAddress: address1 }, + { peerId: peerId2, onionAddress: address2 }, + ] const result = argvInvitationCode([ 'something', 'quiet:/invalid', 'zbay://invalid', 'quiet://invalid', 'quiet://?param=invalid', - invitationDeepUrl('validCode'), + invitationDeepUrl(expectedCodes), ]) - expect(result).toBe('validCode') + expect(result).toEqual(expectedCodes) }) it('builds proper invitation deep url', () => { - expect(invitationDeepUrl('validCode')).toEqual('quiet://?code=validCode') + expect( + invitationDeepUrl([ + { peerId: 'peerID1', onionAddress: 'address1' }, + { peerId: 'peerID2', onionAddress: 'address2' }, + ]) + ).toEqual('quiet://?peerID1=address1&peerID2=address2') + }) + + it('creates invitation share url based on invitation pairs', () => { + const pairs = [ + { peerId: 'peerID1', onionAddress: 'address1' }, + { peerId: 'peerID2', onionAddress: 'address2' }, + ] + const expected = `${QUIET_JOIN_PAGE}#peerID1=address1&peerID2=address2` + expect(pairsToInvitationShareUrl(pairs)).toEqual(expected) }) it('builds proper invitation share url', () => { - expect(invitationShareUrl('validCode')).toEqual(`https://${Site.DOMAIN}/${Site.JOIN_PAGE}#validCode`) + const peerList = [ + '/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + 'invalidAddress', + '/dns4/somethingElse.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA', + ] + expect(invitationShareUrl(peerList)).toEqual( + `${QUIET_JOIN_PAGE}#QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad&QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA=somethingElse` + ) + }) + + it('retrieves invitation codes from deep url', () => { + const codes = retrieveInvitationCode(`quiet://?${peerId1}=${address1}&${peerId2}=${address2}`) + expect(codes).toEqual([ + { peerId: peerId1, onionAddress: address1 }, + { peerId: peerId2, onionAddress: address2 }, + ]) + }) + + it('retrieves invitation codes from deep url with partly invalid codes', () => { + const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLs' + const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' + const codes = retrieveInvitationCode(`quiet://?${peerId1}=${address1}&${peerId2}=${address2}}`) + expect(codes).toEqual([{ peerId: peerId1, onionAddress: address1 }]) }) }) diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index 6d17e38251..e5e6af26f7 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -1,46 +1,132 @@ -import { InvitationParams, Site } from './static' - -export const retrieveInvitationCode = (url: string): string => { +import { InvitationPair } from '@quiet/types' +import { ONION_ADDRESS_REGEX, Site } from './static' +import { createLibp2pAddress } from './libp2p' +export const retrieveInvitationCode = (url: string): InvitationPair[] => { /** - * Extract invitation code from deep url. - * Valid format: quiet://?code= + * Extract invitation codes from deep url. + * Valid format: quiet://?=&= */ let data: URL try { data = new URL(url) } catch (e) { - return '' + return [] } - if (!data || data.protocol !== 'quiet:') return '' - const code = data.searchParams.get(InvitationParams.CODE) - if (code) { - console.log('Retrieved code:', code) - return code + if (!data || data.protocol !== 'quiet:') return [] + const params = data.searchParams + const codes: InvitationPair[] = [] + for (const [peerId, onionAddress] of params.entries()) { + if (!invitationCodeValid(peerId, onionAddress)) continue + codes.push({ + peerId, + onionAddress, + }) } - return '' + console.log('Retrieved codes:', codes) + return codes } -export const argvInvitationCode = (argv: string[]): string => { +export const invitationShareUrl = (peers: string[] = []): string => { /** - * Extract invitation code from deep url if url is present in argv + * @arg {string[]} peers - List of peer's p2p addresses + * @returns {string} - Complete shareable invitation link, e.g. https://tryquiet.org/join/#=&= */ - let invitationCode = '' - for (const arg of argv) { - invitationCode = retrieveInvitationCode(arg) - if (invitationCode) { - break + console.log('Invitation share url, peers:', peers) + const pairs = [] + for (const peerAddress of peers) { + let peerId: string + let onionAddress: string + try { + peerId = peerAddress.split('/p2p/')[1] + } catch (e) { + console.info(`Could not add peer address '${peerAddress}' to invitation url. Reason: ${e.message}`) + continue + } + try { + onionAddress = peerAddress.split('/tcp/')[0].split('/dns4/')[1] + } catch (e) { + console.info(`Could not add peer address '${peerAddress}' to invitation url. Reason: ${e.message}`) + continue } + + if (!peerId || !onionAddress) { + console.error(`No peerId or address in ${peerAddress}`) + continue + } + const rawAddress = onionAddress.endsWith('.onion') ? onionAddress.split('.')[0] : onionAddress + pairs.push(`${peerId}=${rawAddress}`) + } + + console.log('invitationShareUrl', pairs.join('&')) + const url = new URL(`${Site.MAIN_PAGE}${Site.JOIN_PAGE}#${pairs.join('&')}`) + return url.href +} + +export const pairsToP2pAddresses = (pairs: InvitationPair[]): string[] => { + const addresses: string[] = [] + for (const pair of pairs) { + addresses.push(createLibp2pAddress(pair.onionAddress, pair.peerId)) + } + return addresses +} + +export const pairsToInvitationShareUrl = (pairs: InvitationPair[]) => { + const url = new URL(`${Site.MAIN_PAGE}${Site.JOIN_PAGE}`) + for (const pair of pairs) { + url.searchParams.append(pair.peerId, pair.onionAddress) } - return invitationCode + return url.href.replace('?', '#') } -export const invitationDeepUrl = (code = ''): string => { +export const invitationDeepUrl = (pairs: InvitationPair[] = []): string => { const url = new URL('quiet://') - url.searchParams.append(InvitationParams.CODE, code) + for (const pair of pairs) { + url.searchParams.append(pair.peerId, pair.onionAddress) + } return url.href } -export const invitationShareUrl = (code = ''): string => { - const url = new URL(`https://${Site.DOMAIN}/${Site.JOIN_PAGE}#${code}`) - return url.href +export const argvInvitationCode = (argv: string[]): InvitationPair[] => { + /** + * Extract invitation codes from deep url if url is present in argv + */ + let invitationCodes: InvitationPair[] = [] + for (const arg of argv) { + invitationCodes = retrieveInvitationCode(arg) + if (invitationCodes.length > 0) { + break + } + } + return invitationCodes +} + +export const invitationCodeValid = (peerId: string, onionAddress: string): boolean => { + if (!peerId.match(/^[a-zA-Z0-9]{46}$/g)) { + // TODO: test it more properly e.g with PeerId.createFromB58String(peerId.trim()) + console.log(`PeerId ${peerId} is not valid`) + return false + } + if (!onionAddress.trim().match(ONION_ADDRESS_REGEX)) { + console.log(`Onion address ${onionAddress} is not valid`) + return false + } + return true +} + +export const getInvitationPairs = (code: string) => { + /** + * @param code =&= + */ + const pairs = code.split('&') + const codes: InvitationPair[] = [] + for (const pair of pairs) { + const [peerId, address] = pair.split('=') + if (!peerId || !address) continue + if (!invitationCodeValid(peerId, address)) continue + codes.push({ + peerId: peerId, + onionAddress: address, + }) + } + return codes } diff --git a/packages/backend/src/nest/libp2p/libp2p.utils.ts b/packages/common/src/libp2p.ts similarity index 100% rename from packages/backend/src/nest/libp2p/libp2p.utils.ts rename to packages/common/src/libp2p.ts diff --git a/packages/common/src/static.ts b/packages/common/src/static.ts index 057018d0a7..05b1f35f0a 100644 --- a/packages/common/src/static.ts +++ b/packages/common/src/static.ts @@ -1,11 +1,9 @@ export const ONION_ADDRESS_REGEX = /^[a-z0-9]{56}$/g -export enum InvitationParams { - CODE = 'code', -} - export enum Site { DOMAIN = 'tryquiet.org', MAIN_PAGE = 'https://tryquiet.org/', JOIN_PAGE = 'join', } + +export const QUIET_JOIN_PAGE = `${Site.MAIN_PAGE}${Site.JOIN_PAGE}` diff --git a/packages/common/tsconfig.build.json b/packages/common/tsconfig.build.json index 4c2fe1bc29..b889234c8b 100644 --- a/packages/common/tsconfig.build.json +++ b/packages/common/tsconfig.build.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.build.json", "compilerOptions": { + "target": "ES2020", "rootDir": "./src", "outDir": "./lib", "typeRoots": [ diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index afd02831cc..6adb0ef8de 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "target": "es2017", + "target": "ES2020", "outDir": "./lib", "typeRoots": [ "../@types", diff --git a/packages/desktop/src/main/invitation.ts b/packages/desktop/src/main/invitation.ts index 23f61c847e..39bf432962 100644 --- a/packages/desktop/src/main/invitation.ts +++ b/packages/desktop/src/main/invitation.ts @@ -3,11 +3,15 @@ import path from 'path' import os from 'os' import { execSync } from 'child_process' import { BrowserWindow } from 'electron' +import { InvitationPair } from '@quiet/types' -export const processInvitationCode = (mainWindow: BrowserWindow, code: string) => { - if (!code) return +export const processInvitationCode = (mainWindow: BrowserWindow, codes: InvitationPair[]) => { + if (codes.length === 0) { + console.log('No valid invitation codes, not processing') + return + } mainWindow.webContents.send('invitation', { - code, + codes, }) } diff --git a/packages/desktop/src/main/main.test.ts b/packages/desktop/src/main/main.test.ts index faa8e913fc..0239a05cd0 100644 --- a/packages/desktop/src/main/main.test.ts +++ b/packages/desktop/src/main/main.test.ts @@ -240,20 +240,30 @@ describe('Invitation code', () => { it('handles invitation code on open-url event (on macos)', async () => { expect(mockAppOnCalls[2][0]).toBe('ready') await mockAppOnCalls[2][1]() - const code = 'invitationCode' + const codes = [ + { + peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + }, + ] expect(mockAppOnCalls[1][0]).toBe('open-url') const event = { preventDefault: () => {} } - mockAppOnCalls[1][1](event, invitationDeepUrl(code)) - expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { code: code }) + mockAppOnCalls[1][1](event, invitationDeepUrl(codes)) + expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { codes }) }) it('process invitation code on second-instance event', async () => { - const code = 'invitationCodeArgv' + const codes = [ + { + peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + }, + ] await mockAppOnCalls[2][1]() - const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', invitationDeepUrl(code)] + const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', invitationDeepUrl(codes)] expect(mockAppOnCalls[0][0]).toBe('second-instance') const event = { preventDefault: () => {} } mockAppOnCalls[0][1](event, commandLine) - expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { code: code }) + expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { codes }) }) }) diff --git a/packages/desktop/src/main/main.ts b/packages/desktop/src/main/main.ts index 932780c8e0..b8c20310f4 100644 --- a/packages/desktop/src/main/main.ts +++ b/packages/desktop/src/main/main.ts @@ -11,9 +11,8 @@ import { Crypto } from '@peculiar/webcrypto' import logger from './logger' import { DATA_DIR, DEV_DATA_DIR } from '../shared/static' import { fork, ChildProcess } from 'child_process' -import { getFilesData } from '@quiet/common' +import { argvInvitationCode, getFilesData, retrieveInvitationCode } from '@quiet/common' import { updateDesktopFile, processInvitationCode } from './invitation' -import { argvInvitationCode, retrieveInvitationCode } from '@quiet/common' const ElectronStore = require('electron-store') ElectronStore.initRenderer() diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx index 3985a2845f..a9752d46f8 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx @@ -17,10 +17,17 @@ import PerformCommunityActionComponent from '../PerformCommunityActionComponent' import { inviteLinkField } from '../../../forms/fields/communityFields' import { InviteLinkErrors } from '../../../forms/fieldsErrors' import { CommunityOwnership } from '@quiet/types' -import { Site, InvitationParams } from '@quiet/common' +import { Site, QUIET_JOIN_PAGE } from '@quiet/common' describe('join community', () => { - const validCode = 'nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad' + const validCode = + 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' + const validPair = [ + { + onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + }, + ] it('users switches from join to create', async () => { const { store } = await prepareStore({ @@ -99,7 +106,8 @@ describe('join community', () => { }) it('joins community on submit if connection is ready and registrar url is correct', async () => { - const registrarUrl = 'nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad' + const registrarUrl = + 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' const handleCommunityAction = jest.fn() @@ -127,14 +135,17 @@ describe('join community', () => { expect(submitButton).toBeEnabled() await userEvent.click(submitButton) - await waitFor(() => expect(handleCommunityAction).toBeCalledWith(registrarUrl)) + await waitFor(() => + expect(handleCommunityAction).toBeCalledWith([ + { + peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + }, + ]) + ) }) - it.each([ - [`https://${Site.DOMAIN}/${Site.JOIN_PAGE}#${validCode}`], - [`https://${Site.DOMAIN}/${Site.JOIN_PAGE}/#${validCode}`], - [`https://${Site.DOMAIN}/${Site.JOIN_PAGE}?code=${validCode}`], // Old link format - ])( + it.each([[`${QUIET_JOIN_PAGE}#${validCode}`], [`${QUIET_JOIN_PAGE}/#${validCode}`]])( 'joins community on submit if connection is ready and invitation code is a correct invitation url (%s)', async (invitationLink: string) => { const registrarUrl = new URL(invitationLink) @@ -165,12 +176,13 @@ describe('join community', () => { expect(submitButton).toBeEnabled() await userEvent.click(submitButton) - await waitFor(() => expect(handleCommunityAction).toBeCalledWith(validCode)) + await waitFor(() => expect(handleCommunityAction).toBeCalledWith(validPair)) } ) it('trims whitespaces from registrar url', async () => { - const registrarUrl = 'nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad ' + const registrarUrl = + 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd ' const handleCommunityAction = jest.fn() @@ -198,28 +210,25 @@ describe('join community', () => { expect(submitButton).toBeEnabled() await userEvent.click(submitButton) - await waitFor(() => expect(handleCommunityAction).toBeCalledWith(registrarUrl.trim())) + await waitFor(() => + expect(handleCommunityAction).toBeCalledWith([ + { + peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + }, + ]) + ) }) it.each([ - ['http://nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad.onion', InviteLinkErrors.InvalidCode], - ['nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2ola09bp2', InviteLinkErrors.InvalidCode], - ['nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2ola!', InviteLinkErrors.InvalidCode], - ['nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2ola ', InviteLinkErrors.InvalidCode], + [`http://${validCode}`, InviteLinkErrors.InvalidCode], + ['QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=bbb', InviteLinkErrors.InvalidCode], + ['bbb=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', InviteLinkErrors.InvalidCode], + ['QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE= ', InviteLinkErrors.InvalidCode], ['nqnw4kc4c77fb47lk52m5l57h4tc', InviteLinkErrors.InvalidCode], - [`https://${Site.DOMAIN}/${Site.JOIN_PAGE}?${InvitationParams.CODE}=invalidcode`, InviteLinkErrors.InvalidCode], - [ - `https://otherwebsite.com/${Site.JOIN_PAGE}?${InvitationParams.CODE}=nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad`, - InviteLinkErrors.InvalidCode, - ], - [ - `https://${Site.DOMAIN}/${Site.JOIN_PAGE}?param=nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad`, - InviteLinkErrors.InvalidCode, - ], - [ - `https://${Site.DOMAIN}/share?${InvitationParams.CODE}=nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad`, - InviteLinkErrors.InvalidCode, - ], + [`https://otherwebsite.com/${Site.JOIN_PAGE}#${validCode}`, InviteLinkErrors.InvalidCode], + [`${QUIET_JOIN_PAGE}?param=nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad`, InviteLinkErrors.InvalidCode], + [`${Site.MAIN_PAGE}/share?${validCode}`, InviteLinkErrors.InvalidCode], ])('user inserting invalid url %s should see "%s" error', async (url: string, error: string) => { const handleCommunityAction = jest.fn() @@ -269,7 +278,7 @@ describe('join community', () => { const textInput = result.queryByPlaceholderText(inviteLinkField().fieldProps.placeholder) expect(textInput).not.toBeNull() // @ts-expect-error - await userEvent.type(textInput, 'nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad') + await userEvent.type(textInput, validCode) const submitButton = result.getByTestId('continue-joinCommunity') expect(submitButton).not.toBeNull() @@ -293,7 +302,7 @@ describe('join community', () => { ) const textInput = screen.getByPlaceholderText(inviteLinkField().fieldProps.placeholder) - await userEvent.type(textInput, 'nqnw4kc4c77fb47lk52m5l57h4tcxceo7ymxekfn7yh5m66t4jv2olad') + await userEvent.type(textInput, validCode) const submitButton = screen.getByText('Continue') expect(submitButton).toBeEnabled() diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx index c86b370f4a..590c156805 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { socketSelectors } from '../../../sagas/socket/socket.selectors' -import { CommunityOwnership, CreateNetworkPayload, TOR_BOOTSTRAP_COMPLETE } from '@quiet/types' +import { CommunityOwnership, CreateNetworkPayload, InvitationPair } from '@quiet/types' import { communities, identity, connection } from '@quiet/state-manager' import PerformCommunityActionComponent from '../../../components/CreateJoinCommunity/PerformCommunityActionComponent' import { ModalName } from '../../../sagas/modals/modals.types' @@ -15,7 +15,7 @@ const JoinCommunity = () => { const currentCommunity = useSelector(communities.selectors.currentCommunity) const currentIdentity = useSelector(identity.selectors.currentIdentity) - const invitationCode = useSelector(communities.selectors.invitationCode) + const invitationCode = useSelector(communities.selectors.invitationCodes) const joinCommunityModal = useModal(ModalName.joinCommunityModal) const createCommunityModal = useModal(ModalName.createCommunityModal) @@ -36,10 +36,10 @@ const JoinCommunity = () => { } }, [currentCommunity]) - const handleCommunityAction = (address: string) => { + const handleCommunityAction = (address: InvitationPair[]) => { const payload: CreateNetworkPayload = { ownership: CommunityOwnership.User, - registrar: address, + peers: address, } dispatch(communities.actions.createNetwork(payload)) } diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx index bdf8df169c..ead282576a 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx @@ -12,7 +12,7 @@ import { LoadingButton } from '../ui/LoadingButton/LoadingButton' import { CreateCommunityDictionary, JoinCommunityDictionary } from '../CreateJoinCommunity/community.dictionary' -import { CommunityOwnership } from '@quiet/types' +import { CommunityOwnership, InvitationPair } from '@quiet/types' import { Controller, useForm } from 'react-hook-form' import { TextInput } from '../../forms/components/textInput' @@ -20,8 +20,8 @@ import { InviteLinkErrors } from '../../forms/fieldsErrors' import { IconButton, InputAdornment } from '@mui/material' import VisibilityOff from '@mui/icons-material/VisibilityOff' import Visibility from '@mui/icons-material/Visibility' -import { ONION_ADDRESS_REGEX, parseName } from '@quiet/common' -import { getInvitationCode } from '@quiet/state-manager' +import { ONION_ADDRESS_REGEX, pairsToInvitationShareUrl, parseName } from '@quiet/common' +import { getInvitationCodes } from '@quiet/state-manager' const PREFIX = 'PerformCommunityActionComponent' @@ -129,7 +129,7 @@ interface PerformCommunityActionFormValues { export interface PerformCommunityActionProps { open: boolean communityOwnership: CommunityOwnership - handleCommunityAction: (value: string) => void + handleCommunityAction: (value: any) => void handleRedirection: () => void handleClose: () => void isConnectionReady?: boolean @@ -137,7 +137,7 @@ export interface PerformCommunityActionProps { hasReceivedResponse: boolean revealInputValue?: boolean handleClickInputReveal?: () => void - invitationCode?: string + invitationCode?: InvitationPair[] } export const PerformCommunityActionComponent: React.FC = ({ @@ -179,22 +179,26 @@ export const PerformCommunityActionComponent: React.FC submitForm(handleCommunityAction, values, setFormSent) const submitForm = ( - handleSubmit: (value: string) => void, + handleSubmit: (value: any) => void, values: PerformCommunityActionFormValues, setFormSent: (value: boolean) => void ) => { - let submitValue = communityOwnership === CommunityOwnership.Owner ? parseName(values.name) : values.name.trim() + if (communityOwnership === CommunityOwnership.Owner) { + setFormSent(true) + handleSubmit(parseName(values.name)) + return + } if (communityOwnership === CommunityOwnership.User) { - submitValue = getInvitationCode(submitValue) - if (!submitValue || !submitValue.match(ONION_ADDRESS_REGEX)) { + const codes = getInvitationCodes(values.name.trim()) + if (!codes.length) { setError('name', { message: InviteLinkErrors.InvalidCode }) return } - } - setFormSent(true) - handleSubmit(submitValue) + setFormSent(true) + handleSubmit(codes) + } } const onChange = (name: string) => { @@ -207,9 +211,9 @@ export const PerformCommunityActionComponent: React.FC { - if (communityOwnership === CommunityOwnership.User && invitationCode) { + if (communityOwnership === CommunityOwnership.User && invitationCode?.length) { setFormSent(true) - setValue('name', invitationCode) + setValue('name', pairsToInvitationShareUrl(invitationCode)) } }, [communityOwnership, invitationCode]) diff --git a/packages/desktop/src/renderer/components/CreateUsername/CreateUsername.tsx b/packages/desktop/src/renderer/components/CreateUsername/CreateUsername.tsx index c6b439bc05..a0be2008c5 100644 --- a/packages/desktop/src/renderer/components/CreateUsername/CreateUsername.tsx +++ b/packages/desktop/src/renderer/components/CreateUsername/CreateUsername.tsx @@ -18,10 +18,10 @@ const CreateUsername = () => { const error = useSelector(errors.selectors.registrarErrors) useEffect(() => { - if (currentCommunity && !currentIdentity?.userCertificate && !createUsernameModal.open) { + if (currentCommunity && !currentIdentity?.userCsr && !createUsernameModal.open) { createUsernameModal.handleOpen() } - if (currentIdentity?.userCertificate && createUsernameModal.open) { + if (currentIdentity?.userCsr && createUsernameModal.open) { createUsernameModal.handleClose() } }, [currentIdentity, currentCommunity]) diff --git a/packages/desktop/src/renderer/forms/fields/communityFields.ts b/packages/desktop/src/renderer/forms/fields/communityFields.ts index bda6aa21a4..7d048a35ff 100644 --- a/packages/desktop/src/renderer/forms/fields/communityFields.ts +++ b/packages/desktop/src/renderer/forms/fields/communityFields.ts @@ -36,10 +36,6 @@ export const inviteLinkField = (name = 'name'): FieldData => { }, validation: { required: FieldErrors.Required, - pattern: { - value: /^[a-z0-9:/.#?= ]+$/g, - message: InviteLinkErrors.InvalidCode, - }, }, } } diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index a56c5f0931..ef2e8f1e08 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -22,7 +22,7 @@ ipcRenderer.on('force-save-state', async _event => { ipcRenderer.on('invitation', (_event, invitation) => { console.log('invitation', invitation, 'dispatching action') - store.dispatch(communities.actions.handleInvitationCode(invitation.code)) + store.dispatch(communities.actions.handleInvitationCodes(invitation.codes)) }) const container = document.getElementById('root') diff --git a/packages/desktop/src/renderer/sagas/index.saga.ts b/packages/desktop/src/renderer/sagas/index.saga.ts index 35d0dbc985..e41f8b6faa 100644 --- a/packages/desktop/src/renderer/sagas/index.saga.ts +++ b/packages/desktop/src/renderer/sagas/index.saga.ts @@ -7,7 +7,7 @@ import { socketActions } from './socket/socket.slice' export default function* root(): Generator { const dataPort = new URLSearchParams(window.location.search).get('dataPort') || '' yield all([ - takeEvery(communities.actions.handleInvitationCode.type, handleInvitationCodeSaga), + takeEvery(communities.actions.handleInvitationCodes.type, handleInvitationCodeSaga), startConnectionSaga( socketActions.startConnection({ dataPort: parseInt(dataPort), diff --git a/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.test.ts b/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.test.ts index 470a0ae044..d95c7fd035 100644 --- a/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.test.ts +++ b/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.test.ts @@ -1,5 +1,5 @@ import { communities, getFactory, Store } from '@quiet/state-manager' -import { Community, CommunityOwnership, CreateNetworkPayload } from '@quiet/types' +import { Community, CommunityOwnership, CreateNetworkPayload, InvitationPair } from '@quiet/types' import { FactoryGirl } from 'factory-girl' import { expectSaga } from 'redux-saga-test-plan' import { handleInvitationCodeSaga } from './handleInvitationCode.saga' @@ -13,7 +13,7 @@ describe('Handle invitation code', () => { let store: Store let factory: FactoryGirl let community: Community - let validInvitationCode: string + let validInvitationPair: InvitationPair[] beforeEach(async () => { store = ( @@ -26,15 +26,20 @@ describe('Handle invitation code', () => { ).store factory = await getFactory(store) - validInvitationCode = 'bb5wacaftixjl3yhq2cp3ls2ife2e5wlwct3hjlb4lyk4iniypmgozyd' + validInvitationPair = [ + { + onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + }, + ] }) it('creates network if code is valid', async () => { const payload: CreateNetworkPayload = { ownership: CommunityOwnership.User, - registrar: validInvitationCode, + peers: validInvitationPair, } - await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCode(validInvitationCode)) + await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCodes(validInvitationPair)) .withState(store.getState()) .put(communities.actions.createNetwork(payload)) .run() @@ -44,10 +49,10 @@ describe('Handle invitation code', () => { community = await factory.create['payload']>('Community') const payload: CreateNetworkPayload = { ownership: CommunityOwnership.User, - registrar: validInvitationCode, + peers: validInvitationPair, } - await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCode(validInvitationCode)) + await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCodes(validInvitationPair)) .withState(store.getState()) .put( modalsActions.openModal({ @@ -63,15 +68,14 @@ describe('Handle invitation code', () => { }) it('does not try to create network if code is invalid', async () => { - const code = 'invalid' const payload: CreateNetworkPayload = { ownership: CommunityOwnership.User, - registrar: code, + peers: [], } - await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCode(code)) + await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCodes([])) .withState(store.getState()) - .put(communities.actions.clearInvitationCode()) + .put(communities.actions.clearInvitationCodes()) .put( modalsActions.openModal({ name: ModalName.warningModal, diff --git a/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.ts b/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.ts index 5fab99fb8d..a43c71a988 100644 --- a/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.ts +++ b/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.ts @@ -3,12 +3,11 @@ import { select, put, delay } from 'typed-redux-saga' import { CommunityOwnership, CreateNetworkPayload } from '@quiet/types' import { communities } from '@quiet/state-manager' import { socketSelectors } from '../socket/socket.selectors' -import { ONION_ADDRESS_REGEX } from '@quiet/common' import { ModalName } from '../modals/modals.types' import { modalsActions } from '../modals/modals.slice' export function* handleInvitationCodeSaga( - action: PayloadAction['payload']> + action: PayloadAction['payload']> ): Generator { while (true) { const connected = yield* select(socketSelectors.isConnected) @@ -32,26 +31,22 @@ export function* handleInvitationCodeSaga( return } - const code = action.payload.trim() - - if (code.match(ONION_ADDRESS_REGEX)) { + if (action.payload.length > 0) { const payload: CreateNetworkPayload = { ownership: CommunityOwnership.User, - registrar: code, + peers: action.payload, } yield* put(communities.actions.createNetwork(payload)) - return + } else { + yield* put(communities.actions.clearInvitationCodes()) + yield* put( + modalsActions.openModal({ + name: ModalName.warningModal, + args: { + title: 'Invalid link', + subtitle: 'The invite link you received is not valid. Please check it and try again.', + }, + }) + ) } - - yield* put(communities.actions.clearInvitationCode()) - - yield* put( - modalsActions.openModal({ - name: ModalName.warningModal, - args: { - title: 'Invalid link', - subtitle: 'The invite link you received is not valid. Please check it and try again.', - }, - }) - ) } diff --git a/packages/desktop/src/rtl-tests/community.create.test.tsx b/packages/desktop/src/rtl-tests/community.create.test.tsx index 7bf3329724..91304eadcc 100644 --- a/packages/desktop/src/rtl-tests/community.create.test.tsx +++ b/packages/desktop/src/rtl-tests/community.create.test.tsx @@ -183,14 +183,12 @@ describe('User', () => { expect(actions).toMatchInlineSnapshot(` Array [ "Communities/createNetwork", - "Communities/clearInvitationCode", "Communities/addNewCommunity", "Communities/setCurrentCommunity", "Modals/closeModal", "Modals/openModal", "Identity/registerUsername", "Communities/responseCreateNetwork", - "Communities/clearInvitationCode", "Communities/updateCommunityData", "Identity/addNewIdentity", "Network/setLoadingPanelType", @@ -201,8 +199,10 @@ describe('User', () => { "Identity/storeUserCertificate", "Identity/savedOwnerCertificate", "Communities/launchRegistrar", + "Identity/saveUserCsr", "Files/checkForMissingFiles", "Network/addInitializedCommunity", + "Communities/clearInvitationCodes", "Identity/saveOwnerCertToDb", "PublicChannels/createGeneralChannel", "PublicChannels/channelsReplicated", diff --git a/packages/desktop/src/rtl-tests/community.join.test.tsx b/packages/desktop/src/rtl-tests/community.join.test.tsx index b23f4f8692..e2f5984c17 100644 --- a/packages/desktop/src/rtl-tests/community.join.test.tsx +++ b/packages/desktop/src/rtl-tests/community.join.test.tsx @@ -26,7 +26,6 @@ import { getFactory, errors, ResponseCreateNetworkPayload, - connection, } from '@quiet/state-manager' import Channel from '../renderer/components/Channel/Channel' import LoadingPanel from '../renderer/components/LoadingPanel/LoadingPanel' @@ -37,14 +36,14 @@ import { ErrorPayload, ResponseLaunchCommunityPayload, SendOwnerCertificatePayload, - SendUserCertificatePayload, } from '@quiet/types' -import { selectOptions } from 'mathjax-full/js/util/Options' jest.setTimeout(20_000) describe('User', () => { let socket: MockedSocket + const validCode = + 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' // trigger beforeEach(() => { socket = new MockedSocket() @@ -167,7 +166,7 @@ describe('User', () => { // Enter community address and hit button const joinCommunityInput = screen.getByPlaceholderText(dictionary.placeholder) const joinCommunityButton = screen.getByText(dictionary.button) - await userEvent.type(joinCommunityInput, '3lyn5yjwwb74he5olv43eej7knt34folvrgrfsw6vzitvkxmc5wpe4yd') + await userEvent.type(joinCommunityInput, validCode) await userEvent.click(joinCommunityButton) // Confirm user is being redirected to username registration @@ -194,28 +193,24 @@ describe('User', () => { expect(actions).toMatchInlineSnapshot(` Array [ "Communities/createNetwork", - "Communities/clearInvitationCode", + "Communities/setInvitationCodes", "Communities/addNewCommunity", "Communities/setCurrentCommunity", "Modals/closeModal", "Modals/openModal", "Identity/registerUsername", "Communities/responseCreateNetwork", - "Communities/clearInvitationCode", "Communities/updateCommunityData", "Identity/addNewIdentity", "Network/setLoadingPanelType", "Modals/openModal", "Identity/registerCertificate", - "Communities/addOwnerCertificate", - "Communities/storePeerList", - "Identity/storeUserCertificate", - "Communities/updateCommunity", - "Communities/updateCommunityData", "Communities/launchCommunity", "Communities/launchRegistrar", + "Identity/saveUserCsr", "Files/checkForMissingFiles", "Network/addInitializedCommunity", + "Communities/clearInvitationCodes", "PublicChannels/channelsReplicated", "PublicChannels/addChannel", "Messages/addPublicChannelsMessagesBase", @@ -227,7 +222,8 @@ describe('User', () => { `) }) - it('sees proper registration error when trying to join with already taken username', async () => { + // We don't display registration errors right now + it.skip('sees proper registration error when trying to join with already taken username', async () => { const { store, runSaga } = await prepareStore( {}, socket // Fork state manager's sagas @@ -292,7 +288,7 @@ describe('User', () => { // Enter community address and hit button const joinCommunityInput = screen.getByPlaceholderText(dictionary.placeholder) const joinCommunityButton = screen.getByText(dictionary.button) - await userEvent.type(joinCommunityInput, '3lyn5yjwwb74he5olv43eej7knt34folvrgrfsw6vzitvkxmc5wpe4yd') + await userEvent.type(joinCommunityInput, validCode) await userEvent.click(joinCommunityButton) // Confirm user is being redirected to username registration @@ -336,7 +332,7 @@ describe('User', () => { `) }) - it('clears error before sending another username registration request', async () => { + it.skip('clears error before sending another username registration request', async () => { const { store, runSaga } = await prepareStore( {}, socket // Fork state manager's sagas @@ -389,7 +385,7 @@ describe('User', () => { // Enter community address and hit button const joinCommunityInput = screen.getByPlaceholderText(dictionary.placeholder) const joinCommunityButton = screen.getByText(dictionary.button) - await userEvent.type(joinCommunityInput, '3lyn5yjwwb74he5olv43eej7knt34folvrgrfsw6vzitvkxmc5wpe4yd') + await userEvent.type(joinCommunityInput, validCode) await userEvent.click(joinCommunityButton) // Confirm user is being redirected to username registration diff --git a/packages/desktop/src/rtl-tests/customProtocol.test.tsx b/packages/desktop/src/rtl-tests/customProtocol.test.tsx index 7e85eef1f7..b0dbcaf697 100644 --- a/packages/desktop/src/rtl-tests/customProtocol.test.tsx +++ b/packages/desktop/src/rtl-tests/customProtocol.test.tsx @@ -10,6 +10,7 @@ import { modalsActions } from '../renderer/sagas/modals/modals.slice' import { ModalName } from '../renderer/sagas/modals/modals.types' import JoinCommunity from '../renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity' import CreateUsername from '../renderer/components/CreateUsername/CreateUsername' +import { InvitationPair } from '@quiet/types' jest.setTimeout(20_000) @@ -64,9 +65,11 @@ describe('Opening app through custom protocol', () => { socket // Fork state manager's sagas ) - const invitationCode = 'bidrmzr3ee6qa2vvrlcnqvvvsk2gmjktcqkunba326parszr44gibwyd' + const invitationCodes: InvitationPair[] = [ + { peerId: 'abcdef', onionAddress: 'bidrmzr3ee6qa2vvrlcnqvvvsk2gmjktcqkunba326parszr44gibwyd' }, + ] - store.dispatch(communities.actions.handleInvitationCode(invitationCode)) + store.dispatch(communities.actions.handleInvitationCodes(invitationCodes)) store.dispatch(modalsActions.openModal({ name: ModalName.joinCommunityModal })) diff --git a/packages/desktop/src/rtl-tests/loadingPanel.test.tsx b/packages/desktop/src/rtl-tests/loadingPanel.test.tsx index dc226a1ff3..57b6b0cf53 100644 --- a/packages/desktop/src/rtl-tests/loadingPanel.test.tsx +++ b/packages/desktop/src/rtl-tests/loadingPanel.test.tsx @@ -133,7 +133,7 @@ describe('Loading panel', () => { expect(screen.queryByTestId('joiningPanelComponent')).toBeNull() }) - it('Do not display Loading panel when community and identity are created but certificate is missing', async () => { + it('Do not display Loading panel when community and identity are created but user csr is missing', async () => { const { store } = await prepareStore( {}, socket // Fork state manager's sagas @@ -149,20 +149,11 @@ describe('Loading panel', () => { await factory.create['payload']>('Identity', { id: community.id, nickname: 'alice', + userCertificate: null, + userCsr: null, }) - const aliceCertificate = store.getState().Identity.identities.entities[community.id]?.userCertificate - - expect(aliceCertificate).not.toBeUndefined() - expect(aliceCertificate).not.toBeNull() - - store.dispatch( - identity.actions.storeUserCertificate({ - communityId: community.id, - // @ts-expect-error - userCertificate: null, - }) - ) + expect(store.getState().Identity.identities.entities[community.id]?.userCsr).toBeNull() renderComponent( <> @@ -176,14 +167,8 @@ describe('Loading panel', () => { expect(screen.queryByTestId('createUsernameModalActions')).not.toBeNull() // Assertions that we don't see Loading Pannel expect(screen.queryByTestId('spinnerLoader')).toBeNull() - // 'Create username' modal should be closed after receiving certificate - store.dispatch( - identity.actions.storeUserCertificate({ - communityId: community.id, - // @ts-expect-error - userCertificate: aliceCertificate, - }) - ) + // 'Create username' modal should be closed after creating csr + store.dispatch(identity.actions.registerUsername('alice')) await waitFor(() => expect(screen.queryByTestId('createUsernameModalActions')).toBeNull()) }) }) diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json index 7a5f655f77..0186a73548 100644 --- a/packages/desktop/tsconfig.json +++ b/packages/desktop/tsconfig.json @@ -10,6 +10,7 @@ "allowJs": true, "lib": [ "ES2020", + "DOM.Iterable", "dom" ], "typeRoots": [ diff --git a/packages/e2e-tests/src/tests/invitationLink.test.ts b/packages/e2e-tests/src/tests/invitationLink.test.ts index 05cce35a94..0b4e917019 100644 --- a/packages/e2e-tests/src/tests/invitationLink.test.ts +++ b/packages/e2e-tests/src/tests/invitationLink.test.ts @@ -9,7 +9,7 @@ import { Sidebar, WarningModal, } from '../selectors' -import { capitalizeFirstLetter, invitationDeepUrl } from '@quiet/common' +import { capitalizeFirstLetter, getInvitationPairs, invitationDeepUrl } from '@quiet/common' import { execSync } from 'child_process' import { type SupportedPlatformDesktop } from '@quiet/types' @@ -132,7 +132,7 @@ describe('New user joins using invitation link while having app opened', () => { it.skip('Guest clicks invitation link with invalid invitation code', async () => { // Fix when modals ordering is fixed (joining modal hiddes warning modal) console.log('opening invalid code') - execSync(`xdg-open ${invitationDeepUrl('invalidcode')}`) + execSync(`xdg-open ${invitationDeepUrl([{ peerId: 'invalid', onionAddress: 'alsoInvalid' }])}`) }) it.skip('Guest sees modal with warning about invalid code, closes it', async () => { @@ -155,7 +155,9 @@ describe('New user joins using invitation link while having app opened', () => { win32: 'start', } - execSync(`${command[process.platform as SupportedPlatformDesktop]} ${invitationDeepUrl(url.hash.substring(1))}`) + const pairs = getInvitationPairs(url.hash.substring(1)) + expect(pairs).not.toBe([]) + execSync(`${command[process.platform as SupportedPlatformDesktop]} ${invitationDeepUrl(pairs)}`) console.log('Guest opened invitation link') }) diff --git a/packages/e2e-tests/src/tests/twoClients.test.ts b/packages/e2e-tests/src/tests/twoClients.test.ts index 60a85cfdad..fc0ced5a10 100644 --- a/packages/e2e-tests/src/tests/twoClients.test.ts +++ b/packages/e2e-tests/src/tests/twoClients.test.ts @@ -152,7 +152,7 @@ describe('Two Clients', () => { await joinCommunityModal.submit() }) - it('RegisterUsernameModal - User tries to register already taken username, sees error', async () => { + it.skip('RegisterUsernameModal - User tries to register already taken username, sees error', async () => { console.log('new user - 4') registerModal2 = new RegisterUsernameModal(guestApp.driver) const isRegisterModal2 = await registerModal2.element.isDisplayed() @@ -165,6 +165,7 @@ describe('Two Clients', () => { it('RegisterUsernameModal - User successfully register not taken username', async () => { console.log('new user - 5') + registerModal2 = new RegisterUsernameModal(guestApp.driver) const isRegisterModal = await registerModal2.element.isDisplayed() expect(isRegisterModal).toBeTruthy() await registerModal2.clearInput() diff --git a/packages/identity/src/index.ts b/packages/identity/src/index.ts index 0e2139b2fb..e319ce40fd 100644 --- a/packages/identity/src/index.ts +++ b/packages/identity/src/index.ts @@ -2,6 +2,7 @@ import { createRootCA, type RootCA } from './generateRootCA' import { extractPubKey, parseCertificate, + parseCertificationRequest, keyFromCertificate, keyObjectFromString, extractPubKeyString, @@ -36,7 +37,14 @@ import { export { createRootCA } export type { RootCA } -export { extractPubKey, parseCertificate, keyFromCertificate, keyObjectFromString, extractPubKeyString } +export { + extractPubKey, + parseCertificate, + keyFromCertificate, + keyObjectFromString, + extractPubKeyString, + parseCertificationRequest, +} export { verifyUserCert } export { verifySignature } export { sign } diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json index afd02831cc..6adb0ef8de 100644 --- a/packages/logger/tsconfig.json +++ b/packages/logger/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "target": "es2017", + "target": "ES2020", "outDir": "./lib", "typeRoots": [ "../@types", diff --git a/packages/mobile/src/components/ContextMenu/menus/InvitationContextMenu.container.tsx b/packages/mobile/src/components/ContextMenu/menus/InvitationContextMenu.container.tsx index f28feff2da..c092fbc895 100644 --- a/packages/mobile/src/components/ContextMenu/menus/InvitationContextMenu.container.tsx +++ b/packages/mobile/src/components/ContextMenu/menus/InvitationContextMenu.container.tsx @@ -21,8 +21,6 @@ export const InvitationContextMenu: FC = () => { const dispatch = useDispatch() const screen = useSelector(navigationSelectors.currentScreen) - - const community = useSelector(communities.selectors.currentCommunity) const invitationLink = useSelector(communities.selectors.invitationUrl) const invitationContextMenu = useContextMenu(MenuName.Invitation) diff --git a/packages/mobile/src/components/JoinCommunity/JoinCommunity.component.tsx b/packages/mobile/src/components/JoinCommunity/JoinCommunity.component.tsx index 1f4b90c4aa..96dbf5f5fe 100644 --- a/packages/mobile/src/components/JoinCommunity/JoinCommunity.component.tsx +++ b/packages/mobile/src/components/JoinCommunity/JoinCommunity.component.tsx @@ -7,7 +7,7 @@ import { Typography } from '../Typography/Typography.component' import { TextWithLink } from '../TextWithLink/TextWithLink.component' import { JoinCommunityProps } from './JoinCommunity.types' -import { getInvitationCode } from '@quiet/state-manager' +import { getInvitationCodes } from '@quiet/state-manager' import { ONION_ADDRESS_REGEX } from '@quiet/common' export const JoinCommunity: FC = ({ @@ -32,16 +32,14 @@ export const JoinCommunity: FC = ({ Keyboard.dismiss() setLoading(true) - let submitValue: string | undefined = joinCommunityInput - - if (submitValue === undefined || submitValue?.length === 0) { + if (joinCommunityInput === undefined || joinCommunityInput?.length === 0) { setLoading(false) setInputError('Community address can not be empty') return } - submitValue = getInvitationCode(submitValue.trim()) - if (!submitValue || !submitValue.match(ONION_ADDRESS_REGEX)) { + const submitValue = getInvitationCodes(joinCommunityInput.trim()) + if (!submitValue?.length) { setLoading(false) setInputError('Please check your invitation code and try again') return diff --git a/packages/mobile/src/components/JoinCommunity/JoinCommunity.types.ts b/packages/mobile/src/components/JoinCommunity/JoinCommunity.types.ts index fb07d60ca6..3086a86790 100644 --- a/packages/mobile/src/components/JoinCommunity/JoinCommunity.types.ts +++ b/packages/mobile/src/components/JoinCommunity/JoinCommunity.types.ts @@ -1,5 +1,7 @@ +import { InvitationPair } from '@quiet/types' + export interface JoinCommunityProps { - joinCommunityAction: (address: string) => void + joinCommunityAction: (address: InvitationPair[]) => void redirectionAction: () => void networkCreated: boolean invitationCode?: string diff --git a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx index 2cf924a853..b73ef07e52 100644 --- a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx +++ b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx @@ -2,7 +2,7 @@ import React, { FC, useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { identity, communities } from '@quiet/state-manager' -import { CommunityOwnership, CreateNetworkPayload } from '@quiet/types' +import { CommunityOwnership, CreateNetworkPayload, InvitationPair } from '@quiet/types' import { JoinCommunity } from '../../components/JoinCommunity/JoinCommunity.component' import { navigationActions } from '../../store/navigation/navigation.slice' import { ScreenNames } from '../../const/ScreenNames.enum' @@ -35,10 +35,10 @@ export const JoinCommunityScreen: FC = ({ route }) => }, [dispatch, community, route.params?.code]) const joinCommunityAction = useCallback( - (address: string) => { + (pairs: InvitationPair[]) => { const payload: CreateNetworkPayload = { ownership: CommunityOwnership.User, - registrar: address, + peers: pairs, } dispatch(communities.actions.createNetwork(payload)) dispatch( 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 a2fb46277e..7b1dcb5766 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts @@ -3,7 +3,7 @@ import { combineReducers } from '@reduxjs/toolkit' import { reducers } from '../../root.reducer' import { Store } from '../../store.types' import { prepareStore } from '../../../tests/utils/prepareStore' -import { communities, Community, connection, identity } from '@quiet/state-manager' +import { communities, Community, connection, getInvitationCodes, identity } from '@quiet/state-manager' import { initActions } from '../init.slice' import { navigationActions } from '../../navigation/navigation.slice' import { ScreenNames } from '../../../const/ScreenNames.enum' @@ -14,7 +14,9 @@ describe('deepLinkSaga', () => { let store: Store const id = '00d045ab' - + const validCode = + 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' + const validPairs = getInvitationCodes(validCode) const community: Community = { id, name: '', @@ -28,7 +30,6 @@ describe('deepLinkSaga', () => { privateKey: '', address: '', }, - registrarUrl: 'https://bidrmzr3ee6qa2vvrlcnqvvvsk2gmjktcqkunba326parszr44gibwyd.onion', onionAddress: '', privateKey: '', port: 0, @@ -54,31 +55,28 @@ describe('deepLinkSaga', () => { dataPort: 5001, }) ) - - const code = 'bidrmzr3ee6qa2vvrlcnqvvvsk2gmjktcqkunba326parszr44gibwyd' - const reducer = combineReducers(reducers) - await expectSaga(deepLinkSaga, initActions.deepLink(code)) + await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) .withReducer(reducer) .withState(store.getState()) .put( navigationActions.replaceScreen({ screen: ScreenNames.JoinCommunityScreen, params: { - code, + code: validCode, }, }) ) .put( communities.actions.createNetwork({ ownership: CommunityOwnership.User, - registrar: code, + peers: validPairs, }) ) .run() }) - test('opens channel list screen if the same url has been used', async () => { + test.skip('opens channel list screen if the same url has been used', async () => { store.dispatch( initActions.setWebsocketConnected({ dataPort: 5001, @@ -94,10 +92,8 @@ describe('deepLinkSaga', () => { store.dispatch(communities.actions.setCurrentCommunity(community.id)) - const code = 'bidrmzr3ee6qa2vvrlcnqvvvsk2gmjktcqkunba326parszr44gibwyd' - const reducer = combineReducers(reducers) - await expectSaga(deepLinkSaga, initActions.deepLink(code)) + await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) .withReducer(reducer) .withState(store.getState()) .put( @@ -108,7 +104,7 @@ describe('deepLinkSaga', () => { .not.put( communities.actions.createNetwork({ ownership: CommunityOwnership.User, - registrar: code, + peers: validPairs, }) ) .run() @@ -124,60 +120,22 @@ describe('deepLinkSaga', () => { store.dispatch(communities.actions.addNewCommunity(community)) store.dispatch(communities.actions.setCurrentCommunity(community.id)) - - const code = 'ctbebt3ixybtu4ty2dr3ychjtxpkhuun4neuavkjjhplgzfde5vgelad' - const reducer = combineReducers(reducers) - await expectSaga(deepLinkSaga, initActions.deepLink(code)) + await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) .withReducer(reducer) .withState(store.getState()) .not.put( communities.actions.createNetwork({ ownership: CommunityOwnership.User, - registrar: code, + peers: validPairs, }) ) .run() }) - test('continues if link used mid registration', async () => { - store.dispatch( - initActions.setWebsocketConnected({ - dataPort: 5001, - }) - ) - - store.dispatch(communities.actions.addNewCommunity(community)) - - store.dispatch( - // @ts-expect-error - identity.actions.addNewIdentity({ ..._identity, userCertificate: null }) - ) - - store.dispatch(communities.actions.setCurrentCommunity(community.id)) - - const code = 'bidrmzr3ee6qa2vvrlcnqvvvsk2gmjktcqkunba326parszr44gibwyd' + test.todo('continues if link used mid registration') - const reducer = combineReducers(reducers) - await expectSaga(deepLinkSaga, initActions.deepLink(code)) - .withReducer(reducer) - .withState(store.getState()) - .put( - navigationActions.replaceScreen({ - screen: ScreenNames.UsernameRegistrationScreen, - params: undefined, - }) - ) - .not.put( - communities.actions.createNetwork({ - ownership: CommunityOwnership.User, - registrar: code, - }) - ) - .run() - }) - - test('continues if link used mid registration and locks input while waiting for server response', async () => { + test.skip('continues if link used mid registration and locks input while waiting for server response', async () => { store.dispatch( initActions.setWebsocketConnected({ dataPort: 5001, @@ -195,10 +153,8 @@ describe('deepLinkSaga', () => { store.dispatch(connection.actions.setTorConnectionProcess(ConnectionProcessInfo.REGISTERING_USER_CERTIFICATE)) - const code = 'bidrmzr3ee6qa2vvrlcnqvvvsk2gmjktcqkunba326parszr44gibwyd' - const reducer = combineReducers(reducers) - await expectSaga(deepLinkSaga, initActions.deepLink(code)) + await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) .withReducer(reducer) .withState(store.getState()) .put( @@ -210,7 +166,7 @@ describe('deepLinkSaga', () => { .not.put( communities.actions.createNetwork({ ownership: CommunityOwnership.User, - registrar: code, + peers: validPairs, }) ) .run() diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts index a999979007..5a0dd0f2ab 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts @@ -1,6 +1,6 @@ import { PayloadAction } from '@reduxjs/toolkit' import { select, delay, put } from 'typed-redux-saga' -import { communities, connection, identity } from '@quiet/state-manager' +import { communities, connection, getInvitationCodes, identity } from '@quiet/state-manager' import { ScreenNames } from '../../../const/ScreenNames.enum' import { navigationActions } from '../../navigation/navigation.slice' import { initSelectors } from '../init.selectors' @@ -9,6 +9,7 @@ import { appImages } from '../../../assets' import { replaceScreen } from '../../../RootNavigation' import { UsernameRegistrationRouteProps } from '../../../route.params' import { CommunityOwnership, ConnectionProcessInfo, CreateNetworkPayload } from '@quiet/types' +import { retrieveInvitationCode } from '@quiet/common' export function* deepLinkSaga(action: PayloadAction['payload']>): Generator { const code = action.payload @@ -22,40 +23,9 @@ export function* deepLinkSaga(action: PayloadAction { expect(invitationUrl).toEqual('') }) - it('returns proper invitation url if registrationUrl is in old format', async () => { - const code = 'aznu6kiyutsgjhdue4i4xushjzey6boxf4i4isd53admsibvbt6qyiyd' - const registrarUrl = `http://${code}` - const { store } = prepareStore() - const factory = await getFactory(store) - await factory.create['payload']>('Community', { - registrarUrl, - port: 0, - onionAddress: '', - }) - const invitationUrl = communitiesSelectors.invitationUrl(store.getState()) - expect(invitationUrl).toEqual(invitationShareUrl(code)) - }) - - it('returns proper invitation url if registrationUrl is just onion address', async () => { - const code = 'aznu6kiyutsgjhdue4i4xushjzey6boxf4i4isd53admsibvbt6qyiyd' - const { store } = prepareStore() - const factory = await getFactory(store) - await factory.create['payload']>('Community', { - registrarUrl: code, - port: 0, - onionAddress: '', - }) - const invitationUrl = communitiesSelectors.invitationUrl(store.getState()) - expect(invitationUrl).toEqual(invitationShareUrl(code)) - }) - it('returns proper ownerNickname - ownerCertificate exist', async () => { const { store } = prepareStore() expect(identity.userCertificate).not.toBeUndefined() diff --git a/packages/state-manager/src/sagas/communities/communities.selectors.ts b/packages/state-manager/src/sagas/communities/communities.selectors.ts index 4a1d44c232..7c97185122 100644 --- a/packages/state-manager/src/sagas/communities/communities.selectors.ts +++ b/packages/state-manager/src/sagas/communities/communities.selectors.ts @@ -57,16 +57,15 @@ export const invitationCode = createSelector(communitiesSlice, reducerState => { return reducerState.invitationCode }) +export const invitationCodes = createSelector(communitiesSlice, reducerState => { + return reducerState.invitationCodes +}) + export const invitationUrl = createSelector(currentCommunity, community => { - if (!community?.registrarUrl) return '' - let registrarUrl = '' - try { - const url = new URL(community.registrarUrl) - registrarUrl = url.hostname.split('.')[0] - } catch (e) { - registrarUrl = community.registrarUrl - } - return invitationShareUrl(registrarUrl) + const peerList = community?.peerList + if (!peerList || peerList?.length === 0) return '' + const initialPeers = peerList.slice(0, 4) + return invitationShareUrl(initialPeers) }) export const registrationAttempts = (communityId: string) => @@ -108,6 +107,7 @@ export const communitiesSelectors = { currentCommunityId, registrarUrl, registrationAttempts, + invitationCodes, invitationCode, invitationUrl, ownerNickname, diff --git a/packages/state-manager/src/sagas/communities/communities.slice.ts b/packages/state-manager/src/sagas/communities/communities.slice.ts index 41072e01d6..da7ed55693 100644 --- a/packages/state-manager/src/sagas/communities/communities.slice.ts +++ b/packages/state-manager/src/sagas/communities/communities.slice.ts @@ -2,6 +2,7 @@ import { createSlice, type EntityState, type PayloadAction } from '@reduxjs/tool import { StoreKeys } from '../store.keys' import { communitiesAdapter } from './communities.adapter' import { + InvitationPair, type AddOwnerCertificatePayload, type Community as CommunityType, type CreateNetworkPayload, @@ -10,10 +11,12 @@ import { type StorePeerListPayload, type UpdateCommunityPayload, type UpdateRegistrationAttemptsPayload, + CommunityMetadataPayload, } from '@quiet/types' export class CommunitiesState { public invitationCode: string | undefined = undefined + public invitationCodes: InvitationPair[] = [] public currentCommunity = '' public communities: EntityState = communitiesAdapter.getInitialState() } @@ -89,11 +92,14 @@ export const communitiesSlice = createSlice({ }, }) }, - handleInvitationCode: (state, action: PayloadAction) => { - state.invitationCode = action.payload + handleInvitationCodes: (state, action: PayloadAction) => { + state.invitationCodes = action.payload }, - clearInvitationCode: state => { - state.invitationCode = undefined + setInvitationCodes: (state, action: PayloadAction) => { + state.invitationCodes = action.payload + }, + clearInvitationCodes: state => { + state.invitationCodes = [] }, addOwnerCertificate: (state, action: PayloadAction) => { const { communityId, ownerCertificate } = action.payload @@ -104,6 +110,7 @@ export const communitiesSlice = createSlice({ }, }) }, + saveCommunityMetadata: (state, _action: PayloadAction) => state, }, }) diff --git a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.test.ts b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.test.ts index 551daff58e..6e8bf52fe0 100644 --- a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.test.ts +++ b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.test.ts @@ -28,7 +28,7 @@ describe('createNetwork', () => { createNetworkSaga, communitiesActions.createNetwork({ ownership: CommunityOwnership.User, - registrar: 'registrarUrl', + peers: [{ peerId: 'peerId', onionAddress: 'address' }], }) ) .withReducer(reducer) 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 028efd328e..eeeabb3fec 100644 --- a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts +++ b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts @@ -28,18 +28,18 @@ export function* createNetworkSaga( } const id = yield* call(generateId) - - const registrarUrl = action.payload.registrar ? `http://${action.payload.registrar}.onion` : undefined - const payload: Community = { id, name: action.payload.name, - registrarUrl, CA, rootCa: CA?.rootCertString, } - yield* put(communitiesActions.clearInvitationCode()) + const invitationPeers = action.payload.peers + if (invitationPeers) { + yield* put(communitiesActions.setInvitationCodes(invitationPeers)) + } + yield* put(communitiesActions.addNewCommunity(payload)) yield* put(communitiesActions.setCurrentCommunity(id)) } 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 020a6f8459..253624c057 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 @@ -118,7 +118,6 @@ describe('launchCommunity', () => { id: launchCommunityPayload.id, peerId: launchCommunityPayload.peerId, hiddenService: launchCommunityPayload.hiddenService, - certs: launchCommunityPayload.certs, peers: launchCommunityPayload.peers, }, ]) @@ -182,7 +181,6 @@ describe('launchCommunity', () => { id: launchCommunityPayload.id, peerId: launchCommunityPayload.peerId, hiddenService: launchCommunityPayload.hiddenService, - certs: launchCommunityPayload.certs, peers: launchCommunityPayload.peers, }, ]) 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 4640049c20..b441f62767 100644 --- a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts +++ b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts @@ -8,6 +8,7 @@ import { connectionActions } from '../../appConnection/connection.slice' 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' export function* initCommunities(): Generator { @@ -34,24 +35,24 @@ export function* launchCommunitySaga( communityId = yield* select(communitiesSelectors.currentCommunityId) } - const community = yield* select(communitiesSelectors.selectById(communityId)) const identity = yield* select(identitySelectors.selectById(communityId)) - if (!identity?.userCertificate || !identity.userCsr?.userKey || !community?.rootCa) { - console.error('Could not launch community, Community or Identity is lacking data') + if (!identity?.userCsr?.userKey) { + console.error('Could not launch community, No identity private key') return } - const peerList = yield* select(connectionSelectors.peerList) + const invitationCodes = yield* select(communitiesSelectors.invitationCodes) + let peerList: string[] = [] + if (invitationCodes) { + peerList = pairsToP2pAddresses(invitationCodes) + } else { + peerList = yield* select(connectionSelectors.peerList) + } const payload: InitCommunityPayload = { id: identity.id, peerId: identity.peerId, hiddenService: identity.hiddenService, - certs: { - certificate: identity.userCertificate, - key: identity.userCsr.userKey, - CA: [community.rootCa], - }, peers: peerList, } diff --git a/packages/state-manager/src/sagas/communities/responseCreateNetwork/responseCreateNetwork.saga.test.ts b/packages/state-manager/src/sagas/communities/responseCreateNetwork/responseCreateNetwork.saga.test.ts index 31f71fa16e..10ad9f8ce3 100644 --- a/packages/state-manager/src/sagas/communities/responseCreateNetwork/responseCreateNetwork.saga.test.ts +++ b/packages/state-manager/src/sagas/communities/responseCreateNetwork/responseCreateNetwork.saga.test.ts @@ -61,7 +61,6 @@ describe('responseCreateNetwork', () => { .withState(store.getState()) .provide([[call.fn(generateDmKeyPair), dmKeys]]) .call(generateDmKeyPair) - .put(communitiesActions.clearInvitationCode()) .put(communitiesActions.updateCommunityData(community)) .put(identityActions.addNewIdentity(identity)) .run() diff --git a/packages/state-manager/src/sagas/communities/responseCreateNetwork/responseCreateNetwork.saga.ts b/packages/state-manager/src/sagas/communities/responseCreateNetwork/responseCreateNetwork.saga.ts index 224e210418..4cefc0e43a 100644 --- a/packages/state-manager/src/sagas/communities/responseCreateNetwork/responseCreateNetwork.saga.ts +++ b/packages/state-manager/src/sagas/communities/responseCreateNetwork/responseCreateNetwork.saga.ts @@ -22,7 +22,6 @@ export function* responseCreateNetworkSaga(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, + }) + ) + yield* put( + communitiesActions.addOwnerCertificate({ + communityId: communityId, + ownerCertificate: action.payload.ownerCertificate, + }) + ) +} 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 3546f4b03e..777b7161db 100644 --- a/packages/state-manager/src/sagas/identity/identity.master.saga.ts +++ b/packages/state-manager/src/sagas/identity/identity.master.saga.ts @@ -4,8 +4,11 @@ import { identityActions } from './identity.slice' import { registerCertificateSaga } from './registerCertificate/registerCertificate.saga' import { saveOwnerCertToDbSaga } from './saveOwnerCertToDb/saveOwnerCertToDb.saga' import { registerUsernameSaga } from './registerUsername/registerUsername.saga' -import { savedOwnerCertificateSaga } from './savedOwnerCertificate/savedOwnerCertificate.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' export function* identityMasterSaga(socket: Socket): Generator { yield all([ @@ -14,5 +17,7 @@ export function* identityMasterSaga(socket: Socket): Generator { takeEvery(identityActions.saveOwnerCertToDb.type, saveOwnerCertToDbSaga, socket), takeEvery(identityActions.savedOwnerCertificate.type, savedOwnerCertificateSaga, socket), takeEvery(identityActions.verifyJoinTimestamp.type, verifyJoinTimestampSaga), + takeEvery(identityActions.saveUserCsr.type, saveUserCsrSaga, socket), + takeEvery(usersActions.responseSendCertificates.type, updateCertificateSaga), ]) } diff --git a/packages/state-manager/src/sagas/identity/identity.selectors.ts b/packages/state-manager/src/sagas/identity/identity.selectors.ts index 1891d9c54d..42e154d575 100644 --- a/packages/state-manager/src/sagas/identity/identity.selectors.ts +++ b/packages/state-manager/src/sagas/identity/identity.selectors.ts @@ -33,6 +33,10 @@ export const joinedCommunities = createSelector(selectCommunities, selectEntitie export const joinTimestamp = createSelector(currentIdentity, identity => identity?.joinTimestamp) +export const csr = createSelector(communitiesSelectors.currentCommunityId, selectEntities, (id, identities) => { + return identities[id]?.userCsr +}) + export const identitySelectors = { selectById, selectEntities, @@ -40,4 +44,5 @@ export const identitySelectors = { communityMembership, joinedCommunities, joinTimestamp, + csr, } diff --git a/packages/state-manager/src/sagas/identity/identity.slice.ts b/packages/state-manager/src/sagas/identity/identity.slice.ts index 6911d6c04c..445ac34449 100644 --- a/packages/state-manager/src/sagas/identity/identity.slice.ts +++ b/packages/state-manager/src/sagas/identity/identity.slice.ts @@ -42,6 +42,7 @@ export const identitySlice = createSlice({ }, }) }, + saveUserCsr: state => state, verifyJoinTimestamp: state => state, updateJoinTimestamp: (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 9e91149243..81e94d2372 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 @@ -5,7 +5,7 @@ import { prepareStore } from '../../../utils/tests/prepareStore' import { getFactory } from '../../../utils/tests/factories' import { combineReducers } from '@reduxjs/toolkit' import { reducers } from '../../reducers' -import { type communitiesActions } from '../../communities/communities.slice' +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' @@ -51,7 +51,7 @@ describe('registerCertificateSaga', () => { .run() }) - it('request certificate registration when user is not community owner', async () => { + it('launch community when user is not community owner', async () => { setupCrypto() const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket @@ -102,14 +102,7 @@ describe('registerCertificateSaga', () => { .withReducer(reducer) .withState(store.getState()) .not.apply(socket, socket.emit, [SocketActionTypes.REGISTER_OWNER_CERTIFICATE]) - .apply(socket, socket.emit, [ - SocketActionTypes.REGISTER_USER_CERTIFICATE, - { - communityId: community.id, - userCsr: identity.userCsr.userCsr, - serviceAddress: community.registrarUrl, - }, - ]) + .put(communitiesActions.launchCommunity(community.id)) .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 362af87d95..ade168914d 100644 --- a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts +++ b/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts @@ -1,6 +1,6 @@ import { applyEmitParams, type Socket } from '../../../types' import { type PayloadAction } from '@reduxjs/toolkit' -import { apply, select } from 'typed-redux-saga' +import { apply, select, put } from 'typed-redux-saga' import { communitiesSelectors } from '../../communities/communities.selectors' import { type identityActions } from '../identity.slice' import { @@ -8,6 +8,7 @@ import { type RegisterUserCertificatePayload, SocketActionTypes, } from '@quiet/types' +import { communitiesActions } from '../../communities/communities.slice' export function* registerCertificateSaga( socket: Socket, @@ -31,16 +32,6 @@ export function* registerCertificateSaga( yield* apply(socket, socket.emit, applyEmitParams(SocketActionTypes.REGISTER_OWNER_CERTIFICATE, payload)) } else { - if (!currentCommunity.registrarUrl) { - console.error('Could not register certificate, no registrar url') - return - } - const payload: RegisterUserCertificatePayload = { - communityId: action.payload.communityId, - userCsr: action.payload.userCsr.userCsr, - serviceAddress: currentCommunity.registrarUrl, - } - - yield* apply(socket, socket.emit, applyEmitParams(SocketActionTypes.REGISTER_USER_CERTIFICATE, payload)) + yield* put(communitiesActions.launchCommunity(action.payload.communityId)) } } 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 19b48ba03c..52642d02d2 100644 --- a/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts +++ b/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts @@ -7,7 +7,6 @@ import { config } from '../../users/const/certFieldTypes' import { Socket, applyEmitParams } from '../../../types' import { communitiesSelectors } from '../../communities/communities.selectors' import { CreateUserCsrPayload, RegisterCertificatePayload, SocketActionTypes, Community } from '@quiet/types' -import { connectionSelectors } from '../../appConnection/connection.selectors' export function* registerUsernameSaga(socket: Socket, action: PayloadAction): Generator { // Nickname can differ between saga calls @@ -60,7 +59,6 @@ export function* registerUsernameSaga(socket: Socket, action: PayloadAction): Generator { + const certificate = yield* select(identitySelectors.communityMembership) + const communityId = yield* select(communitiesSelectors.currentCommunityId) + + if (certificate) return + + const csr = yield* select(identitySelectors.csr) + + if (!csr?.userCsr) return + + const parsedCsr = yield* call(loadCSR, csr?.userCsr) + const pubKey = getReqFieldValue(parsedCsr, CertFieldsTypes.dmPublicKey) + + const cert = action.payload.certificates.find(cert => { + const parsedCert = parseCertificate(cert) + const certPubKey = getCertFieldValue(parsedCert, CertFieldsTypes.dmPublicKey) + if (certPubKey === pubKey) return cert + }) + + if (cert) { + yield* put( + identityActions.storeUserCertificate({ + userCertificate: cert, + communityId, + }) + ) + } +} 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 e1c3c0e1d1..b212976452 100644 --- a/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts +++ b/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts @@ -15,7 +15,10 @@ export function* sendMessageSaga( action: PayloadAction['payload']> ): Generator { const identity = yield* select(identitySelectors.currentIdentity) - if (!identity?.userCsr || !identity.userCertificate) return + if (!identity?.userCsr || !identity?.userCertificate) { + console.info('No user CSR or user certificate') + return + } const certificate = identity.userCertificate 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 44ee61d282..a8cd03748e 100644 --- a/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts +++ b/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts @@ -42,6 +42,8 @@ import { type SavedOwnerCertificatePayload, SendUserCertificatePayload, type SendOwnerCertificatePayload, + SaveCSRPayload, + CommunityMetadata, } from '@quiet/types' const log = logger('socket') @@ -86,7 +88,10 @@ export function subscribe(socket: Socket) { | ReturnType | ReturnType | ReturnType + | ReturnType + | ReturnType | ReturnType + | ReturnType >(emit => { // UPDATE FOR APP socket.on(SocketActionTypes.TOR_INITIALIZED, () => { @@ -158,10 +163,12 @@ export function subscribe(socket: Socket) { // Community socket.on(SocketActionTypes.NEW_COMMUNITY, (_payload: ResponseCreateCommunityPayload) => { + console.log('on SocketActionTypes.NEW_COMMUNITY') emit(identityActions.saveOwnerCertToDb()) emit(publicChannelsActions.createGeneralChannel()) }) socket.on(SocketActionTypes.REGISTRAR, (payload: ResponseRegistrarPayload) => { + console.log('SocketActionTypes.REGISTRAR') log(SocketActionTypes.REGISTRAR, payload) emit(communitiesActions.responseRegistrar(payload)) emit(networkActions.addInitializedRegistrar(payload.id)) @@ -176,8 +183,10 @@ export function subscribe(socket: Socket) { socket.on(SocketActionTypes.COMMUNITY, (payload: ResponseLaunchCommunityPayload) => { console.log('Hunting for heisenbug: Community event received in state-manager') emit(communitiesActions.launchRegistrar(payload.id)) + emit(identityActions.saveUserCsr()) emit(filesActions.checkForMissingFiles(payload.id)) emit(networkActions.addInitializedCommunity(payload.id)) + emit(communitiesActions.clearInvitationCodes()) }) // Errors socket.on(SocketActionTypes.ERROR, (payload: ErrorPayload) => { @@ -194,9 +203,8 @@ export function subscribe(socket: Socket) { ) emit(usersActions.responseSendCertificates(payload)) }) - socket.on(SocketActionTypes.SEND_USER_CERTIFICATE, (payload: SendOwnerCertificatePayload) => { - console.log('user cert with owner cert', payload) + console.log('Received SEND_USER_CERTIFICATE', payload.communityId) emit( communitiesActions.addOwnerCertificate({ @@ -226,6 +234,7 @@ export function subscribe(socket: Socket) { emit(communitiesActions.launchCommunity(payload.communityId)) }) socket.on(SocketActionTypes.SAVED_OWNER_CERTIFICATE, (payload: SavedOwnerCertificatePayload) => { + console.log('Received SAVED_OWNER_CERTIFICATE', payload.communityId) emit( communitiesActions.addOwnerCertificate({ communityId: payload.communityId, @@ -246,6 +255,15 @@ export function subscribe(socket: Socket) { ) emit(identityActions.savedOwnerCertificate(payload.communityId)) }) + socket.on(SocketActionTypes.SAVE_COMMUNITY_METADATA, (payload: CommunityMetadata) => { + console.log('SAVE COMMUNITY METADATA', payload) + emit( + communitiesActions.saveCommunityMetadata({ + rootCa: payload.rootCa, + ownerCertificate: payload.ownerCertificate, + }) + ) + }) return () => undefined }) } diff --git a/packages/state-manager/src/sagas/users/users.slice.ts b/packages/state-manager/src/sagas/users/users.slice.ts index c5d1e9b8bf..d70706e37c 100644 --- a/packages/state-manager/src/sagas/users/users.slice.ts +++ b/packages/state-manager/src/sagas/users/users.slice.ts @@ -6,6 +6,7 @@ import { type SendCertificatesResponse } from '@quiet/types' export class UsersState { public certificates: EntityState = certificatesAdapter.getInitialState() + public csrs: EntityState = certificatesAdapter.getInitialState() } export const usersSlice = createSlice({ diff --git a/packages/state-manager/src/types.ts b/packages/state-manager/src/types.ts index abd95a819b..2a71e13bc4 100644 --- a/packages/state-manager/src/types.ts +++ b/packages/state-manager/src/types.ts @@ -3,6 +3,7 @@ import { type DefaultEventsMap } from 'socket.io-client/build/typed-events' import { type messagesActions } from './sagas/messages/messages.slice' import { type publicChannelsActions } from './sagas/publicChannels/publicChannels.slice' import { + SaveCSRPayload, type CancelDownloadPayload, type Community, type DeleteFilesFromChannelSocketPayload, @@ -37,6 +38,7 @@ export interface EmitEvents { [SocketActionTypes.CLOSE]: () => void [SocketActionTypes.LEAVE_COMMUNITY]: () => void [SocketActionTypes.CREATE_NETWORK]: EmitEvent + [SocketActionTypes.SAVE_USER_CSR]: EmitEvent } export type Socket = IOSocket diff --git a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts index 31c99bf1e5..1d3860c69f 100644 --- a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts +++ b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts @@ -1,14 +1,30 @@ -import { getInvitationCode } from './invitationCode' -import { Site } from '@quiet/common' +import { getInvitationCodes } from './invitationCode' +import { QUIET_JOIN_PAGE } from '@quiet/common' describe('Invitation code helper', () => { + const peerId1 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA' + const address1 = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' + const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' + const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' + it('retrieves invitation code if url is a proper share url', () => { - const result = getInvitationCode(`https://${Site.DOMAIN}/${Site.JOIN_PAGE}#validCode`) - expect(result).toEqual('validCode') + const result = getInvitationCodes(`${QUIET_JOIN_PAGE}#${peerId1}=${address1}&${peerId2}=${address2}`) + expect(result).toEqual([ + { peerId: peerId1, onionAddress: address1 }, + { peerId: peerId2, onionAddress: address2 }, + ]) + }) + + it('returns empty list if code is not a proper share url nor a code', () => { + const result = getInvitationCodes('invalidCode') + expect(result).toEqual([]) }) - it('returns passed value if url is not a proper share url', () => { - const result = getInvitationCode('validCode') - expect(result).toEqual('validCode') + it('retrieves invitation code if url is a proper code', () => { + const result = getInvitationCodes(`${peerId1}=${address1}&${peerId2}=${address2}`) + expect(result).toEqual([ + { peerId: peerId1, onionAddress: address1 }, + { peerId: peerId2, onionAddress: address2 }, + ]) }) }) diff --git a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts index e2abc314cb..a3f5aff0a2 100644 --- a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts +++ b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts @@ -1,37 +1,36 @@ -import { Site } from '@quiet/common' +import { Site, getInvitationPairs, invitationCodeValid } from '@quiet/common' +import { InvitationPair } from '@quiet/types' -export const getInvitationCode = (codeOrUrl: string): string => { +export const getInvitationCodes = (codeOrUrl: string): InvitationPair[] => { /** - * Extract code from invitation share url or return passed value for further validation + * Extract codes from invitation share url or return passed value for further validation */ - let code = '' + let codes: InvitationPair[] = [] + let potentialCode let validUrl: URL | null = null try { validUrl = new URL(codeOrUrl) } catch (e) { - code = codeOrUrl + // It may be just code, not URL + potentialCode = codeOrUrl } + if (validUrl && validUrl.host === Site.DOMAIN && validUrl.pathname.includes(Site.JOIN_PAGE)) { const hash = validUrl.hash - - let invitationCode: string = hash.substring(1) - - // Ensure backward compatibility - if (hash.includes('code=')) { - // Mix of old and new link - invitationCode = hash.substring(6) - } else if (validUrl.searchParams.has('code')) { - // Old link - invitationCode = validUrl.searchParams.get('code') || '' + if (hash) { + // Parse hash + const pairs = hash.substring(1) + codes = getInvitationPairs(pairs) } - - code = invitationCode + } else if (potentialCode) { + // Parse code just as hash value + codes = getInvitationPairs(potentialCode) } - if (!code) { - console.warn(`No invitation code. Code/url passed: ${codeOrUrl}`) + if (codes.length === 0) { + console.warn(`No invitation codes. Code/url passed: ${codeOrUrl}`) } - return code + return codes } diff --git a/packages/types/src/community.ts b/packages/types/src/community.ts index ee7c0467a2..a38096d4fb 100644 --- a/packages/types/src/community.ts +++ b/packages/types/src/community.ts @@ -1,4 +1,5 @@ import { type HiddenService, type PeerId, type Identity } from './identity' +import { InvitationPair } from './network' export interface Community { id: string @@ -34,7 +35,7 @@ export interface NetworkData { export interface CreateNetworkPayload { ownership: CommunityOwnership name?: string - registrar?: string + peers?: InvitationPair[] } export interface ResponseCreateNetworkPayload { @@ -52,7 +53,7 @@ export interface InitCommunityPayload { id: string peerId: PeerId hiddenService: HiddenService - certs: Certificates + certs?: Certificates peers?: string[] } @@ -102,3 +103,14 @@ export interface AddOwnerCertificatePayload { communityId: string ownerCertificate: string } + +export interface CommunityMetadata { + id: string + rootCa: string + ownerCertificate: string +} + +export interface CommunityMetadataPayload { + rootCa: string + ownerCertificate: string +} diff --git a/packages/types/src/connection.ts b/packages/types/src/connection.ts index 11085d1088..e342c2e2b4 100644 --- a/packages/types/src/connection.ts +++ b/packages/types/src/connection.ts @@ -18,6 +18,7 @@ export interface NetworkStats { export enum ConnectionProcessInfo { CONNECTING_TO_COMMUNITY = 'Connecting to community owner via Tor', REGISTERING_USER_CERTIFICATE = 'Registering user certificate', + SAVING_USER_CSR = 'Saving user csr', REGISTERING_OWNER_CERTIFICATE = 'Registering owner certificate', LAUNCHING_COMMUNITY = 'Launching community', SPAWNING_HIDDEN_SERVICE = 'Spawning hidden service for community', diff --git a/packages/types/src/identity.ts b/packages/types/src/identity.ts index 175e8d7c56..09ff51adab 100644 --- a/packages/types/src/identity.ts +++ b/packages/types/src/identity.ts @@ -60,7 +60,6 @@ export interface RegisterCertificatePayload { export interface RegisterUserCertificatePayload { communityId: string userCsr: string - serviceAddress: string } export interface PermsData { @@ -79,6 +78,11 @@ export interface SaveCertificatePayload { rootPermsData: PermsData } +export interface SaveCSRPayload { + // communityId: string + csr: string +} + export interface SaveOwnerCertificatePayload { id: string peerId: string diff --git a/packages/types/src/network.ts b/packages/types/src/network.ts index 7795ea5b5f..c92e0c5fa1 100644 --- a/packages/types/src/network.ts +++ b/packages/types/src/network.ts @@ -2,3 +2,8 @@ export enum LoadingPanelType { StartingApplication = 'Starting Quiet', Joining = 'Connecting to peers', } + +export type InvitationPair = { + peerId: string + onionAddress: string +} diff --git a/packages/types/src/socket.ts b/packages/types/src/socket.ts index bbbfc64cbb..548394ede6 100644 --- a/packages/types/src/socket.ts +++ b/packages/types/src/socket.ts @@ -53,11 +53,15 @@ export enum SocketActionTypes { // S SAVE_OWNER_CERTIFICATE = 'saveOwnerCertificate', SAVED_OWNER_CERTIFICATE = 'savedOwnerCertificate', + SAVE_USER_CSR = 'saveUserCsr', + SAVE_COMMUNITY_METADATA = 'saveCommunityMetadata', + SAVED_USER_CSR = 'savedUserCsr', SEND_DIRECT_MESSAGE = 'sendDirectMessage', SEND_MESSAGE = 'sendMessage', SEND_MESSAGES_IDS = 'sendIds', SEND_PEER_ID = 'sendPeerId', SEND_USER_CERTIFICATE = 'sendUserCertificate', + SEND_COMMUNITY_METADATA = 'sendCommunityMetadata', SUBSCRIBE_FOR_ALL_CONVERSATIONS = 'subscribeToAllConversations', SUBSCRIBE_FOR_DIRECT_MESSAGE_THREAD = 'subscribeToDirectMessageThread', // T diff --git a/tsconfig.build.json b/tsconfig.build.json index cbe3d6e834..533d6c77ee 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,7 +1,7 @@ { "compilerOptions": { "baseUrl": ".", - "target": "es6", + "target": "ES2020", "module": "commonjs", "declaration": true, "esModuleInterop": true, @@ -13,7 +13,8 @@ "./node_modules/@types" ], "lib": [ - "es6", + "ES2020", + "DOM.Iterable", "dom" ] } diff --git a/tsconfig.json b/tsconfig.json index 5116cd30c1..8eafb17145 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "ES2020", "baseUrl": ".", "paths": { "@quiet/identity": ["packages/identity/src/index.ts"], @@ -18,8 +18,9 @@ "experimentalDecorators": true, "moduleResolution": "node", "lib": [ - "es6", - "dom" + "dom", + "DOM.Iterable", + "ES2020" ] } }