From 3281ac9d372caa9e8b2edbc94b00b466416807a4 Mon Sep 17 00:00:00 2001 From: Mo Date: Mon, 24 Jul 2023 07:46:20 -0500 Subject: [PATCH] refactor: break up vault services (#2364) --- .../Contacts/UseCase/HandleKeyPairChange.ts | 2 +- .../SharedVaults/SharedVaultService.spec.ts | 34 +- .../Domain/SharedVaults/SharedVaultService.ts | 478 ++++-------------- .../SharedVaultServiceInterface.ts | 33 +- .../UseCase/NotifyVaultUsersOfKeyRotation.ts | 4 +- .../UseCase/SendVaultDataChangedMessage.ts | 2 +- .../UseCase/SendVaultKeyChangedMessage.ts | 2 +- .../UseCase/ShareContactWithVault.ts | 2 +- .../UseCase/UpdateSharedVaultInvite.ts | 31 -- .../InviteRecord.ts} | 2 +- .../UseCase/AcceptVaultInvite.ts | 0 .../UseCase/InviteToVault.ts | 2 +- .../UseCase/ReuploadAllInvites.ts | 0 .../UseCase/ReuploadInvite.ts | 2 +- .../UseCase/ReuploadVaultInvites.ts | 0 .../UseCase/SendVaultInvite.ts | 0 .../Domain/VaultInvite/VaultInviteService.ts | 274 ++++++++++ .../VaultInvite/VaultInviteServiceEvent.ts | 4 + .../VaultInviteServiceInterface.ts | 22 + .../UseCase/GetVaultContacts.ts | 0 .../UseCase/GetVaultUsers.ts | 0 .../Domain/VaultUser/UseCase/IsVaultAdmin.ts | 12 + .../UseCase/LeaveSharedVault.ts | 2 +- .../UseCase/RemoveSharedVaultMember.ts | 11 +- .../src/Domain/VaultUser/VaultUserService.ts | 103 ++++ .../Domain/VaultUser/VaultUserServiceEvent.ts | 3 + .../VaultUser/VaultUserServiceInterface.ts | 13 + packages/services/src/Domain/index.ts | 40 +- packages/snjs/lib/Application/Application.ts | 38 +- .../Application/Dependencies/Dependencies.ts | 52 +- .../lib/Application/Dependencies/Types.ts | 3 + packages/snjs/mocha/lib/AppContext.js | 20 +- packages/snjs/mocha/lib/Collaboration.js | 10 +- packages/snjs/mocha/vaults/conflicts.test.js | 4 +- packages/snjs/mocha/vaults/deletion.test.js | 13 +- packages/snjs/mocha/vaults/files.test.js | 2 +- packages/snjs/mocha/vaults/invites.test.js | 43 +- packages/snjs/mocha/vaults/items.test.js | 9 +- .../snjs/mocha/vaults/key_rotation.test.js | 29 +- .../snjs/mocha/vaults/permissions.test.js | 12 +- .../snjs/mocha/vaults/shared_vaults.test.js | 5 +- .../PasswordWizard/PreprocessingStep.tsx | 6 +- .../Vaults/Contacts/EditContactModal.tsx | 4 +- .../Vaults/Invites/ContactInviteModal.tsx | 8 +- .../Panes/Vaults/Invites/InviteItem.tsx | 8 +- .../Preferences/Panes/Vaults/Vaults.tsx | 28 +- .../Panes/Vaults/Vaults/VaultItem.tsx | 6 +- .../Vaults/VaultModal/EditVaultModal.tsx | 8 +- .../Vaults/VaultModal/VaultModalInvites.tsx | 4 +- .../Vaults/VaultModal/VaultModalMembers.tsx | 6 +- 50 files changed, 763 insertions(+), 633 deletions(-) delete mode 100644 packages/services/src/Domain/SharedVaults/UseCase/UpdateSharedVaultInvite.ts rename packages/services/src/Domain/{SharedVaults/PendingSharedVaultInviteRecord.ts => VaultInvite/InviteRecord.ts} (84%) rename packages/services/src/Domain/{SharedVaults => VaultInvite}/UseCase/AcceptVaultInvite.ts (100%) rename packages/services/src/Domain/{SharedVaults => VaultInvite}/UseCase/InviteToVault.ts (98%) rename packages/services/src/Domain/{SharedVaults => VaultInvite}/UseCase/ReuploadAllInvites.ts (100%) rename packages/services/src/Domain/{SharedVaults => VaultInvite}/UseCase/ReuploadInvite.ts (95%) rename packages/services/src/Domain/{SharedVaults => VaultInvite}/UseCase/ReuploadVaultInvites.ts (100%) rename packages/services/src/Domain/{SharedVaults => VaultInvite}/UseCase/SendVaultInvite.ts (100%) create mode 100644 packages/services/src/Domain/VaultInvite/VaultInviteService.ts create mode 100644 packages/services/src/Domain/VaultInvite/VaultInviteServiceEvent.ts create mode 100644 packages/services/src/Domain/VaultInvite/VaultInviteServiceInterface.ts rename packages/services/src/Domain/{SharedVaults => VaultUser}/UseCase/GetVaultContacts.ts (100%) rename packages/services/src/Domain/{SharedVaults => VaultUser}/UseCase/GetVaultUsers.ts (100%) create mode 100644 packages/services/src/Domain/VaultUser/UseCase/IsVaultAdmin.ts rename packages/services/src/Domain/{SharedVaults => VaultUser}/UseCase/LeaveSharedVault.ts (93%) rename packages/services/src/Domain/{SharedVaults => VaultUser}/UseCase/RemoveSharedVaultMember.ts (54%) create mode 100644 packages/services/src/Domain/VaultUser/VaultUserService.ts create mode 100644 packages/services/src/Domain/VaultUser/VaultUserServiceEvent.ts create mode 100644 packages/services/src/Domain/VaultUser/VaultUserServiceInterface.ts diff --git a/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.ts b/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.ts index d8abcb24a..531968c02 100644 --- a/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.ts +++ b/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.ts @@ -1,6 +1,6 @@ import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { PkcKeyPair } from '@standardnotes/sncrypto-common' -import { ReuploadAllInvites } from '../../SharedVaults/UseCase/ReuploadAllInvites' +import { ReuploadAllInvites } from '../../VaultInvite/UseCase/ReuploadAllInvites' import { ResendAllMessages } from '../../AsymmetricMessage/UseCase/ResendAllMessages' export class HandleKeyPairChange implements UseCaseInterface { diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts b/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts index a14ee4764..4d7c021f3 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts @@ -1,24 +1,15 @@ +import { IsVaultAdmin } from './../VaultUser/UseCase/IsVaultAdmin' import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface' -import { GetVaultUsers } from './UseCase/GetVaultUsers' -import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember' import { DeleteSharedVault } from './UseCase/DeleteSharedVault' import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault' import { ShareContactWithVault } from './UseCase/ShareContactWithVault' import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault' -import { LeaveVault } from './UseCase/LeaveSharedVault' -import { InviteToVault } from './UseCase/InviteToVault' -import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite' -import { GetVaultContacts } from './UseCase/GetVaultContacts' -import { GetAllContacts } from './../Contacts/UseCase/GetAllContacts' import { FindContact } from './../Contacts/UseCase/FindContact' -import { GetUntrustedPayload } from './../AsymmetricMessage/UseCase/GetUntrustedPayload' -import { GetTrustedPayload } from './../AsymmetricMessage/UseCase/GetTrustedPayload' import { SendVaultDataChangedMessage } from './UseCase/SendVaultDataChangedMessage' import { NotifyVaultUsersOfKeyRotation } from './UseCase/NotifyVaultUsersOfKeyRotation' import { HandleKeyPairChange } from './../Contacts/UseCase/HandleKeyPairChange' import { CreateSharedVault } from './UseCase/CreateSharedVault' import { GetVault } from './../Vaults/UseCase/GetVault' -import { SharedVaultInvitesServer } from '@standardnotes/api' import { SharedVaultService } from './SharedVaultService' import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface' @@ -40,56 +31,37 @@ describe('SharedVaultService', () => { const encryption = {} as jest.Mocked const session = {} as jest.Mocked const vaults = {} as jest.Mocked - const invitesServer = {} as jest.Mocked const getVault = {} as jest.Mocked const createSharedVaultUseCase = {} as jest.Mocked const handleKeyPairChange = {} as jest.Mocked const notifyVaultUsersOfKeyRotation = {} as jest.Mocked const sendVaultDataChangeMessage = {} as jest.Mocked - const getTrustedPayload = {} as jest.Mocked - const getUntrustedPayload = {} as jest.Mocked const findContact = {} as jest.Mocked - const getAllContacts = {} as jest.Mocked - const getVaultContacts = {} as jest.Mocked - const acceptVaultInvite = {} as jest.Mocked - const inviteToVault = {} as jest.Mocked - const leaveVault = {} as jest.Mocked const deleteThirdPartyVault = {} as jest.Mocked const shareContactWithVault = {} as jest.Mocked const convertToSharedVault = {} as jest.Mocked const deleteSharedVaultUseCase = {} as jest.Mocked - const removeVaultMember = {} as jest.Mocked - const getSharedVaultUsersUseCase = {} as jest.Mocked + const isVaultAdmin = {} as jest.Mocked const eventBus = {} as jest.Mocked eventBus.addEventHandler = jest.fn() service = new SharedVaultService( - sync, items, encryption, session, vaults, - invitesServer, getVault, createSharedVaultUseCase, handleKeyPairChange, notifyVaultUsersOfKeyRotation, sendVaultDataChangeMessage, - getTrustedPayload, - getUntrustedPayload, findContact, - getAllContacts, - getVaultContacts, - acceptVaultInvite, - inviteToVault, - leaveVault, deleteThirdPartyVault, shareContactWithVault, convertToSharedVault, deleteSharedVaultUseCase, - removeVaultMember, - getSharedVaultUsersUseCase, + isVaultAdmin, eventBus, ) }) diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts index 6e66ca935..40b530f0f 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts @@ -1,114 +1,70 @@ import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData' -import { InviteToVault } from './UseCase/InviteToVault' -import { - ClientDisplayableError, - SharedVaultInviteServerHash, - isErrorResponse, - SharedVaultUserServerHash, - isClientDisplayableError, - SharedVaultPermission, - UserEventType, -} from '@standardnotes/responses' -import { SharedVaultInvitesServer } from '@standardnotes/api' +import { ClientDisplayableError, UserEventType } from '@standardnotes/responses' import { DecryptedItemInterface, PayloadEmitSource, TrustedContactInterface, SharedVaultListingInterface, VaultListingInterface, - AsymmetricMessageSharedVaultInvite, KeySystemRootKeyStorageMode, } from '@standardnotes/models' import { SharedVaultServiceInterface } from './SharedVaultServiceInterface' import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent' -import { GetVaultUsers } from './UseCase/GetVaultUsers' -import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember' import { AbstractService } from '../Service/AbstractService' import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' -import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' -import { SyncEvent, SyncEventReceivedSharedVaultInvitesData } from '../Event/SyncEvent' +import { SyncEvent } from '../Event/SyncEvent' import { SessionEvent } from '../Session/SessionEvent' import { InternalEventInterface } from '../Internal/InternalEventInterface' -import { LeaveVault } from './UseCase/LeaveSharedVault' import { VaultServiceInterface } from '../Vaults/VaultServiceInterface' import { UserEventServiceEvent, UserEventServiceEventPayload } from '../UserEvent/UserEventServiceEvent' import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault' import { DeleteSharedVault } from './UseCase/DeleteSharedVault' import { VaultServiceEvent, VaultServiceEventPayload } from '../Vaults/VaultServiceEvent' -import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite' -import { GetTrustedPayload } from '../AsymmetricMessage/UseCase/GetTrustedPayload' -import { PendingSharedVaultInviteRecord } from './PendingSharedVaultInviteRecord' -import { GetUntrustedPayload } from '../AsymmetricMessage/UseCase/GetUntrustedPayload' import { ShareContactWithVault } from './UseCase/ShareContactWithVault' -import { GetVaultContacts } from './UseCase/GetVaultContacts' import { NotifyVaultUsersOfKeyRotation } from './UseCase/NotifyVaultUsersOfKeyRotation' import { CreateSharedVault } from './UseCase/CreateSharedVault' import { SendVaultDataChangedMessage } from './UseCase/SendVaultDataChangedMessage' import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault' import { GetVault } from '../Vaults/UseCase/GetVault' -import { ContentType, Result } from '@standardnotes/domain-core' +import { ContentType } from '@standardnotes/domain-core' import { HandleKeyPairChange } from '../Contacts/UseCase/HandleKeyPairChange' import { FindContact } from '../Contacts/UseCase/FindContact' -import { GetAllContacts } from '../Contacts/UseCase/GetAllContacts' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' +import { IsVaultAdmin } from '../VaultUser/UseCase/IsVaultAdmin' export class SharedVaultService extends AbstractService implements SharedVaultServiceInterface, InternalEventHandlerInterface { - private pendingInvites: Record = {} - constructor( - private sync: SyncServiceInterface, private items: ItemManagerInterface, private encryption: EncryptionProviderInterface, private session: SessionsClientInterface, private vaults: VaultServiceInterface, - private invitesServer: SharedVaultInvitesServer, - private getVault: GetVault, - private createSharedVaultUseCase: CreateSharedVault, - private handleKeyPairChange: HandleKeyPairChange, - private notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation, - private sendVaultDataChangeMessage: SendVaultDataChangedMessage, - private getTrustedPayload: GetTrustedPayload, - private getUntrustedPayload: GetUntrustedPayload, - private findContact: FindContact, - private getAllContacts: GetAllContacts, - private getVaultContacts: GetVaultContacts, - private acceptVaultInvite: AcceptVaultInvite, - private inviteToVault: InviteToVault, - private leaveVault: LeaveVault, - private deleteThirdPartyVault: DeleteThirdPartyVault, - private shareContactWithVault: ShareContactWithVault, - private convertToSharedVault: ConvertToSharedVault, - private deleteSharedVaultUseCase: DeleteSharedVault, - private removeVaultMember: RemoveVaultMember, - private getSharedVaultUsersUseCase: GetVaultUsers, + private _getVault: GetVault, + private _createSharedVaultUseCase: CreateSharedVault, + private _handleKeyPairChange: HandleKeyPairChange, + private _notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation, + private _sendVaultDataChangeMessage: SendVaultDataChangedMessage, + private _findContact: FindContact, + private _deleteThirdPartyVault: DeleteThirdPartyVault, + private _shareContactWithVault: ShareContactWithVault, + private _convertToSharedVault: ConvertToSharedVault, + private _deleteSharedVaultUseCase: DeleteSharedVault, + private _isVaultAdmin: IsVaultAdmin, eventBus: InternalEventBusInterface, ) { super(eventBus) - eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged) - eventBus.addEventHandler(this, UserEventServiceEvent.UserEventReceived) - eventBus.addEventHandler(this, VaultServiceEvent.VaultRootKeyRotated) - this.eventDisposers.push( - items.addObserver( - ContentType.TYPES.TrustedContact, - async ({ changed, inserted, source }) => { - await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange() - - if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) { - void this.handleCreationOfNewTrustedContacts(inserted) - } - if (source === PayloadEmitSource.LocalChanged && changed.length > 0) { - void this.handleTrustedContactsChange(changed) - } - }, - ), + items.addObserver(ContentType.TYPES.TrustedContact, async ({ changed, source }) => { + if (source === PayloadEmitSource.LocalChanged && changed.length > 0) { + void this.handleTrustedContactsChange(changed) + } + }), ) this.eventDisposers.push( @@ -120,54 +76,88 @@ export class SharedVaultService ) } + override deinit(): void { + super.deinit() + ;(this.items as unknown) = undefined + ;(this.encryption as unknown) = undefined + ;(this.session as unknown) = undefined + ;(this.vaults as unknown) = undefined + ;(this._getVault as unknown) = undefined + ;(this._createSharedVaultUseCase as unknown) = undefined + ;(this._handleKeyPairChange as unknown) = undefined + ;(this._notifyVaultUsersOfKeyRotation as unknown) = undefined + ;(this._sendVaultDataChangeMessage as unknown) = undefined + ;(this._findContact as unknown) = undefined + ;(this._deleteThirdPartyVault as unknown) = undefined + ;(this._shareContactWithVault as unknown) = undefined + ;(this._convertToSharedVault as unknown) = undefined + ;(this._deleteSharedVaultUseCase as unknown) = undefined + ;(this._isVaultAdmin as unknown) = undefined + } + async handleEvent(event: InternalEventInterface): Promise { - if (event.type === SessionEvent.UserKeyPairChanged) { - void this.invitesServer.deleteAllInboundInvites() - - const eventData = event.payload as UserKeyPairChangedEventData - - void this.handleKeyPairChange.execute({ - newKeys: eventData.current, - previousKeys: eventData.previous, - }) - } else if (event.type === UserEventServiceEvent.UserEventReceived) { - await this.handleUserEvent(event.payload as UserEventServiceEventPayload) - } else if (event.type === VaultServiceEvent.VaultRootKeyRotated) { - const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated] - await this.handleVaultRootKeyRotatedEvent(payload.vault) - } else if (event.type === SyncEvent.ReceivedSharedVaultInvites) { - await this.processInboundInvites(event.payload as SyncEventReceivedSharedVaultInvitesData) - } else if (event.type === SyncEvent.ReceivedRemoteSharedVaults) { - void this.notifyCollaborationStatusChanged() + switch (event.type) { + case SessionEvent.UserKeyPairChanged: { + const eventData = event.payload as UserKeyPairChangedEventData + void this._handleKeyPairChange.execute({ + newKeys: eventData.current, + previousKeys: eventData.previous, + }) + break + } + case UserEventServiceEvent.UserEventReceived: + await this.handleUserEvent(event.payload as UserEventServiceEventPayload) + break + case VaultServiceEvent.VaultRootKeyRotated: { + const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated] + await this.handleVaultRootKeyRotatedEvent(payload.vault) + break + } + case SyncEvent.ReceivedRemoteSharedVaults: + void this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged) + break } } private async handleUserEvent(event: UserEventServiceEventPayload): Promise { - if (event.eventPayload.eventType === UserEventType.RemovedFromSharedVault) { - const vault = this.getVault.execute({ - sharedVaultUuid: event.eventPayload.sharedVaultUuid, - }) - if (!vault.isFailed()) { - await this.deleteThirdPartyVault.execute(vault.getValue()) + switch (event.eventPayload.eventType) { + case UserEventType.RemovedFromSharedVault: { + const vault = this._getVault.execute({ + sharedVaultUuid: event.eventPayload.sharedVaultUuid, + }) + if (!vault.isFailed()) { + await this._deleteThirdPartyVault.execute(vault.getValue()) + } + break } - } else if (event.eventPayload.eventType === UserEventType.SharedVaultItemRemoved) { - const item = this.items.findItem(event.eventPayload.itemUuid) - if (item) { - this.items.removeItemsLocally([item]) + case UserEventType.SharedVaultItemRemoved: { + const item = this.items.findItem(event.eventPayload.itemUuid) + if (item) { + this.items.removeItemsLocally([item]) + } + break } } } + private isCurrentUserVaultOwner(sharedVault: SharedVaultListingInterface): boolean { + if (!sharedVault.sharing.ownerUserUuid) { + throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`) + } + + return sharedVault.sharing.ownerUserUuid === this.session.userUuid + } + private async handleVaultRootKeyRotatedEvent(vault: VaultListingInterface): Promise { if (!vault.isSharedVaultListing()) { return } - if (!this.isCurrentUserSharedVaultOwner(vault)) { + if (!this.isCurrentUserVaultOwner(vault)) { return } - await this.notifyVaultUsersOfKeyRotation.execute({ + await this._notifyVaultUsersOfKeyRotation.execute({ sharedVault: vault, senderUuid: this.session.getSureUser().uuid, keys: { @@ -183,7 +173,7 @@ export class SharedVaultService userInputtedPassword: string | undefined storagePreference?: KeySystemRootKeyStorageMode }): Promise { - return this.createSharedVaultUseCase.execute({ + return this._createSharedVaultUseCase.execute({ vaultName: dto.name, vaultDescription: dto.description, userInputtedPassword: dto.userInputtedPassword, @@ -194,11 +184,7 @@ export class SharedVaultService async convertVaultToSharedVault( vault: VaultListingInterface, ): Promise { - return this.convertToSharedVault.execute({ vault }) - } - - public getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[] { - return Object.values(this.pendingInvites) + return this._convertToSharedVault.execute({ vault }) } private getAllSharedVaults(): SharedVaultListingInterface[] { @@ -206,38 +192,6 @@ export class SharedVaultService return vaults as SharedVaultListingInterface[] } - private findSharedVault(sharedVaultUuid: string): SharedVaultListingInterface | undefined { - const result = this.getVault.execute({ sharedVaultUuid }) - if (result.isFailed()) { - return undefined - } - - return result.getValue() - } - - public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean { - if (!sharedVault.sharing.ownerUserUuid) { - throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`) - } - return sharedVault.sharing.ownerUserUuid === this.session.userUuid - } - - public isCurrentUserSharedVaultOwner(sharedVault: SharedVaultListingInterface): boolean { - if (!sharedVault.sharing.ownerUserUuid) { - throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`) - } - return sharedVault.sharing.ownerUserUuid === this.session.userUuid - } - - public isSharedVaultUserSharedVaultOwner(user: SharedVaultUserServerHash): boolean { - const vault = this.findSharedVault(user.shared_vault_uuid) - return vault != undefined && vault.sharing.ownerUserUuid === user.user_uuid - } - - private async handleCreationOfNewTrustedContacts(_contacts: TrustedContactInterface[]): Promise { - await this.downloadInboundInvites() - } - private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise { for (const contact of contacts) { if (contact.isMe) { @@ -254,7 +208,7 @@ export class SharedVaultService continue } - await this.sendVaultDataChangeMessage.execute({ + await this._sendVaultDataChangeMessage.execute({ vault, senderUuid: this.session.getSureUser().uuid, keys: { @@ -265,220 +219,8 @@ export class SharedVaultService } } - public async downloadInboundInvites(): Promise { - const response = await this.invitesServer.getInboundUserInvites() - - if (isErrorResponse(response)) { - return ClientDisplayableError.FromString(`Failed to get inbound user invites ${response}`) - } - - this.pendingInvites = {} - - await this.processInboundInvites(response.data.invites) - - return response.data.invites - } - - public async getOutboundInvites( - sharedVault?: SharedVaultListingInterface, - ): Promise { - const response = await this.invitesServer.getOutboundUserInvites() - - if (isErrorResponse(response)) { - return ClientDisplayableError.FromString(`Failed to get outbound user invites ${response}`) - } - - if (sharedVault) { - return response.data.invites.filter((invite) => invite.shared_vault_uuid === sharedVault.sharing.sharedVaultUuid) - } - - return response.data.invites - } - - public async deleteInvite(invite: SharedVaultInviteServerHash): Promise { - const response = await this.invitesServer.deleteInvite({ - sharedVaultUuid: invite.shared_vault_uuid, - inviteUuid: invite.uuid, - }) - - if (isErrorResponse(response)) { - return ClientDisplayableError.FromString(`Failed to delete invite ${response}`) - } - - delete this.pendingInvites[invite.uuid] - } - public async deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise { - return this.deleteSharedVaultUseCase.execute({ sharedVault }) - } - - private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise { - const cachedInvites = this.getCachedPendingInviteRecords().map((record) => record.invite) - - await this.processInboundInvites(cachedInvites) - } - - private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise { - if (invites.length === 0) { - return - } - - for (const invite of invites) { - const sender = this.findContact.execute({ userUuid: invite.sender_uuid }) - if (!sender.isFailed()) { - const trustedMessage = this.getTrustedPayload.execute({ - message: invite, - privateKey: this.encryption.getKeyPair().privateKey, - sender: sender.getValue(), - }) - - if (!trustedMessage.isFailed()) { - this.pendingInvites[invite.uuid] = { - invite, - message: trustedMessage.getValue(), - trusted: true, - } - - continue - } - } - - const untrustedMessage = this.getUntrustedPayload.execute({ - message: invite, - privateKey: this.encryption.getKeyPair().privateKey, - }) - - if (!untrustedMessage.isFailed()) { - this.pendingInvites[invite.uuid] = { - invite, - message: untrustedMessage.getValue(), - trusted: false, - } - } - } - - await this.notifyCollaborationStatusChanged() - } - - private async notifyCollaborationStatusChanged(): Promise { - await this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged) - } - - async acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise { - if (!pendingInvite.trusted) { - throw new Error('Cannot accept untrusted invite') - } - - await this.acceptVaultInvite.execute({ invite: pendingInvite.invite, message: pendingInvite.message }) - - delete this.pendingInvites[pendingInvite.invite.uuid] - - void this.sync.sync() - - await this.decryptErroredItemsAfterInviteAccept() - - await this.sync.syncSharedVaultsFromScratch([pendingInvite.invite.shared_vault_uuid]) - } - - private async decryptErroredItemsAfterInviteAccept(): Promise { - await this.encryption.decryptErroredPayloads() - } - - public async getInvitableContactsForSharedVault( - sharedVault: SharedVaultListingInterface, - ): Promise { - const users = await this.getSharedVaultUsers(sharedVault) - if (!users) { - return [] - } - - const contacts = this.getAllContacts.execute() - if (contacts.isFailed()) { - return [] - } - return contacts.getValue().filter((contact) => { - const isContactAlreadyInVault = users.some((user) => user.user_uuid === contact.contactUuid) - return !isContactAlreadyInVault - }) - } - - private async getSharedVaultContacts(sharedVault: SharedVaultListingInterface): Promise { - const contacts = await this.getVaultContacts.execute(sharedVault.sharing.sharedVaultUuid) - if (contacts.isFailed()) { - return [] - } - - return contacts.getValue() - } - - async inviteContactToSharedVault( - sharedVault: SharedVaultListingInterface, - contact: TrustedContactInterface, - permissions: SharedVaultPermission, - ): Promise> { - const sharedVaultContacts = await this.getSharedVaultContacts(sharedVault) - - const result = await this.inviteToVault.execute({ - keys: { - encryption: this.encryption.getKeyPair(), - signing: this.encryption.getSigningKeyPair(), - }, - senderUuid: this.session.getSureUser().uuid, - sharedVault, - recipient: contact, - sharedVaultContacts, - permissions, - }) - - void this.notifyCollaborationStatusChanged() - - await this.sync.sync() - - return result - } - - async removeUserFromSharedVault( - sharedVault: SharedVaultListingInterface, - userUuid: string, - ): Promise { - if (!this.isCurrentUserSharedVaultAdmin(sharedVault)) { - throw new Error('Only vault admins can remove users') - } - - if (this.vaults.isVaultLocked(sharedVault)) { - throw new Error('Cannot remove user from locked vault') - } - - const result = await this.removeVaultMember.execute({ - sharedVaultUuid: sharedVault.sharing.sharedVaultUuid, - userUuid, - }) - if (isClientDisplayableError(result)) { - return result - } - - void this.notifyCollaborationStatusChanged() - - await this.vaults.rotateVaultRootKey(sharedVault) - } - - async leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise { - const result = await this.leaveVault.execute({ - sharedVault: sharedVault, - userUuid: this.session.getSureUser().uuid, - }) - - if (isClientDisplayableError(result)) { - return result - } - - void this.notifyCollaborationStatusChanged() - } - - async getSharedVaultUsers( - sharedVault: SharedVaultListingInterface, - ): Promise { - return this.getSharedVaultUsersUseCase.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid }) + return this._deleteSharedVaultUseCase.execute({ sharedVault }) } async shareContactWithVaults(contact: TrustedContactInterface): Promise { @@ -486,10 +228,17 @@ export class SharedVaultService throw new Error('Cannot share self contact') } - const ownedVaults = this.getAllSharedVaults().filter(this.isCurrentUserSharedVaultAdmin.bind(this)) + const ownedVaults = this.getAllSharedVaults().filter((vault) => { + return this._isVaultAdmin + .execute({ + sharedVault: vault, + userUuid: this.session.userUuid, + }) + .getValue() + }) for (const vault of ownedVaults) { - await this.shareContactWithVault.execute({ + await this._shareContactWithVault.execute({ keys: { encryption: this.encryption.getKeyPair(), signing: this.encryption.getSigningKeyPair(), @@ -506,7 +255,7 @@ export class SharedVaultService return undefined } - const contact = this.findContact.execute({ userUuid: item.last_edited_by_uuid }) + const contact = this._findContact.execute({ userUuid: item.last_edited_by_uuid }) return contact.isFailed() ? undefined : contact.getValue() } @@ -516,37 +265,8 @@ export class SharedVaultService return undefined } - const contact = this.findContact.execute({ userUuid: item.user_uuid }) + const contact = this._findContact.execute({ userUuid: item.user_uuid }) return contact.isFailed() ? undefined : contact.getValue() } - - override deinit(): void { - super.deinit() - ;(this.sync as unknown) = undefined - ;(this.items as unknown) = undefined - ;(this.encryption as unknown) = undefined - ;(this.session as unknown) = undefined - ;(this.vaults as unknown) = undefined - ;(this.invitesServer as unknown) = undefined - ;(this.getVault as unknown) = undefined - ;(this.createSharedVaultUseCase as unknown) = undefined - ;(this.handleKeyPairChange as unknown) = undefined - ;(this.notifyVaultUsersOfKeyRotation as unknown) = undefined - ;(this.sendVaultDataChangeMessage as unknown) = undefined - ;(this.getTrustedPayload as unknown) = undefined - ;(this.getUntrustedPayload as unknown) = undefined - ;(this.findContact as unknown) = undefined - ;(this.getAllContacts as unknown) = undefined - ;(this.getVaultContacts as unknown) = undefined - ;(this.acceptVaultInvite as unknown) = undefined - ;(this.inviteToVault as unknown) = undefined - ;(this.leaveVault as unknown) = undefined - ;(this.deleteThirdPartyVault as unknown) = undefined - ;(this.shareContactWithVault as unknown) = undefined - ;(this.convertToSharedVault as unknown) = undefined - ;(this.deleteSharedVaultUseCase as unknown) = undefined - ;(this.removeVaultMember as unknown) = undefined - ;(this.getSharedVaultUsersUseCase as unknown) = undefined - } } diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultServiceInterface.ts b/packages/services/src/Domain/SharedVaults/SharedVaultServiceInterface.ts index fd40fe38b..2127698cd 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultServiceInterface.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultServiceInterface.ts @@ -1,9 +1,4 @@ -import { - ClientDisplayableError, - SharedVaultInviteServerHash, - SharedVaultUserServerHash, - SharedVaultPermission, -} from '@standardnotes/responses' +import { ClientDisplayableError } from '@standardnotes/responses' import { DecryptedItemInterface, TrustedContactInterface, @@ -13,8 +8,6 @@ import { } from '@standardnotes/models' import { AbstractService } from '../Service/AbstractService' import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent' -import { PendingSharedVaultInviteRecord } from './PendingSharedVaultInviteRecord' -import { Result } from '@standardnotes/domain-core' export interface SharedVaultServiceInterface extends AbstractService { @@ -25,32 +18,8 @@ export interface SharedVaultServiceInterface storagePreference?: KeySystemRootKeyStorageMode }): Promise deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise - convertVaultToSharedVault(vault: VaultListingInterface): Promise - inviteContactToSharedVault( - sharedVault: SharedVaultListingInterface, - contact: TrustedContactInterface, - permissions: SharedVaultPermission, - ): Promise> - removeUserFromSharedVault( - sharedVault: SharedVaultListingInterface, - userUuid: string, - ): Promise - leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise - getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise - isSharedVaultUserSharedVaultOwner(user: SharedVaultUserServerHash): boolean - isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean - getItemLastEditedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined getItemSharedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined - - downloadInboundInvites(): Promise - getOutboundInvites( - sharedVault?: SharedVaultListingInterface, - ): Promise - acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise - getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[] - getInvitableContactsForSharedVault(sharedVault: SharedVaultListingInterface): Promise - deleteInvite(invite: SharedVaultInviteServerHash): Promise } diff --git a/packages/services/src/Domain/SharedVaults/UseCase/NotifyVaultUsersOfKeyRotation.ts b/packages/services/src/Domain/SharedVaults/UseCase/NotifyVaultUsersOfKeyRotation.ts index 31bc919cc..970a3b28c 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/NotifyVaultUsersOfKeyRotation.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/NotifyVaultUsersOfKeyRotation.ts @@ -4,8 +4,8 @@ import { SharedVaultInviteServerHash, isErrorResponse } from '@standardnotes/res import { SendVaultKeyChangedMessage } from './SendVaultKeyChangedMessage' import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { Result, UseCaseInterface } from '@standardnotes/domain-core' -import { InviteToVault } from './InviteToVault' -import { GetVaultContacts } from './GetVaultContacts' +import { InviteToVault } from '../../VaultInvite/UseCase/InviteToVault' +import { GetVaultContacts } from '../../VaultUser/UseCase/GetVaultContacts' import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage' import { FindContact } from '../../Contacts/UseCase/FindContact' diff --git a/packages/services/src/Domain/SharedVaults/UseCase/SendVaultDataChangedMessage.ts b/packages/services/src/Domain/SharedVaults/UseCase/SendVaultDataChangedMessage.ts index 552fd55a9..5a80dc850 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/SendVaultDataChangedMessage.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/SendVaultDataChangedMessage.ts @@ -5,7 +5,7 @@ import { TrustedContactInterface, } from '@standardnotes/models' import { AsymmetricMessageServerHash } from '@standardnotes/responses' -import { GetVaultUsers } from './GetVaultUsers' +import { GetVaultUsers } from '../../VaultUser/UseCase/GetVaultUsers' import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage' import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage' diff --git a/packages/services/src/Domain/SharedVaults/UseCase/SendVaultKeyChangedMessage.ts b/packages/services/src/Domain/SharedVaults/UseCase/SendVaultKeyChangedMessage.ts index fe8769a30..1af0edc38 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/SendVaultKeyChangedMessage.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/SendVaultKeyChangedMessage.ts @@ -5,7 +5,7 @@ import { TrustedContactInterface, } from '@standardnotes/models' import { AsymmetricMessageServerHash } from '@standardnotes/responses' -import { GetVaultUsers } from './GetVaultUsers' +import { GetVaultUsers } from '../../VaultUser/UseCase/GetVaultUsers' import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage' import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage' diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ShareContactWithVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/ShareContactWithVault.ts index 74580d876..30a69e8ef 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/ShareContactWithVault.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/ShareContactWithVault.ts @@ -8,7 +8,7 @@ import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage' import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage' import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { FindContact } from '../../Contacts/UseCase/FindContact' -import { GetVaultUsers } from './GetVaultUsers' +import { GetVaultUsers } from '../../VaultUser/UseCase/GetVaultUsers' export class ShareContactWithVault implements UseCaseInterface { constructor( diff --git a/packages/services/src/Domain/SharedVaults/UseCase/UpdateSharedVaultInvite.ts b/packages/services/src/Domain/SharedVaults/UseCase/UpdateSharedVaultInvite.ts deleted file mode 100644 index 901ce68e4..000000000 --- a/packages/services/src/Domain/SharedVaults/UseCase/UpdateSharedVaultInvite.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - ClientDisplayableError, - SharedVaultInviteServerHash, - isErrorResponse, - SharedVaultPermission, -} from '@standardnotes/responses' -import { SharedVaultInvitesServerInterface } from '@standardnotes/api' - -export class UpdateSharedVaultInviteUseCase { - constructor(private vaultInvitesServer: SharedVaultInvitesServerInterface) {} - - async execute(params: { - sharedVaultUuid: string - inviteUuid: string - encryptedMessage: string - permissions: SharedVaultPermission - }): Promise { - const response = await this.vaultInvitesServer.updateInvite({ - sharedVaultUuid: params.sharedVaultUuid, - inviteUuid: params.inviteUuid, - encryptedMessage: params.encryptedMessage, - permissions: params.permissions, - }) - - if (isErrorResponse(response)) { - return ClientDisplayableError.FromNetworkError(response) - } - - return response.data.invite - } -} diff --git a/packages/services/src/Domain/SharedVaults/PendingSharedVaultInviteRecord.ts b/packages/services/src/Domain/VaultInvite/InviteRecord.ts similarity index 84% rename from packages/services/src/Domain/SharedVaults/PendingSharedVaultInviteRecord.ts rename to packages/services/src/Domain/VaultInvite/InviteRecord.ts index f2260571e..25bb68b6b 100644 --- a/packages/services/src/Domain/SharedVaults/PendingSharedVaultInviteRecord.ts +++ b/packages/services/src/Domain/VaultInvite/InviteRecord.ts @@ -1,7 +1,7 @@ import { AsymmetricMessageSharedVaultInvite } from '@standardnotes/models' import { SharedVaultInviteServerHash } from '@standardnotes/responses' -export type PendingSharedVaultInviteRecord = { +export type InviteRecord = { invite: SharedVaultInviteServerHash message: AsymmetricMessageSharedVaultInvite trusted: boolean diff --git a/packages/services/src/Domain/SharedVaults/UseCase/AcceptVaultInvite.ts b/packages/services/src/Domain/VaultInvite/UseCase/AcceptVaultInvite.ts similarity index 100% rename from packages/services/src/Domain/SharedVaults/UseCase/AcceptVaultInvite.ts rename to packages/services/src/Domain/VaultInvite/UseCase/AcceptVaultInvite.ts diff --git a/packages/services/src/Domain/SharedVaults/UseCase/InviteToVault.ts b/packages/services/src/Domain/VaultInvite/UseCase/InviteToVault.ts similarity index 98% rename from packages/services/src/Domain/SharedVaults/UseCase/InviteToVault.ts rename to packages/services/src/Domain/VaultInvite/UseCase/InviteToVault.ts index d010ac676..cd0c0b5e2 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/InviteToVault.ts +++ b/packages/services/src/Domain/VaultInvite/UseCase/InviteToVault.ts @@ -9,7 +9,7 @@ import { SendVaultInvite } from './SendVaultInvite' import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage' import { Result, UseCaseInterface } from '@standardnotes/domain-core' -import { ShareContactWithVault } from './ShareContactWithVault' +import { ShareContactWithVault } from '../../SharedVaults/UseCase/ShareContactWithVault' import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface' export class InviteToVault implements UseCaseInterface { diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ReuploadAllInvites.ts b/packages/services/src/Domain/VaultInvite/UseCase/ReuploadAllInvites.ts similarity index 100% rename from packages/services/src/Domain/SharedVaults/UseCase/ReuploadAllInvites.ts rename to packages/services/src/Domain/VaultInvite/UseCase/ReuploadAllInvites.ts diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ReuploadInvite.ts b/packages/services/src/Domain/VaultInvite/UseCase/ReuploadInvite.ts similarity index 95% rename from packages/services/src/Domain/SharedVaults/UseCase/ReuploadInvite.ts rename to packages/services/src/Domain/VaultInvite/UseCase/ReuploadInvite.ts index daa2ad0d0..047785f50 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/ReuploadInvite.ts +++ b/packages/services/src/Domain/VaultInvite/UseCase/ReuploadInvite.ts @@ -1,4 +1,4 @@ -import { DecryptOwnMessage } from './../../Encryption/UseCase/Asymmetric/DecryptOwnMessage' +import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage' import { AsymmetricMessageSharedVaultInvite, TrustedContactInterface } from '@standardnotes/models' import { SharedVaultInviteServerHash } from '@standardnotes/responses' import { PkcKeyPair } from '@standardnotes/sncrypto-common' diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ReuploadVaultInvites.ts b/packages/services/src/Domain/VaultInvite/UseCase/ReuploadVaultInvites.ts similarity index 100% rename from packages/services/src/Domain/SharedVaults/UseCase/ReuploadVaultInvites.ts rename to packages/services/src/Domain/VaultInvite/UseCase/ReuploadVaultInvites.ts diff --git a/packages/services/src/Domain/SharedVaults/UseCase/SendVaultInvite.ts b/packages/services/src/Domain/VaultInvite/UseCase/SendVaultInvite.ts similarity index 100% rename from packages/services/src/Domain/SharedVaults/UseCase/SendVaultInvite.ts rename to packages/services/src/Domain/VaultInvite/UseCase/SendVaultInvite.ts diff --git a/packages/services/src/Domain/VaultInvite/VaultInviteService.ts b/packages/services/src/Domain/VaultInvite/VaultInviteService.ts new file mode 100644 index 000000000..8ad6416fe --- /dev/null +++ b/packages/services/src/Domain/VaultInvite/VaultInviteService.ts @@ -0,0 +1,274 @@ +import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite' +import { SyncEvent, SyncEventReceivedSharedVaultInvitesData } from './../Event/SyncEvent' +import { SessionEvent } from './../Session/SessionEvent' +import { InternalEventInterface } from './../Internal/InternalEventInterface' +import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface' +import { ItemManagerInterface } from './../Item/ItemManagerInterface' +import { FindContact } from './../Contacts/UseCase/FindContact' +import { GetUntrustedPayload } from './../AsymmetricMessage/UseCase/GetUntrustedPayload' +import { GetTrustedPayload } from './../AsymmetricMessage/UseCase/GetTrustedPayload' +import { InviteRecord } from './InviteRecord' +import { VaultUserServiceInterface } from './../VaultUser/VaultUserServiceInterface' +import { GetVault } from './../Vaults/UseCase/GetVault' +import { InviteToVault } from './UseCase/InviteToVault' +import { GetVaultContacts } from '../VaultUser/UseCase/GetVaultContacts' +import { SyncServiceInterface } from './../Sync/SyncServiceInterface' +import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface' +import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' +import { SessionsClientInterface } from './../Session/SessionsClientInterface' +import { GetAllContacts } from './../Contacts/UseCase/GetAllContacts' +import { + AsymmetricMessageSharedVaultInvite, + PayloadEmitSource, + SharedVaultListingInterface, + TrustedContactInterface, +} from '@standardnotes/models' +import { VaultInviteServiceInterface } from './VaultInviteServiceInterface' +import { + ClientDisplayableError, + SharedVaultInviteServerHash, + SharedVaultPermission, + SharedVaultUserServerHash, + isErrorResponse, +} from '@standardnotes/responses' +import { AbstractService } from './../Service/AbstractService' +import { VaultInviteServiceEvent } from './VaultInviteServiceEvent' +import { ContentType, Result } from '@standardnotes/domain-core' +import { SharedVaultInvitesServer } from '@standardnotes/api' + +export class VaultInviteService + extends AbstractService + implements VaultInviteServiceInterface, InternalEventHandlerInterface +{ + private pendingInvites: Record = {} + + constructor( + items: ItemManagerInterface, + private session: SessionsClientInterface, + private vaultUsers: VaultUserServiceInterface, + private sync: SyncServiceInterface, + private encryption: EncryptionProviderInterface, + private invitesServer: SharedVaultInvitesServer, + private _getAllContacts: GetAllContacts, + private _getVault: GetVault, + private _getVaultContacts: GetVaultContacts, + private _inviteToVault: InviteToVault, + private _getTrustedPayload: GetTrustedPayload, + private _getUntrustedPayload: GetUntrustedPayload, + private _findContact: FindContact, + private _acceptVaultInvite: AcceptVaultInvite, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) + + this.eventDisposers.push( + items.addObserver(ContentType.TYPES.TrustedContact, async ({ inserted, source }) => { + if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) { + void this.downloadInboundInvites() + } + + await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange() + }), + ) + } + + override deinit(): void { + super.deinit() + ;(this.session as unknown) = undefined + ;(this.vaultUsers as unknown) = undefined + ;(this.sync as unknown) = undefined + ;(this.encryption as unknown) = undefined + ;(this.invitesServer as unknown) = undefined + ;(this._getAllContacts as unknown) = undefined + ;(this._getVault as unknown) = undefined + ;(this._getVaultContacts as unknown) = undefined + ;(this._inviteToVault as unknown) = undefined + ;(this._getTrustedPayload as unknown) = undefined + ;(this._getUntrustedPayload as unknown) = undefined + ;(this._findContact as unknown) = undefined + ;(this._acceptVaultInvite as unknown) = undefined + + this.pendingInvites = {} + } + + async handleEvent(event: InternalEventInterface): Promise { + switch (event.type) { + case SessionEvent.UserKeyPairChanged: + void this.invitesServer.deleteAllInboundInvites() + break + case SyncEvent.ReceivedSharedVaultInvites: + await this.processInboundInvites(event.payload as SyncEventReceivedSharedVaultInvitesData) + break + } + } + + public getCachedPendingInviteRecords(): InviteRecord[] { + return Object.values(this.pendingInvites) + } + + public async downloadInboundInvites(): Promise { + const response = await this.invitesServer.getInboundUserInvites() + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to get inbound user invites ${response}`) + } + + this.pendingInvites = {} + + await this.processInboundInvites(response.data.invites) + + return response.data.invites + } + + public async getOutboundInvites( + sharedVault?: SharedVaultListingInterface, + ): Promise { + const response = await this.invitesServer.getOutboundUserInvites() + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to get outbound user invites ${response}`) + } + + if (sharedVault) { + return response.data.invites.filter((invite) => invite.shared_vault_uuid === sharedVault.sharing.sharedVaultUuid) + } + + return response.data.invites + } + + public async acceptInvite(pendingInvite: InviteRecord): Promise { + if (!pendingInvite.trusted) { + throw new Error('Cannot accept untrusted invite') + } + + await this._acceptVaultInvite.execute({ invite: pendingInvite.invite, message: pendingInvite.message }) + + delete this.pendingInvites[pendingInvite.invite.uuid] + + void this.sync.sync() + + await this.encryption.decryptErroredPayloads() + + await this.sync.syncSharedVaultsFromScratch([pendingInvite.invite.shared_vault_uuid]) + } + + public async getInvitableContactsForSharedVault( + sharedVault: SharedVaultListingInterface, + ): Promise { + const users = await this.vaultUsers.getSharedVaultUsers(sharedVault) + if (!users) { + return [] + } + + const contacts = this._getAllContacts.execute() + if (contacts.isFailed()) { + return [] + } + return contacts.getValue().filter((contact) => { + const isContactAlreadyInVault = users.some((user) => user.user_uuid === contact.contactUuid) + return !isContactAlreadyInVault + }) + } + + public async inviteContactToSharedVault( + sharedVault: SharedVaultListingInterface, + contact: TrustedContactInterface, + permissions: SharedVaultPermission, + ): Promise> { + const contactsResult = await this._getVaultContacts.execute(sharedVault.sharing.sharedVaultUuid) + if (contactsResult.isFailed()) { + return Result.fail(contactsResult.getError()) + } + + const contacts = contactsResult.getValue() + + const result = await this._inviteToVault.execute({ + keys: { + encryption: this.encryption.getKeyPair(), + signing: this.encryption.getSigningKeyPair(), + }, + senderUuid: this.session.getSureUser().uuid, + sharedVault, + recipient: contact, + sharedVaultContacts: contacts, + permissions, + }) + + void this.notifyEvent(VaultInviteServiceEvent.InviteSent) + + await this.sync.sync() + + return result + } + + public isVaultUserOwner(user: SharedVaultUserServerHash): boolean { + const result = this._getVault.execute({ sharedVaultUuid: user.shared_vault_uuid }) + if (result.isFailed()) { + return false + } + + const vault = result.getValue() + return vault != undefined && vault.sharing.ownerUserUuid === user.user_uuid + } + + public async deleteInvite(invite: SharedVaultInviteServerHash): Promise { + const response = await this.invitesServer.deleteInvite({ + sharedVaultUuid: invite.shared_vault_uuid, + inviteUuid: invite.uuid, + }) + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to delete invite ${response}`) + } + + delete this.pendingInvites[invite.uuid] + } + + private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise { + const cachedInvites = this.getCachedPendingInviteRecords().map((record) => record.invite) + + await this.processInboundInvites(cachedInvites) + } + + private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise { + if (invites.length === 0) { + return + } + + for (const invite of invites) { + const sender = this._findContact.execute({ userUuid: invite.sender_uuid }) + if (!sender.isFailed()) { + const trustedMessage = this._getTrustedPayload.execute({ + message: invite, + privateKey: this.encryption.getKeyPair().privateKey, + sender: sender.getValue(), + }) + + if (!trustedMessage.isFailed()) { + this.pendingInvites[invite.uuid] = { + invite, + message: trustedMessage.getValue(), + trusted: true, + } + + continue + } + } + + const untrustedMessage = this._getUntrustedPayload.execute({ + message: invite, + privateKey: this.encryption.getKeyPair().privateKey, + }) + + if (!untrustedMessage.isFailed()) { + this.pendingInvites[invite.uuid] = { + invite, + message: untrustedMessage.getValue(), + trusted: false, + } + } + } + + void this.notifyEvent(VaultInviteServiceEvent.InvitesReloaded) + } +} diff --git a/packages/services/src/Domain/VaultInvite/VaultInviteServiceEvent.ts b/packages/services/src/Domain/VaultInvite/VaultInviteServiceEvent.ts new file mode 100644 index 000000000..d97d3b687 --- /dev/null +++ b/packages/services/src/Domain/VaultInvite/VaultInviteServiceEvent.ts @@ -0,0 +1,4 @@ +export enum VaultInviteServiceEvent { + InviteSent = 'VaultInviteServiceEvent.InviteSent', + InvitesReloaded = 'VaultInviteServiceEvent.InvitesReloaded', +} diff --git a/packages/services/src/Domain/VaultInvite/VaultInviteServiceInterface.ts b/packages/services/src/Domain/VaultInvite/VaultInviteServiceInterface.ts new file mode 100644 index 000000000..744b648e4 --- /dev/null +++ b/packages/services/src/Domain/VaultInvite/VaultInviteServiceInterface.ts @@ -0,0 +1,22 @@ +import { InviteRecord } from './InviteRecord' +import { ApplicationServiceInterface } from '../Service/ApplicationServiceInterface' +import { SharedVaultListingInterface, TrustedContactInterface } from '@standardnotes/models' +import { ClientDisplayableError, SharedVaultInviteServerHash, SharedVaultPermission } from '@standardnotes/responses' +import { VaultInviteServiceEvent } from './VaultInviteServiceEvent' +import { Result } from '@standardnotes/domain-core' + +export interface VaultInviteServiceInterface extends ApplicationServiceInterface { + getInvitableContactsForSharedVault(sharedVault: SharedVaultListingInterface): Promise + inviteContactToSharedVault( + sharedVault: SharedVaultListingInterface, + contact: TrustedContactInterface, + permissions: SharedVaultPermission, + ): Promise> + getCachedPendingInviteRecords(): InviteRecord[] + deleteInvite(invite: SharedVaultInviteServerHash): Promise + downloadInboundInvites(): Promise + getOutboundInvites( + sharedVault?: SharedVaultListingInterface, + ): Promise + acceptInvite(pendingInvite: InviteRecord): Promise +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/GetVaultContacts.ts b/packages/services/src/Domain/VaultUser/UseCase/GetVaultContacts.ts similarity index 100% rename from packages/services/src/Domain/SharedVaults/UseCase/GetVaultContacts.ts rename to packages/services/src/Domain/VaultUser/UseCase/GetVaultContacts.ts diff --git a/packages/services/src/Domain/SharedVaults/UseCase/GetVaultUsers.ts b/packages/services/src/Domain/VaultUser/UseCase/GetVaultUsers.ts similarity index 100% rename from packages/services/src/Domain/SharedVaults/UseCase/GetVaultUsers.ts rename to packages/services/src/Domain/VaultUser/UseCase/GetVaultUsers.ts diff --git a/packages/services/src/Domain/VaultUser/UseCase/IsVaultAdmin.ts b/packages/services/src/Domain/VaultUser/UseCase/IsVaultAdmin.ts new file mode 100644 index 000000000..edf081462 --- /dev/null +++ b/packages/services/src/Domain/VaultUser/UseCase/IsVaultAdmin.ts @@ -0,0 +1,12 @@ +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { SharedVaultListingInterface } from '@standardnotes/models' + +export class IsVaultAdmin implements SyncUseCaseInterface { + execute(dto: { sharedVault: SharedVaultListingInterface; userUuid: string }): Result { + if (!dto.sharedVault.sharing.ownerUserUuid) { + throw new Error(`Shared vault ${dto.sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`) + } + + return Result.ok(dto.sharedVault.sharing.ownerUserUuid === dto.userUuid) + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/LeaveSharedVault.ts b/packages/services/src/Domain/VaultUser/UseCase/LeaveSharedVault.ts similarity index 93% rename from packages/services/src/Domain/SharedVaults/UseCase/LeaveSharedVault.ts rename to packages/services/src/Domain/VaultUser/UseCase/LeaveSharedVault.ts index 355b4c123..deeaaf539 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/LeaveSharedVault.ts +++ b/packages/services/src/Domain/VaultUser/UseCase/LeaveSharedVault.ts @@ -1,6 +1,6 @@ import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' import { SharedVaultUsersServerInterface } from '@standardnotes/api' -import { DeleteThirdPartyVault } from './DeleteExternalSharedVault' +import { DeleteThirdPartyVault } from '../../SharedVaults/UseCase/DeleteExternalSharedVault' import { ItemManagerInterface } from '../../Item/ItemManagerInterface' import { SharedVaultListingInterface } from '@standardnotes/models' diff --git a/packages/services/src/Domain/SharedVaults/UseCase/RemoveSharedVaultMember.ts b/packages/services/src/Domain/VaultUser/UseCase/RemoveSharedVaultMember.ts similarity index 54% rename from packages/services/src/Domain/SharedVaults/UseCase/RemoveSharedVaultMember.ts rename to packages/services/src/Domain/VaultUser/UseCase/RemoveSharedVaultMember.ts index 290ffd5ab..8997cad78 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/RemoveSharedVaultMember.ts +++ b/packages/services/src/Domain/VaultUser/UseCase/RemoveSharedVaultMember.ts @@ -1,17 +1,20 @@ -import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' +import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/responses' import { SharedVaultUsersServerInterface } from '@standardnotes/api' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' -export class RemoveVaultMember { +export class RemoveVaultMember implements UseCaseInterface { constructor(private vaultUserServer: SharedVaultUsersServerInterface) {} - async execute(params: { sharedVaultUuid: string; userUuid: string }): Promise { + async execute(params: { sharedVaultUuid: string; userUuid: string }): Promise> { const response = await this.vaultUserServer.deleteSharedVaultUser({ sharedVaultUuid: params.sharedVaultUuid, userUuid: params.userUuid, }) if (isErrorResponse(response)) { - return ClientDisplayableError.FromNetworkError(response) + return Result.fail(getErrorFromErrorResponse(response).message) } + + return Result.ok() } } diff --git a/packages/services/src/Domain/VaultUser/VaultUserService.ts b/packages/services/src/Domain/VaultUser/VaultUserService.ts new file mode 100644 index 000000000..5e4301eda --- /dev/null +++ b/packages/services/src/Domain/VaultUser/VaultUserService.ts @@ -0,0 +1,103 @@ +import { LeaveVault } from './UseCase/LeaveSharedVault' +import { GetVault } from './../Vaults/UseCase/GetVault' +import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' +import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember' +import { VaultServiceInterface } from './../Vaults/VaultServiceInterface' +import { SessionsClientInterface } from './../Session/SessionsClientInterface' +import { GetVaultUsers } from './UseCase/GetVaultUsers' +import { SharedVaultListingInterface } from '@standardnotes/models' +import { VaultUserServiceInterface } from './VaultUserServiceInterface' +import { ClientDisplayableError, SharedVaultUserServerHash, isClientDisplayableError } from '@standardnotes/responses' +import { AbstractService } from './../Service/AbstractService' +import { VaultUserServiceEvent } from './VaultUserServiceEvent' +import { Result } from '@standardnotes/domain-core' +import { IsVaultAdmin } from './UseCase/IsVaultAdmin' + +export class VaultUserService extends AbstractService implements VaultUserServiceInterface { + constructor( + private session: SessionsClientInterface, + private vaults: VaultServiceInterface, + private _getVaultUsers: GetVaultUsers, + private _removeVaultMember: RemoveVaultMember, + private _isVaultAdmin: IsVaultAdmin, + private _getVault: GetVault, + private _leaveVault: LeaveVault, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) + } + + override deinit(): void { + super.deinit() + ;(this.session as unknown) = undefined + ;(this.vaults as unknown) = undefined + ;(this._getVaultUsers as unknown) = undefined + ;(this._removeVaultMember as unknown) = undefined + ;(this._isVaultAdmin as unknown) = undefined + ;(this._getVault as unknown) = undefined + ;(this._leaveVault as unknown) = undefined + } + + public async getSharedVaultUsers( + sharedVault: SharedVaultListingInterface, + ): Promise { + return this._getVaultUsers.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid }) + } + + public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean { + return this._isVaultAdmin + .execute({ + sharedVault, + userUuid: this.session.userUuid, + }) + .getValue() + } + + async removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise> { + if (!this.isCurrentUserSharedVaultAdmin(sharedVault)) { + throw new Error('Only vault admins can remove users') + } + + if (this.vaults.isVaultLocked(sharedVault)) { + throw new Error('Cannot remove user from locked vault') + } + + const result = await this._removeVaultMember.execute({ + sharedVaultUuid: sharedVault.sharing.sharedVaultUuid, + userUuid, + }) + + if (result.isFailed()) { + return result + } + + void this.notifyEvent(VaultUserServiceEvent.UsersChanged) + + await this.vaults.rotateVaultRootKey(sharedVault) + + return result + } + + public isVaultUserOwner(user: SharedVaultUserServerHash): boolean { + const result = this._getVault.execute({ sharedVaultUuid: user.shared_vault_uuid }) + if (result.isFailed()) { + return false + } + + const vault = result.getValue() + return vault != undefined && vault.sharing.ownerUserUuid === user.user_uuid + } + + async leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise { + const result = await this._leaveVault.execute({ + sharedVault: sharedVault, + userUuid: this.session.getSureUser().uuid, + }) + + if (isClientDisplayableError(result)) { + return result + } + + void this.notifyEvent(VaultUserServiceEvent.UsersChanged) + } +} diff --git a/packages/services/src/Domain/VaultUser/VaultUserServiceEvent.ts b/packages/services/src/Domain/VaultUser/VaultUserServiceEvent.ts new file mode 100644 index 000000000..c9fb7e930 --- /dev/null +++ b/packages/services/src/Domain/VaultUser/VaultUserServiceEvent.ts @@ -0,0 +1,3 @@ +export enum VaultUserServiceEvent { + UsersChanged = 'VaultUserServiceEvent.UsersChanged', +} diff --git a/packages/services/src/Domain/VaultUser/VaultUserServiceInterface.ts b/packages/services/src/Domain/VaultUser/VaultUserServiceInterface.ts new file mode 100644 index 000000000..c514f1fb1 --- /dev/null +++ b/packages/services/src/Domain/VaultUser/VaultUserServiceInterface.ts @@ -0,0 +1,13 @@ +import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface' +import { SharedVaultListingInterface } from '@standardnotes/models' +import { ClientDisplayableError, SharedVaultUserServerHash } from '@standardnotes/responses' +import { VaultUserServiceEvent } from './VaultUserServiceEvent' +import { Result } from '@standardnotes/domain-core' + +export interface VaultUserServiceInterface extends ApplicationServiceInterface { + getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise + isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean + removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise> + leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise + isVaultUserOwner(user: SharedVaultUserServerHash): boolean +} diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 6b9bde1ea..5e4897db4 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -57,15 +57,15 @@ export * from './Device/MobileDeviceInterface' export * from './Device/TypeCheck' export * from './Device/WebOrDesktopDeviceInterface' export * from './Diagnostics/ServiceDiagnostics' -export * from './Encryption/UseCase/DecryptBackupFile' -export * from './Encryption/EncryptionService' export * from './Encryption/EncryptionProviderInterface' +export * from './Encryption/EncryptionService' export * from './Encryption/EncryptionServiceEvent' export * from './Encryption/Functions' export * from './Encryption/UseCase/Asymmetric/DecryptMessage' export * from './Encryption/UseCase/Asymmetric/DecryptOwnMessage' export * from './Encryption/UseCase/Asymmetric/EncryptMessage' export * from './Encryption/UseCase/Asymmetric/GetMessageAdditionalData' +export * from './Encryption/UseCase/DecryptBackupFile' export * from './Encryption/UseCase/ItemsKey/CreateNewDefaultItemsKey' export * from './Encryption/UseCase/ItemsKey/CreateNewItemsKeyWithRollback' export * from './Encryption/UseCase/ItemsKey/FindDefaultItemsKey' @@ -111,6 +111,7 @@ export * from './Item/ItemRelationshipDirection' export * from './Item/ItemsServerInterface' export * from './Item/StaticItemCounter' export * from './ItemsEncryption/ItemsEncryption' +export * from './ItemsEncryption/ItemsEncryption' export * from './KeySystem/KeySystemKeyManager' export * from './Mutator/ImportDataUseCase' export * from './Mutator/MutatorClientInterface' @@ -121,39 +122,25 @@ export * from './Protection/ProtectionClientInterface' export * from './Protection/TimingDisplayOption' export * from './Revision/RevisionClientInterface' export * from './Revision/RevisionManager' -export * from './RootKeyManager/RootKeyManager' export * from './RootKeyManager/KeyMode' -export * from './ItemsEncryption/ItemsEncryption' +export * from './RootKeyManager/RootKeyManager' export * from './Service/AbstractService' export * from './Service/ApplicationServiceInterface' export * from './Session/SessionEvent' export * from './Session/SessionManagerResponse' export * from './Session/SessionsClientInterface' export * from './Session/UserKeyPairChangedEventData' -export * from './SharedVaults/PendingSharedVaultInviteRecord' export * from './SharedVaults/SharedVaultService' export * from './SharedVaults/SharedVaultServiceEvent' export * from './SharedVaults/SharedVaultServiceInterface' -export * from './SharedVaults/UseCase/AcceptVaultInvite' export * from './SharedVaults/UseCase/ConvertToSharedVault' export * from './SharedVaults/UseCase/CreateSharedVault' export * from './SharedVaults/UseCase/DeleteExternalSharedVault' export * from './SharedVaults/UseCase/DeleteSharedVault' -export * from './SharedVaults/UseCase/GetVaultContacts' -export * from './SharedVaults/UseCase/GetVaultContacts' -export * from './SharedVaults/UseCase/GetVaultUsers' -export * from './SharedVaults/UseCase/InviteToVault' -export * from './SharedVaults/UseCase/LeaveSharedVault' export * from './SharedVaults/UseCase/NotifyVaultUsersOfKeyRotation' -export * from './SharedVaults/UseCase/RemoveSharedVaultMember' -export * from './SharedVaults/UseCase/ReuploadAllInvites' -export * from './SharedVaults/UseCase/ReuploadInvite' -export * from './SharedVaults/UseCase/ReuploadVaultInvites' export * from './SharedVaults/UseCase/SendVaultDataChangedMessage' -export * from './SharedVaults/UseCase/SendVaultInvite' export * from './SharedVaults/UseCase/SendVaultKeyChangedMessage' export * from './SharedVaults/UseCase/ShareContactWithVault' -export * from './SharedVaults/UseCase/UpdateSharedVaultInvite' export * from './Singleton/SingletonManagerInterface' export * from './Status/StatusService' export * from './Status/StatusServiceInterface' @@ -186,6 +173,16 @@ export * from './User/UserClientInterface' export * from './User/UserService' export * from './UserEvent/UserEventService' export * from './UserEvent/UserEventServiceEvent' +export * from './VaultInvite/InviteRecord' +export * from './VaultInvite/UseCase/AcceptVaultInvite' +export * from './VaultInvite/UseCase/InviteToVault' +export * from './VaultInvite/UseCase/ReuploadAllInvites' +export * from './VaultInvite/UseCase/ReuploadInvite' +export * from './VaultInvite/UseCase/ReuploadVaultInvites' +export * from './VaultInvite/UseCase/SendVaultInvite' +export * from './VaultInvite/VaultInviteService' +export * from './VaultInvite/VaultInviteServiceEvent' +export * from './VaultInvite/VaultInviteServiceInterface' export * from './Vaults/ChangeVaultOptionsDTO' export * from './Vaults/UseCase/ChangeVaultKeyOptions' export * from './Vaults/UseCase/CreateVault' @@ -197,3 +194,12 @@ export * from './Vaults/UseCase/RotateVaultKey' export * from './Vaults/VaultService' export * from './Vaults/VaultServiceEvent' export * from './Vaults/VaultServiceInterface' +export * from './VaultUser/UseCase/GetVaultContacts' +export * from './VaultUser/UseCase/GetVaultContacts' +export * from './VaultUser/UseCase/GetVaultUsers' +export * from './VaultUser/UseCase/IsVaultAdmin' +export * from './VaultUser/UseCase/LeaveSharedVault' +export * from './VaultUser/UseCase/RemoveSharedVaultMember' +export * from './VaultUser/VaultUserService' +export * from './VaultUser/VaultUserServiceEvent' +export * from './VaultUser/VaultUserServiceInterface' diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index ab7a43e07..49b503f1a 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -71,6 +71,10 @@ import { HistoryServiceInterface, InternalEventPublishStrategy, EncryptionProviderInterface, + VaultUserServiceInterface, + VaultInviteServiceInterface, + UserEventServiceEvent, + VaultServiceEvent, } from '@standardnotes/services' import { PayloadEmitSource, @@ -1130,7 +1134,15 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.events.addEventHandler(this.dependencies.get(TYPES.UserService), AccountEvent.SignedInOrRegistered) this.events.addEventHandler(this.dependencies.get(TYPES.SessionManager), ApiServiceEvent.SessionRefreshed) - this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SyncEvent.ReceivedSharedVaultInvites) + this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites) + this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SessionEvent.UserKeyPairChanged) + + this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SessionEvent.UserKeyPairChanged) + this.events.addEventHandler( + this.dependencies.get(TYPES.SharedVaultService), + UserEventServiceEvent.UserEventReceived, + ) + this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), VaultServiceEvent.VaultRootKeyRotated) this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SyncEvent.ReceivedRemoteSharedVaults) this.events.addEventHandler( @@ -1332,14 +1344,26 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.dependencies.get(TYPES.HistoryManager) } - private get migrations(): MigrationService { - return this.dependencies.get(TYPES.MigrationService) - } - public get encryption(): EncryptionProviderInterface { return this.dependencies.get(TYPES.EncryptionService) } + public get events(): InternalEventBusInterface { + return this.dependencies.get(TYPES.InternalEventBus) + } + + public get vaultUsers(): VaultUserServiceInterface { + return this.dependencies.get(TYPES.VaultUserService) + } + + public get vaultInvites(): VaultInviteServiceInterface { + return this.dependencies.get(TYPES.VaultInviteService) + } + + private get migrations(): MigrationService { + return this.dependencies.get(TYPES.MigrationService) + } + private get legacyApi(): LegacyApiService { return this.dependencies.get(TYPES.LegacyApiService) } @@ -1352,10 +1376,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.dependencies.get(TYPES.WebSocketsService) } - public get events(): InternalEventBusInterface { - return this.dependencies.get(TYPES.InternalEventBus) - } - private get mfa(): SNMfaService { return this.dependencies.get(TYPES.MfaService) } diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index c3d0a76b3..c847a6a81 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -108,6 +108,9 @@ import { RootKeyManager, ItemsEncryptionService, DecryptBackupFile, + VaultUserService, + IsVaultAdmin, + VaultInviteService, } from '@standardnotes/services' import { ItemManager } from '../../Services/Items/ItemManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager' @@ -204,6 +207,10 @@ export class Dependencies { ) }) + this.factory.set(TYPES.IsVaultAdmin, () => { + return new IsVaultAdmin() + }) + this.factory.set(TYPES.DecryptBackupFile, () => { return new DecryptBackupFile(this.get(TYPES.EncryptionService)) }) @@ -608,6 +615,39 @@ export class Dependencies { return new SharedVaultUsersServer(this.get(TYPES.HttpService)) }) + this.factory.set(TYPES.VaultUserService, () => { + return new VaultUserService( + this.get(TYPES.SessionManager), + this.get(TYPES.VaultService), + this.get(TYPES.GetVaultUsers), + this.get(TYPES.RemoveVaultMember), + this.get(TYPES.IsVaultAdmin), + this.get(TYPES.GetVault), + this.get(TYPES.LeaveVault), + this.get(TYPES.InternalEventBus), + ) + }) + + this.factory.set(TYPES.VaultInviteService, () => { + return new VaultInviteService( + this.get(TYPES.ItemManager), + this.get(TYPES.SessionManager), + this.get(TYPES.VaultUserService), + this.get(TYPES.SyncService), + this.get(TYPES.EncryptionService), + this.get(TYPES.SharedVaultInvitesServer), + this.get(TYPES.GetAllContacts), + this.get(TYPES.GetVault), + this.get(TYPES.GetVaultContacts), + this.get(TYPES.InviteToVault), + this.get(TYPES.GetTrustedPayload), + this.get(TYPES.GetUntrustedPayload), + this.get(TYPES.FindContact), + this.get(TYPES.AcceptVaultInvite), + this.get(TYPES.InternalEventBus), + ) + }) + this.factory.set(TYPES.AsymmetricMessageService, () => { return new AsymmetricMessageService( this.get(TYPES.AsymmetricMessageServer), @@ -630,31 +670,21 @@ export class Dependencies { this.factory.set(TYPES.SharedVaultService, () => { return new SharedVaultService( - this.get(TYPES.SyncService), this.get(TYPES.ItemManager), this.get(TYPES.EncryptionService), this.get(TYPES.SessionManager), this.get(TYPES.VaultService), - this.get(TYPES.SharedVaultInvitesServer), this.get(TYPES.GetVault), this.get(TYPES.CreateSharedVault), this.get(TYPES.HandleKeyPairChange), this.get(TYPES.NotifyVaultUsersOfKeyRotation), this.get(TYPES.SendVaultDataChangedMessage), - this.get(TYPES.GetTrustedPayload), - this.get(TYPES.GetUntrustedPayload), this.get(TYPES.FindContact), - this.get(TYPES.GetAllContacts), - this.get(TYPES.GetVaultContacts), - this.get(TYPES.AcceptVaultInvite), - this.get(TYPES.InviteToVault), - this.get(TYPES.LeaveVault), this.get(TYPES.DeleteThirdPartyVault), this.get(TYPES.ShareContactWithVault), this.get(TYPES.ConvertToSharedVault), this.get(TYPES.DeleteSharedVault), - this.get(TYPES.RemoveVaultMember), - this.get(TYPES.GetVaultUsers), + this.get(TYPES.IsVaultAdmin), this.get(TYPES.InternalEventBus), ) }) diff --git a/packages/snjs/lib/Application/Dependencies/Types.ts b/packages/snjs/lib/Application/Dependencies/Types.ts index 60ea1015a..55bca4e5b 100644 --- a/packages/snjs/lib/Application/Dependencies/Types.ts +++ b/packages/snjs/lib/Application/Dependencies/Types.ts @@ -59,6 +59,8 @@ export const TYPES = { EncryptionOperators: Symbol.for('EncryptionOperators'), RootKeyManager: Symbol.for('RootKeyManager'), ItemsEncryptionService: Symbol.for('ItemsEncryptionService'), + VaultUserService: Symbol.for('VaultUserService'), + VaultInviteService: Symbol.for('VaultInviteService'), // Servers RevisionServer: Symbol.for('RevisionServer'), @@ -140,6 +142,7 @@ export const TYPES = { EncryptTypeAPayload: Symbol.for('EncryptTypeAPayload'), EncryptTypeAPayloadWithKeyLookup: Symbol.for('EncryptTypeAPayloadWithKeyLookup'), DecryptBackupFile: Symbol.for('DecryptBackupFile'), + IsVaultAdmin: Symbol.for('IsVaultAdmin'), // Mappers SessionStorageMapper: Symbol.for('SessionStorageMapper'), diff --git a/packages/snjs/mocha/lib/AppContext.js b/packages/snjs/mocha/lib/AppContext.js index 4127f0446..d076f7e87 100644 --- a/packages/snjs/mocha/lib/AppContext.js +++ b/packages/snjs/mocha/lib/AppContext.js @@ -97,6 +97,14 @@ export class AppContext { return this.application.sharedVaults } + get vaultUsers() { + return this.application.vaultUsers + } + + get vaultInvites() { + return this.application.vaultInvites + } + get files() { return this.application.files } @@ -210,16 +218,6 @@ export class AppContext { }) } - resolveWhenSharedVaultUserKeysResolved() { - return new Promise((resolve) => { - this.application.vaults.collaboration.addEventObserver((eventName) => { - if (eventName === SharedVaultServiceEvent.SharedVaultStatusChanged) { - resolve() - } - }) - }) - } - async awaitSignInEvent() { return new Promise((resolve) => { this.application.user.addEventObserver((eventName) => { @@ -394,7 +392,7 @@ export class AppContext { resolveWhenAllInboundSharedVaultInvitesAreDeleted() { return new Promise((resolve) => { - const objectToSpy = this.application.sharedVaults.invitesServer + const objectToSpy = this.application.vaultInvites.invitesServer sinon.stub(objectToSpy, 'deleteAllInboundInvites').callsFake(async (params) => { objectToSpy.deleteAllInboundInvites.restore() const result = await objectToSpy.deleteAllInboundInvites(params) diff --git a/packages/snjs/mocha/lib/Collaboration.js b/packages/snjs/mocha/lib/Collaboration.js index 8163e19c7..d3e79b951 100644 --- a/packages/snjs/mocha/lib/Collaboration.js +++ b/packages/snjs/mocha/lib/Collaboration.js @@ -26,13 +26,13 @@ export const createTrustedContactForUserOfContext = async ( } export const acceptAllInvites = async (context) => { - const inviteRecords = context.sharedVaults.getCachedPendingInviteRecords() + const inviteRecords = context.vaultInvites.getCachedPendingInviteRecords() if (inviteRecords.length === 0) { throw new Error('No pending invites to accept') } for (const record of inviteRecords) { - await context.sharedVaults.acceptPendingSharedVaultInvite(record) + await context.vaultInvites.acceptInvite(record) } } @@ -76,7 +76,7 @@ export const createSharedVaultWithUnacceptedButTrustedInvite = async ( const contact = await createTrustedContactForUserOfContext(context, contactContext) await createTrustedContactForUserOfContext(contactContext, context) - const invite = (await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions)).getValue() + const invite = (await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permissions)).getValue() await contactContext.sync() return { sharedVault, contact, contactContext, deinitContactContext, invite } @@ -91,7 +91,7 @@ export const createSharedVaultWithUnacceptedAndUntrustedInvite = async ( const { contactContext, deinitContactContext } = await createContactContext() const contact = await createTrustedContactForUserOfContext(context, contactContext) - const invite = (await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions)).getValue() + const invite = (await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permissions)).getValue() await contactContext.sync() return { sharedVault, contact, contactContext, deinitContactContext, invite } @@ -103,7 +103,7 @@ export const inviteNewPartyToSharedVault = async (context, sharedVault, permissi const thirdPartyContact = await createTrustedContactForUserOfContext(context, thirdPartyContext) await createTrustedContactForUserOfContext(thirdPartyContext, context) - await context.sharedVaults.inviteContactToSharedVault(sharedVault, thirdPartyContact, permissions) + await context.vaultInvites.inviteContactToSharedVault(sharedVault, thirdPartyContact, permissions) await thirdPartyContext.sync() diff --git a/packages/snjs/mocha/vaults/conflicts.test.js b/packages/snjs/mocha/vaults/conflicts.test.js index 9c0bb0ff6..70a101633 100644 --- a/packages/snjs/mocha/vaults/conflicts.test.js +++ b/packages/snjs/mocha/vaults/conflicts.test.js @@ -28,7 +28,7 @@ describe('shared vault conflicts', function () { await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) contactContext.lockSyncing() - await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid) const promise = contactContext.resolveWithConflicts() contactContext.unlockSyncing() await contactContext.changeNoteTitleAndSync(note, 'new title') @@ -98,7 +98,7 @@ describe('shared vault conflicts', function () { const { sharedVault, note, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) - await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid) await contactContext.changeNoteTitleAndSync(note, 'new title') const notes = contactContext.notes diff --git a/packages/snjs/mocha/vaults/deletion.test.js b/packages/snjs/mocha/vaults/deletion.test.js index df6f4d825..65b8fe7d1 100644 --- a/packages/snjs/mocha/vaults/deletion.test.js +++ b/packages/snjs/mocha/vaults/deletion.test.js @@ -90,7 +90,7 @@ describe('shared vault deletion', function () { const contactNote = contactContext.items.findItem(note.uuid) expect(contactNote).to.not.be.undefined - await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid) await contactContext.sync() @@ -108,7 +108,7 @@ describe('shared vault deletion', function () { expect(originalNote).to.not.be.undefined const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) - await contactContext.sharedVaults.leaveSharedVault(contactVault) + await contactContext.vaultUsers.leaveSharedVault(contactVault) const updatedContactNote = contactContext.items.findItem(note.uuid) expect(updatedContactNote).to.be.undefined @@ -140,14 +140,13 @@ describe('shared vault deletion', function () { const { sharedVault, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) - const originalSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault) + const originalSharedVaultUsers = await context.vaultUsers.getSharedVaultUsers(sharedVault) expect(originalSharedVaultUsers.length).to.equal(2) - const result = await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + const result = await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + expect(result.isFailed()).to.be.false - expect(isClientDisplayableError(result)).to.be.false - - const updatedSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault) + const updatedSharedVaultUsers = await context.vaultUsers.getSharedVaultUsers(sharedVault) expect(updatedSharedVaultUsers.length).to.equal(1) await deinitContactContext() diff --git a/packages/snjs/mocha/vaults/files.test.js b/packages/snjs/mocha/vaults/files.test.js index 7c5f51626..916fcd54b 100644 --- a/packages/snjs/mocha/vaults/files.test.js +++ b/packages/snjs/mocha/vaults/files.test.js @@ -249,7 +249,7 @@ describe('shared vault files', function () { const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault) await contactContext.sync() - await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid) const file = contactContext.items.findItem(uploadedFile.uuid) await Factory.expectThrowsAsync(() => Files.downloadFile(contactContext.files, file), 'Could not download file') diff --git a/packages/snjs/mocha/vaults/invites.test.js b/packages/snjs/mocha/vaults/invites.test.js index a666959ff..fdc0af35f 100644 --- a/packages/snjs/mocha/vaults/invites.test.js +++ b/packages/snjs/mocha/vaults/invites.test.js @@ -8,7 +8,6 @@ describe('shared vault invites', function () { this.timeout(Factory.TwentySecondTimeout) let context - let sharedVaults afterEach(async function () { await context.deinit() @@ -21,8 +20,6 @@ describe('shared vault invites', function () { context = await Factory.createAppContextWithRealCrypto() await context.launch() await context.register() - - sharedVaults = context.sharedVaults }) it('should invite contact to vault', async () => { @@ -31,7 +28,7 @@ describe('shared vault invites', function () { const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) const vaultInvite = ( - await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write) + await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write) ).getValue() expect(vaultInvite).to.not.be.undefined @@ -49,7 +46,7 @@ describe('shared vault invites', function () { const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context) - const invites = contactContext.sharedVaults.getCachedPendingInviteRecords() + const invites = contactContext.vaultInvites.getCachedPendingInviteRecords() expect(invites[0].trusted).to.be.true @@ -60,7 +57,7 @@ describe('shared vault invites', function () { const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithUnacceptedAndUntrustedInvite(context) - const invites = contactContext.sharedVaults.getCachedPendingInviteRecords() + const invites = contactContext.vaultInvites.getCachedPendingInviteRecords() expect(invites[0].trusted).to.be.false @@ -76,7 +73,7 @@ describe('shared vault invites', function () { sharedVault, ) - const invites = thirdPartyContext.sharedVaults.getCachedPendingInviteRecords() + const invites = thirdPartyContext.vaultInvites.getCachedPendingInviteRecords() const message = invites[0].message const delegatedContacts = message.data.trustedContacts @@ -103,7 +100,7 @@ describe('shared vault invites', function () { /** Sync the contact context so that they wouldn't naturally receive changes made before this point */ await contactContext.sync() - await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write) + await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write) /** Contact should now sync and expect to find note */ const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent() @@ -125,10 +122,14 @@ describe('shared vault invites', function () { const sharedVault = await Collaboration.createSharedVault(context) const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) - await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write) + await context.vaultInvites.inviteContactToSharedVault( + sharedVault, + currentContextContact, + SharedVaultPermission.Write, + ) - await contactContext.sharedVaults.downloadInboundInvites() - expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false + await contactContext.vaultInvites.downloadInboundInvites() + expect(contactContext.vaultInvites.getCachedPendingInviteRecords()[0].trusted).to.be.false await deinitContactContext() }) @@ -139,14 +140,18 @@ describe('shared vault invites', function () { const sharedVault = await Collaboration.createSharedVault(context) const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) - await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write) + await context.vaultInvites.inviteContactToSharedVault( + sharedVault, + currentContextContact, + SharedVaultPermission.Write, + ) - await contactContext.sharedVaults.downloadInboundInvites() - expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false + await contactContext.vaultInvites.downloadInboundInvites() + expect(contactContext.vaultInvites.getCachedPendingInviteRecords()[0].trusted).to.be.false await Collaboration.createTrustedContactForUserOfContext(contactContext, context) - expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.true + expect(contactContext.vaultInvites.getCachedPendingInviteRecords()[0].trusted).to.be.true await deinitContactContext() }) @@ -184,12 +189,12 @@ describe('shared vault invites', function () { const { invite, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context) - const preInvites = await contactContext.sharedVaults.downloadInboundInvites() + const preInvites = await contactContext.vaultInvites.downloadInboundInvites() expect(preInvites.length).to.equal(1) - await sharedVaults.deleteInvite(invite) + await context.vaultInvites.deleteInvite(invite) - const postInvites = await contactContext.sharedVaults.downloadInboundInvites() + const postInvites = await contactContext.vaultInvites.downloadInboundInvites() expect(postInvites.length).to.equal(0) await deinitContactContext() @@ -208,7 +213,7 @@ describe('shared vault invites', function () { await contactContext.changePassword('new-password') await promise - const invites = await contactContext.sharedVaults.downloadInboundInvites() + const invites = await contactContext.vaultInvites.downloadInboundInvites() expect(invites.length).to.equal(0) await deinitContactContext() diff --git a/packages/snjs/mocha/vaults/items.test.js b/packages/snjs/mocha/vaults/items.test.js index 6df81b384..ed7fa48a6 100644 --- a/packages/snjs/mocha/vaults/items.test.js +++ b/packages/snjs/mocha/vaults/items.test.js @@ -8,7 +8,6 @@ describe('shared vault items', function () { this.timeout(Factory.TwentySecondTimeout) let context - let sharedVaults afterEach(async function () { await context.deinit() @@ -22,8 +21,6 @@ describe('shared vault items', function () { await context.launch() await context.register() - - sharedVaults = context.sharedVaults }) it('should add item to shared vault with no other members', async () => { @@ -60,7 +57,11 @@ describe('shared vault items', function () { const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) contactContext.lockSyncing() - await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write) + await context.vaultInvites.inviteContactToSharedVault( + sharedVault, + currentContextContact, + SharedVaultPermission.Write, + ) await Collaboration.moveItemToVault(context, sharedVault, note) const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent() diff --git a/packages/snjs/mocha/vaults/key_rotation.test.js b/packages/snjs/mocha/vaults/key_rotation.test.js index e0591e6e0..f27fc4c50 100644 --- a/packages/snjs/mocha/vaults/key_rotation.test.js +++ b/packages/snjs/mocha/vaults/key_rotation.test.js @@ -8,8 +8,6 @@ describe('shared vault key rotation', function () { this.timeout(Factory.TwentySecondTimeout) let context - let vaults - let sharedVaults afterEach(async function () { await context.deinit() @@ -23,9 +21,6 @@ describe('shared vault key rotation', function () { await context.launch() await context.register() - - vaults = context.vaults - sharedVaults = context.sharedVaults }) it('should reencrypt all items keys belonging to key system', async () => { @@ -37,7 +32,7 @@ describe('shared vault key rotation', function () { const spy = sinon.spy(context.keys, 'reencryptKeySystemItemsKeysForVault') const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) - await vaults.rotateVaultRootKey(sharedVault) + await context.vaults.rotateVaultRootKey(sharedVault) await promise expect(spy.callCount).to.equal(1) @@ -52,7 +47,7 @@ describe('shared vault key rotation', function () { contactContext.lockSyncing() const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) - await vaults.rotateVaultRootKey(sharedVault) + await context.vaults.rotateVaultRootKey(sharedVault) await promise const outboundMessages = await context.asymmetric.getOutboundMessages() @@ -79,7 +74,7 @@ describe('shared vault key rotation', function () { contactContext.lockSyncing() const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) - await vaults.rotateVaultRootKey(sharedVault) + await context.vaults.rotateVaultRootKey(sharedVault) await promise const rootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier) @@ -107,7 +102,7 @@ describe('shared vault key rotation', function () { expect(previousPrimaryItemsKey).to.not.be.undefined const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) - await vaults.rotateVaultRootKey(sharedVault) + await context.vaults.rotateVaultRootKey(sharedVault) await promise const contactPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() @@ -129,15 +124,15 @@ describe('shared vault key rotation', function () { await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context) contactContext.lockSyncing() - const originalOutboundInvites = await sharedVaults.getOutboundInvites() + const originalOutboundInvites = await context.vaultInvites.getOutboundInvites() expect(originalOutboundInvites.length).to.equal(1) const originalInviteMessage = originalOutboundInvites[0].encrypted_message const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) - await vaults.rotateVaultRootKey(sharedVault) + await context.vaults.rotateVaultRootKey(sharedVault) await promise - const updatedOutboundInvites = await sharedVaults.getOutboundInvites() + const updatedOutboundInvites = await context.vaultInvites.getOutboundInvites() expect(updatedOutboundInvites.length).to.equal(1) const joinInvite = updatedOutboundInvites[0] @@ -150,7 +145,7 @@ describe('shared vault key rotation', function () { it('new key system items key in rotated shared vault should belong to shared vault', async () => { const sharedVault = await Collaboration.createSharedVault(context) - await vaults.rotateVaultRootKey(sharedVault) + await context.vaults.rotateVaultRootKey(sharedVault) const keySystemItemsKeys = context.keys .getAllKeySystemItemsKeys() @@ -170,7 +165,7 @@ describe('shared vault key rotation', function () { contactContext.lockSyncing() const firstPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) - await vaults.rotateVaultRootKey(sharedVault) + await context.vaults.rotateVaultRootKey(sharedVault) await firstPromise const asymmetricMessageAfterFirstChange = await context.asymmetric.getOutboundMessages() @@ -180,7 +175,7 @@ describe('shared vault key rotation', function () { const messageAfterFirstChange = asymmetricMessageAfterFirstChange[0] const secondPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) - await vaults.rotateVaultRootKey(sharedVault) + await context.vaults.rotateVaultRootKey(sharedVault) await secondPromise const asymmetricMessageAfterSecondChange = await context.asymmetric.getOutboundMessages() @@ -204,7 +199,7 @@ describe('shared vault key rotation', function () { contactContext.lockSyncing() const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) - await vaults.rotateVaultRootKey(sharedVault) + await context.vaults.rotateVaultRootKey(sharedVault) await promise const acceptMessage = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') @@ -223,7 +218,7 @@ describe('shared vault key rotation', function () { const originalKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier) - await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid) const newKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier) diff --git a/packages/snjs/mocha/vaults/permissions.test.js b/packages/snjs/mocha/vaults/permissions.test.js index 4f270a03d..5db651443 100644 --- a/packages/snjs/mocha/vaults/permissions.test.js +++ b/packages/snjs/mocha/vaults/permissions.test.js @@ -8,7 +8,6 @@ describe('shared vault permissions', function () { this.timeout(Factory.TwentySecondTimeout) let context - let sharedVaults afterEach(async function () { await context.deinit() @@ -22,8 +21,6 @@ describe('shared vault permissions', function () { await context.launch() await context.register() - - sharedVaults = context.sharedVaults }) it('non-admin user should not be able to invite user', async () => { @@ -37,7 +34,7 @@ describe('shared vault permissions', function () { contactContext, thirdParty.contactContext, ) - const result = await contactContext.sharedVaults.inviteContactToSharedVault( + const result = await contactContext.vaultInvites.inviteContactToSharedVault( sharedVault, thirdPartyContact, SharedVaultPermission.Write, @@ -53,16 +50,15 @@ describe('shared vault permissions', function () { const sharedVault = await Collaboration.createSharedVault(context) - const result = await sharedVaults.removeUserFromSharedVault(sharedVault, context.userUuid) - - expect(isClientDisplayableError(result)).to.be.true + const result = await context.vaultUsers.removeUserFromSharedVault(sharedVault, context.userUuid) + expect(result.isFailed()).to.be.true }) it('should be able to leave shared vault as added admin', async () => { const { contactVault, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Admin) - const result = await contactContext.sharedVaults.leaveSharedVault(contactVault) + const result = await contactContext.vaultUsers.leaveSharedVault(contactVault) expect(isClientDisplayableError(result)).to.be.false diff --git a/packages/snjs/mocha/vaults/shared_vaults.test.js b/packages/snjs/mocha/vaults/shared_vaults.test.js index 092ea757a..b95f81b67 100644 --- a/packages/snjs/mocha/vaults/shared_vaults.test.js +++ b/packages/snjs/mocha/vaults/shared_vaults.test.js @@ -54,9 +54,8 @@ describe('shared vaults', function () { const { sharedVault, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) - const result = await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) - - expect(result).to.be.undefined + const result = await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + expect(result.isFailed()).to.be.false const promise = contactContext.resolveWhenUserMessagesProcessingCompletes() await contactContext.sync() diff --git a/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx b/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx index 5b76e3773..e61fcde03 100644 --- a/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx +++ b/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx @@ -61,14 +61,14 @@ export const PreprocessingStep = ({ useEffect(() => { const processPendingInvites = async () => { - await application.sharedVaults.downloadInboundInvites() - const hasPendingInvites = application.sharedVaults.getCachedPendingInviteRecords().length > 0 + await application.vaultInvites.downloadInboundInvites() + const hasPendingInvites = application.vaultInvites.getCachedPendingInviteRecords().length > 0 setNeedsUserConfirmation(hasPendingInvites ? 'yes' : 'no') setIsProcessingInvites(false) } void processPendingInvites() - }, [application.sharedVaults]) + }, [application]) const isProcessing = isProcessingSync || isProcessingMessages || isProcessingInvites diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/EditContactModal.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/EditContactModal.tsx index 4ce1dfd08..85177b174 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/EditContactModal.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/EditContactModal.tsx @@ -2,10 +2,10 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 're import Modal, { ModalAction } from '@/Components/Modal/Modal' import DecoratedInput from '@/Components/Input/DecoratedInput' import { useApplication } from '@/Components/ApplicationProvider' -import { PendingSharedVaultInviteRecord, TrustedContactInterface } from '@standardnotes/snjs' +import { InviteRecord, TrustedContactInterface } from '@standardnotes/snjs' type Props = { - fromInvite?: PendingSharedVaultInviteRecord + fromInvite?: InviteRecord editContactUuid?: string onCloseDialog: () => void onAddContact?: (contact: TrustedContactInterface) => void diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx index 3804c6c1f..672da0387 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx @@ -16,11 +16,11 @@ const ContactInviteModal: FunctionComponent = ({ vault, onCloseDialog }) useEffect(() => { const loadContacts = async () => { - const contacts = await application.sharedVaults.getInvitableContactsForSharedVault(vault) + const contacts = await application.vaultInvites.getInvitableContactsForSharedVault(vault) setContacts(contacts) } void loadContacts() - }, [application.sharedVaults, vault]) + }, [application.vaultInvites, vault]) const handleDialogClose = useCallback(() => { onCloseDialog() @@ -28,10 +28,10 @@ const ContactInviteModal: FunctionComponent = ({ vault, onCloseDialog }) const inviteSelectedContacts = useCallback(async () => { for (const contact of selectedContacts) { - await application.sharedVaults.inviteContactToSharedVault(vault, contact, SharedVaultPermission.Write) + await application.vaultInvites.inviteContactToSharedVault(vault, contact, SharedVaultPermission.Write) } handleDialogClose() - }, [application.sharedVaults, vault, handleDialogClose, selectedContacts]) + }, [application.vaultInvites, vault, handleDialogClose, selectedContacts]) const toggleContact = useCallback( (contact: TrustedContactInterface) => { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx index a462af495..1c355e822 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx @@ -2,13 +2,13 @@ import { useApplication } from '@/Components/ApplicationProvider' import Button from '@/Components/Button/Button' import Icon from '@/Components/Icon/Icon' import ModalOverlay from '@/Components/Modal/ModalOverlay' -import { PendingSharedVaultInviteRecord } from '@standardnotes/snjs' +import { InviteRecord } from '@standardnotes/snjs' import { useCallback, useState } from 'react' import EditContactModal from '../Contacts/EditContactModal' import { CheckmarkCircle } from '../../../../UIElements/CheckmarkCircle' type Props = { - inviteRecord: PendingSharedVaultInviteRecord + inviteRecord: InviteRecord } const InviteItem = ({ inviteRecord }: Props) => { @@ -23,8 +23,8 @@ const InviteItem = ({ inviteRecord }: Props) => { }, []) const acceptInvite = useCallback(async () => { - await application.sharedVaults.acceptPendingSharedVaultInvite(inviteRecord) - }, [application.sharedVaults, inviteRecord]) + await application.vaultInvites.acceptInvite(inviteRecord) + }, [application, inviteRecord]) const closeAddContactModal = () => setIsAddContactModalOpen(false) const collaborationId = application.contacts.getCollaborationIDFromInvite(inviteRecord.invite) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx index 0aac0610f..b375da9f8 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx @@ -10,9 +10,10 @@ import { useCallback, useEffect, useState } from 'react' import { VaultListingInterface, TrustedContactInterface, - PendingSharedVaultInviteRecord, + InviteRecord, ContentType, SharedVaultServiceEvent, + VaultUserServiceEvent, } from '@standardnotes/snjs' import VaultItem from './Vaults/VaultItem' import Button from '@/Components/Button/Button' @@ -23,7 +24,7 @@ const Vaults = () => { const application = useApplication() const [vaults, setVaults] = useState([]) - const [invites, setInvites] = useState([]) + const [invites, setInvites] = useState([]) const [contacts, setContacts] = useState([]) const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false) @@ -33,7 +34,6 @@ const Vaults = () => { const closeVaultModal = () => setIsVaultModalOpen(false) const vaultService = application.vaults - const sharedVaultService = application.sharedVaults const contactService = application.contacts const updateVaults = useCallback(async () => { @@ -41,8 +41,8 @@ const Vaults = () => { }, [vaultService]) const updateInvites = useCallback(async () => { - setInvites(sharedVaultService.getCachedPendingInviteRecords()) - }, [sharedVaultService]) + setInvites(application.vaultInvites.getCachedPendingInviteRecords()) + }, [application.vaultInvites]) const updateContacts = useCallback(async () => { setContacts(contactService.getAllContacts()) @@ -60,6 +60,20 @@ const Vaults = () => { }) }, [application.sharedVaults, updateAllData]) + useEffect(() => { + return application.vaultUsers.addEventObserver((event) => { + if (event === VaultUserServiceEvent.UsersChanged) { + void updateAllData() + } + }) + }, [application.vaultUsers, updateAllData]) + + useEffect(() => { + return application.vaultInvites.addEventObserver(() => { + void updateAllData() + }) + }, [application.vaultInvites, updateAllData]) + useEffect(() => { return application.streamItems([ContentType.TYPES.VaultListing, ContentType.TYPES.TrustedContact], () => { void updateAllData() @@ -67,9 +81,9 @@ const Vaults = () => { }, [application, updateAllData]) useEffect(() => { - void sharedVaultService.downloadInboundInvites() + void application.vaultInvites.downloadInboundInvites() void updateAllData() - }, [updateAllData, sharedVaultService]) + }, [updateAllData, application.vaultInvites]) const createNewVault = useCallback(async () => { setIsVaultModalOpen(true) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx index 7a6b3097c..62daa8648 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx @@ -20,7 +20,7 @@ const VaultItem = ({ vault }: Props) => { const [isVaultModalOpen, setIsVaultModalOpen] = useState(false) const closeVaultModal = () => setIsVaultModalOpen(false) - const isAdmin = !vault.isSharedVaultListing() ? true : application.sharedVaults.isCurrentUserSharedVaultAdmin(vault) + const isAdmin = !vault.isSharedVaultListing() ? true : application.vaultUsers.isCurrentUserSharedVaultAdmin(vault) const deleteVault = useCallback(async () => { const confirm = await application.alerts.confirm( @@ -61,11 +61,11 @@ const VaultItem = ({ vault }: Props) => { return } - const success = await application.sharedVaults.leaveSharedVault(vault) + const success = await application.vaultUsers.leaveSharedVault(vault) if (!success) { void application.alerts.alert('Unable to leave vault. Please try again.') } - }, [application.alerts, application.sharedVaults, vault]) + }, [application, vault]) const convertToSharedVault = useCallback(async () => { await application.sharedVaults.convertVaultToSharedVault(vault) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx index 4576af7ed..9c5425e6d 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx @@ -54,20 +54,20 @@ const EditVaultModal: FunctionComponent = ({ onCloseDialog, existingVault if (existingVault.isSharedVaultListing()) { setIsAdmin( - existingVault.isSharedVaultListing() && application.sharedVaults.isCurrentUserSharedVaultAdmin(existingVault), + existingVault.isSharedVaultListing() && application.vaultUsers.isCurrentUserSharedVaultAdmin(existingVault), ) - const users = await application.sharedVaults.getSharedVaultUsers(existingVault) + const users = await application.vaultUsers.getSharedVaultUsers(existingVault) if (users) { setMembers(users) } - const invites = await application.sharedVaults.getOutboundInvites(existingVault) + const invites = await application.vaultInvites.getOutboundInvites(existingVault) if (!isClientDisplayableError(invites)) { setInvites(invites) } } - }, [application.sharedVaults, existingVault]) + }, [application, existingVault]) useEffect(() => { void reloadVaultInfo() diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx index a67838515..040432d97 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx @@ -17,10 +17,10 @@ export const VaultModalInvites = ({ const deleteInvite = useCallback( async (invite: SharedVaultInviteServerHash) => { - await application.sharedVaults.deleteInvite(invite) + await application.vaultInvites.deleteInvite(invite) onChange() }, - [application.sharedVaults, onChange], + [application.vaultInvites, onChange], ) return ( diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalMembers.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalMembers.tsx index 3cd3e5de6..2c680c1dd 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalMembers.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalMembers.tsx @@ -20,18 +20,18 @@ export const VaultModalMembers = ({ const removeMemberFromVault = useCallback( async (memberItem: SharedVaultUserServerHash) => { if (vault.isSharedVaultListing()) { - await application.sharedVaults.removeUserFromSharedVault(vault, memberItem.user_uuid) + await application.vaultUsers.removeUserFromSharedVault(vault, memberItem.user_uuid) onChange() } }, - [application.sharedVaults, vault, onChange], + [application.vaultUsers, vault, onChange], ) return (
Vault Members
{members.map((member) => { - if (application.sharedVaults.isSharedVaultUserSharedVaultOwner(member)) { + if (application.vaultUsers.isVaultUserOwner(member)) { return null }