refactor: break up vault services (#2364)

This commit is contained in:
Mo
2023-07-24 07:46:20 -05:00
committed by GitHub
parent f2d089ab24
commit 3281ac9d37
50 changed files with 763 additions and 633 deletions

View File

@@ -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<void> {

View File

@@ -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<EncryptionProviderInterface>
const session = {} as jest.Mocked<SessionsClientInterface>
const vaults = {} as jest.Mocked<VaultServiceInterface>
const invitesServer = {} as jest.Mocked<SharedVaultInvitesServer>
const getVault = {} as jest.Mocked<GetVault>
const createSharedVaultUseCase = {} as jest.Mocked<CreateSharedVault>
const handleKeyPairChange = {} as jest.Mocked<HandleKeyPairChange>
const notifyVaultUsersOfKeyRotation = {} as jest.Mocked<NotifyVaultUsersOfKeyRotation>
const sendVaultDataChangeMessage = {} as jest.Mocked<SendVaultDataChangedMessage>
const getTrustedPayload = {} as jest.Mocked<GetTrustedPayload>
const getUntrustedPayload = {} as jest.Mocked<GetUntrustedPayload>
const findContact = {} as jest.Mocked<FindContact>
const getAllContacts = {} as jest.Mocked<GetAllContacts>
const getVaultContacts = {} as jest.Mocked<GetVaultContacts>
const acceptVaultInvite = {} as jest.Mocked<AcceptVaultInvite>
const inviteToVault = {} as jest.Mocked<InviteToVault>
const leaveVault = {} as jest.Mocked<LeaveVault>
const deleteThirdPartyVault = {} as jest.Mocked<DeleteThirdPartyVault>
const shareContactWithVault = {} as jest.Mocked<ShareContactWithVault>
const convertToSharedVault = {} as jest.Mocked<ConvertToSharedVault>
const deleteSharedVaultUseCase = {} as jest.Mocked<DeleteSharedVault>
const removeVaultMember = {} as jest.Mocked<RemoveVaultMember>
const getSharedVaultUsersUseCase = {} as jest.Mocked<GetVaultUsers>
const isVaultAdmin = {} as jest.Mocked<IsVaultAdmin>
const eventBus = {} as jest.Mocked<InternalEventBusInterface>
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,
)
})

View File

@@ -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<SharedVaultServiceEvent, SharedVaultServiceEventPayload>
implements SharedVaultServiceInterface, InternalEventHandlerInterface
{
private pendingInvites: Record<string, PendingSharedVaultInviteRecord> = {}
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<TrustedContactInterface>(
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<TrustedContactInterface>(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<void> {
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<void> {
if (event.eventPayload.eventType === UserEventType.RemovedFromSharedVault) {
const vault = this.getVault.execute<SharedVaultListingInterface>({
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<SharedVaultListingInterface>({
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<void> {
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<VaultListingInterface | ClientDisplayableError> {
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<SharedVaultListingInterface | ClientDisplayableError> {
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<SharedVaultListingInterface>({ 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<void> {
await this.downloadInboundInvites()
}
private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise<void> {
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<ClientDisplayableError | SharedVaultInviteServerHash[]> {
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<SharedVaultInviteServerHash[] | ClientDisplayableError> {
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<ClientDisplayableError | void> {
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<ClientDisplayableError | void> {
return this.deleteSharedVaultUseCase.execute({ sharedVault })
}
private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise<void> {
const cachedInvites = this.getCachedPendingInviteRecords().map((record) => record.invite)
await this.processInboundInvites(cachedInvites)
}
private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise<void> {
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<AsymmetricMessageSharedVaultInvite>({
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<AsymmetricMessageSharedVaultInvite>({
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<void> {
await this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged)
}
async acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise<void> {
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<void> {
await this.encryption.decryptErroredPayloads()
}
public async getInvitableContactsForSharedVault(
sharedVault: SharedVaultListingInterface,
): Promise<TrustedContactInterface[]> {
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<TrustedContactInterface[]> {
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<Result<SharedVaultInviteServerHash>> {
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<ClientDisplayableError | void> {
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<ClientDisplayableError | void> {
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<SharedVaultUserServerHash[] | undefined> {
return this.getSharedVaultUsersUseCase.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid })
return this._deleteSharedVaultUseCase.execute({ sharedVault })
}
async shareContactWithVaults(contact: TrustedContactInterface): Promise<void> {
@@ -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
}
}

View File

@@ -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<SharedVaultServiceEvent, SharedVaultServiceEventPayload> {
@@ -25,32 +18,8 @@ export interface SharedVaultServiceInterface
storagePreference?: KeySystemRootKeyStorageMode
}): Promise<VaultListingInterface | ClientDisplayableError>
deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
convertVaultToSharedVault(vault: VaultListingInterface): Promise<SharedVaultListingInterface | ClientDisplayableError>
inviteContactToSharedVault(
sharedVault: SharedVaultListingInterface,
contact: TrustedContactInterface,
permissions: SharedVaultPermission,
): Promise<Result<SharedVaultInviteServerHash>>
removeUserFromSharedVault(
sharedVault: SharedVaultListingInterface,
userUuid: string,
): Promise<ClientDisplayableError | void>
leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise<SharedVaultUserServerHash[] | undefined>
isSharedVaultUserSharedVaultOwner(user: SharedVaultUserServerHash): boolean
isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean
getItemLastEditedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined
getItemSharedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined
downloadInboundInvites(): Promise<ClientDisplayableError | SharedVaultInviteServerHash[]>
getOutboundInvites(
sharedVault?: SharedVaultListingInterface,
): Promise<SharedVaultInviteServerHash[] | ClientDisplayableError>
acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise<void>
getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[]
getInvitableContactsForSharedVault(sharedVault: SharedVaultListingInterface): Promise<TrustedContactInterface[]>
deleteInvite(invite: SharedVaultInviteServerHash): Promise<ClientDisplayableError | void>
}

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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<void> {
constructor(

View File

@@ -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<SharedVaultInviteServerHash | ClientDisplayableError> {
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
}
}

View File

@@ -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

View File

@@ -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<SharedVaultInviteServerHash> {

View File

@@ -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'

View File

@@ -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<VaultInviteServiceEvent>
implements VaultInviteServiceInterface, InternalEventHandlerInterface
{
private pendingInvites: Record<string, InviteRecord> = {}
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<TrustedContactInterface>(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<void> {
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<ClientDisplayableError | SharedVaultInviteServerHash[]> {
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<SharedVaultInviteServerHash[] | ClientDisplayableError> {
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<void> {
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<TrustedContactInterface[]> {
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<Result<SharedVaultInviteServerHash>> {
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<SharedVaultListingInterface>({ 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<ClientDisplayableError | void> {
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<void> {
const cachedInvites = this.getCachedPendingInviteRecords().map((record) => record.invite)
await this.processInboundInvites(cachedInvites)
}
private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise<void> {
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<AsymmetricMessageSharedVaultInvite>({
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<AsymmetricMessageSharedVaultInvite>({
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)
}
}

View File

@@ -0,0 +1,4 @@
export enum VaultInviteServiceEvent {
InviteSent = 'VaultInviteServiceEvent.InviteSent',
InvitesReloaded = 'VaultInviteServiceEvent.InvitesReloaded',
}

View File

@@ -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<VaultInviteServiceEvent, unknown> {
getInvitableContactsForSharedVault(sharedVault: SharedVaultListingInterface): Promise<TrustedContactInterface[]>
inviteContactToSharedVault(
sharedVault: SharedVaultListingInterface,
contact: TrustedContactInterface,
permissions: SharedVaultPermission,
): Promise<Result<SharedVaultInviteServerHash>>
getCachedPendingInviteRecords(): InviteRecord[]
deleteInvite(invite: SharedVaultInviteServerHash): Promise<ClientDisplayableError | void>
downloadInboundInvites(): Promise<ClientDisplayableError | SharedVaultInviteServerHash[]>
getOutboundInvites(
sharedVault?: SharedVaultListingInterface,
): Promise<SharedVaultInviteServerHash[] | ClientDisplayableError>
acceptInvite(pendingInvite: InviteRecord): Promise<void>
}

View File

@@ -0,0 +1,12 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
export class IsVaultAdmin implements SyncUseCaseInterface<boolean> {
execute(dto: { sharedVault: SharedVaultListingInterface; userUuid: string }): Result<boolean> {
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)
}
}

View File

@@ -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'

View File

@@ -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<void> {
constructor(private vaultUserServer: SharedVaultUsersServerInterface) {}
async execute(params: { sharedVaultUuid: string; userUuid: string }): Promise<ClientDisplayableError | void> {
async execute(params: { sharedVaultUuid: string; userUuid: string }): Promise<Result<void>> {
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()
}
}

View File

@@ -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<VaultUserServiceEvent> 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<SharedVaultUserServerHash[] | undefined> {
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<Result<void>> {
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<SharedVaultListingInterface>({ 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<ClientDisplayableError | void> {
const result = await this._leaveVault.execute({
sharedVault: sharedVault,
userUuid: this.session.getSureUser().uuid,
})
if (isClientDisplayableError(result)) {
return result
}
void this.notifyEvent(VaultUserServiceEvent.UsersChanged)
}
}

View File

@@ -0,0 +1,3 @@
export enum VaultUserServiceEvent {
UsersChanged = 'VaultUserServiceEvent.UsersChanged',
}

View File

@@ -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<VaultUserServiceEvent, unknown> {
getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise<SharedVaultUserServerHash[] | undefined>
isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean
removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>>
leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
isVaultUserOwner(user: SharedVaultUserServerHash): boolean
}

View File

@@ -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'

View File

@@ -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<HistoryServiceInterface>(TYPES.HistoryManager)
}
private get migrations(): MigrationService {
return this.dependencies.get<MigrationService>(TYPES.MigrationService)
}
public get encryption(): EncryptionProviderInterface {
return this.dependencies.get<EncryptionProviderInterface>(TYPES.EncryptionService)
}
public get events(): InternalEventBusInterface {
return this.dependencies.get<InternalEventBusInterface>(TYPES.InternalEventBus)
}
public get vaultUsers(): VaultUserServiceInterface {
return this.dependencies.get<VaultUserServiceInterface>(TYPES.VaultUserService)
}
public get vaultInvites(): VaultInviteServiceInterface {
return this.dependencies.get<VaultInviteServiceInterface>(TYPES.VaultInviteService)
}
private get migrations(): MigrationService {
return this.dependencies.get<MigrationService>(TYPES.MigrationService)
}
private get legacyApi(): LegacyApiService {
return this.dependencies.get<LegacyApiService>(TYPES.LegacyApiService)
}
@@ -1352,10 +1376,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.dependencies.get<WebSocketsService>(TYPES.WebSocketsService)
}
public get events(): InternalEventBusInterface {
return this.dependencies.get<InternalEventBusInterface>(TYPES.InternalEventBus)
}
private get mfa(): SNMfaService {
return this.dependencies.get<SNMfaService>(TYPES.MfaService)
}

View File

@@ -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),
)
})

View File

@@ -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'),

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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')

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -16,11 +16,11 @@ const ContactInviteModal: FunctionComponent<Props> = ({ 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<Props> = ({ 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) => {

View File

@@ -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)

View File

@@ -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<VaultListingInterface[]>([])
const [invites, setInvites] = useState<PendingSharedVaultInviteRecord[]>([])
const [invites, setInvites] = useState<InviteRecord[]>([])
const [contacts, setContacts] = useState<TrustedContactInterface[]>([])
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)

View File

@@ -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)

View File

@@ -54,20 +54,20 @@ const EditVaultModal: FunctionComponent<Props> = ({ 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()

View File

@@ -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 (

View File

@@ -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 (
<div className="mb-3">
<div className="mb-3 text-lg">Vault Members</div>
{members.map((member) => {
if (application.sharedVaults.isSharedVaultUserSharedVaultOwner(member)) {
if (application.vaultUsers.isVaultUserOwner(member)) {
return null
}