From 2ce908da29d64de2c76c5344a4daed274646b1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Tue, 26 Sep 2023 13:48:36 +0200 Subject: [PATCH] chore: add method to designate a shared vault survivor (#2538) --- .../DesignateSurvivorResponse.ts | 3 + .../Domain/Server/SharedVaultUsers/Paths.ts | 2 + .../SharedVaultUsersServer.ts | 12 ++++ .../SharedVaultUsersServerInterface.ts | 8 ++- .../DesignateSurvivor.spec.ts | 66 +++++++++++++++++++ .../DesignateSurvivor/DesignateSurvivor.ts | 34 ++++++++++ .../DesignateSurvivor/DesignateSurvivorDTO.ts | 4 ++ .../src/Domain/VaultUser/VaultUserService.ts | 15 +++++ .../VaultUser/VaultUserServiceInterface.ts | 1 + packages/services/src/Domain/index.ts | 6 +- .../Application/Dependencies/Dependencies.ts | 13 +++- .../lib/Application/Dependencies/Types.ts | 1 + 12 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 packages/api/src/Domain/Response/SharedVaultUsers/DesignateSurvivorResponse.ts create mode 100644 packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivor.spec.ts create mode 100644 packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivor.ts create mode 100644 packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivorDTO.ts diff --git a/packages/api/src/Domain/Response/SharedVaultUsers/DesignateSurvivorResponse.ts b/packages/api/src/Domain/Response/SharedVaultUsers/DesignateSurvivorResponse.ts new file mode 100644 index 000000000..9b13dd9d5 --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultUsers/DesignateSurvivorResponse.ts @@ -0,0 +1,3 @@ +export type DesignateSurvivorResponse = { + success: boolean +} diff --git a/packages/api/src/Domain/Server/SharedVaultUsers/Paths.ts b/packages/api/src/Domain/Server/SharedVaultUsers/Paths.ts index ecab7f636..113e7a736 100644 --- a/packages/api/src/Domain/Server/SharedVaultUsers/Paths.ts +++ b/packages/api/src/Domain/Server/SharedVaultUsers/Paths.ts @@ -2,4 +2,6 @@ export const SharedVaultUsersPaths = { getSharedVaultUsers: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}/users`, deleteSharedVaultUser: (sharedVaultUuid: string, userUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}/users/${userUuid}`, + designateSurvivor: (sharedVaultUuid: string, sharedVaultMemberUuid: string) => + `/v1/shared-vaults/${sharedVaultUuid}/users/${sharedVaultMemberUuid}/designate-survivor`, } diff --git a/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServer.ts b/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServer.ts index 299b2e3ba..0581cf6d0 100644 --- a/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServer.ts +++ b/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServer.ts @@ -1,4 +1,6 @@ import { HttpResponse } from '@standardnotes/responses' +import { Uuid } from '@standardnotes/domain-core' + import { HttpServiceInterface } from '../../Http' import { GetSharedVaultUsersRequestParams } from '../../Request/SharedVaultUser/GetSharedVaultUsersRequestParams' import { DeleteSharedVaultUserRequestParams } from '../../Request/SharedVaultUser/DeleteSharedVaultUserRequestParams' @@ -6,10 +8,20 @@ import { DeleteSharedVaultUserResponse } from '../../Response/SharedVaultUsers/D import { SharedVaultUsersServerInterface } from './SharedVaultUsersServerInterface' import { SharedVaultUsersPaths } from './Paths' import { GetSharedVaultUsersResponse } from '../../Response/SharedVaultUsers/GetSharedVaultUsersResponse' +import { DesignateSurvivorResponse } from '../../Response/SharedVaultUsers/DesignateSurvivorResponse' export class SharedVaultUsersServer implements SharedVaultUsersServerInterface { constructor(private httpService: HttpServiceInterface) {} + async designateSurvivor(params: { + sharedVaultUuid: Uuid + sharedVaultMemberUuid: Uuid + }): Promise> { + return this.httpService.post( + SharedVaultUsersPaths.designateSurvivor(params.sharedVaultUuid.value, params.sharedVaultMemberUuid.value), + ) + } + getSharedVaultUsers(params: GetSharedVaultUsersRequestParams): Promise> { return this.httpService.get(SharedVaultUsersPaths.getSharedVaultUsers(params.sharedVaultUuid)) } diff --git a/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServerInterface.ts b/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServerInterface.ts index 9cb87c423..233ecabc5 100644 --- a/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServerInterface.ts +++ b/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServerInterface.ts @@ -1,12 +1,18 @@ import { HttpResponse } from '@standardnotes/responses' +import { Uuid } from '@standardnotes/domain-core' + import { GetSharedVaultUsersRequestParams } from '../../Request/SharedVaultUser/GetSharedVaultUsersRequestParams' import { DeleteSharedVaultUserRequestParams } from '../../Request/SharedVaultUser/DeleteSharedVaultUserRequestParams' import { DeleteSharedVaultUserResponse } from '../../Response/SharedVaultUsers/DeleteSharedVaultUserResponse' import { GetSharedVaultUsersResponse } from '../../Response/SharedVaultUsers/GetSharedVaultUsersResponse' +import { DesignateSurvivorResponse } from '../../Response/SharedVaultUsers/DesignateSurvivorResponse' export interface SharedVaultUsersServerInterface { getSharedVaultUsers(params: GetSharedVaultUsersRequestParams): Promise> - + designateSurvivor(params: { + sharedVaultUuid: Uuid + sharedVaultMemberUuid: Uuid + }): Promise> deleteSharedVaultUser( params: DeleteSharedVaultUserRequestParams, ): Promise> diff --git a/packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivor.spec.ts b/packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivor.spec.ts new file mode 100644 index 000000000..f21659139 --- /dev/null +++ b/packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivor.spec.ts @@ -0,0 +1,66 @@ +import { SharedVaultUsersServerInterface } from '@standardnotes/api' + +import { DesignateSurvivor } from './DesignateSurvivor' + +describe('DesignateSurvivor', () => { + let sharedVaultUserServer: SharedVaultUsersServerInterface + + const createUseCase = () => new DesignateSurvivor( + sharedVaultUserServer, + ) + + beforeEach(() => { + sharedVaultUserServer = {} as jest.Mocked + sharedVaultUserServer.designateSurvivor = jest.fn().mockReturnValue({ + status: 200, + }) + }) + + it('should mark designated survivor', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultMemberUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(false) + }) + + it('should fail if shared vault uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + sharedVaultUuid: 'invalid', + sharedVaultMemberUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + }) + + it('should fail if shared vault member uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultMemberUuid: 'invalid', + }) + + expect(result.isFailed()).toBe(true) + }) + + it('should fail if shared vault user server fails', async () => { + sharedVaultUserServer.designateSurvivor = jest.fn().mockReturnValue({ + status: 500, + }) + + const useCase = createUseCase() + + const result = await useCase.execute({ + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultMemberUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + }) +}) diff --git a/packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivor.ts b/packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivor.ts new file mode 100644 index 000000000..168f17d1f --- /dev/null +++ b/packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivor.ts @@ -0,0 +1,34 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { SharedVaultUsersServerInterface } from '@standardnotes/api' +import { HttpStatusCode } from '@standardnotes/responses' + +import { DesignateSurvivorDTO } from './DesignateSurvivorDTO' + +export class DesignateSurvivor implements UseCaseInterface { + constructor(private sharedVaultUserServer: SharedVaultUsersServerInterface) {} + + async execute(dto: DesignateSurvivorDTO): Promise> { + const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + return Result.fail(sharedVaultUuidOrError.getError()) + } + const sharedVaultUuid = sharedVaultUuidOrError.getValue() + + const sharedVaultMemberUuidOrError = Uuid.create(dto.sharedVaultMemberUuid) + if (sharedVaultMemberUuidOrError.isFailed()) { + return Result.fail(sharedVaultMemberUuidOrError.getError()) + } + const sharedVaultMemberUuid = sharedVaultMemberUuidOrError.getValue() + + const response = await this.sharedVaultUserServer.designateSurvivor({ + sharedVaultUuid, + sharedVaultMemberUuid, + }) + + if (response.status !== HttpStatusCode.Success) { + return Result.fail('Failed to mark designated survivor on the server') + } + + return Result.ok() + } +} diff --git a/packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivorDTO.ts b/packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivorDTO.ts new file mode 100644 index 000000000..111fedb8c --- /dev/null +++ b/packages/services/src/Domain/VaultUser/UseCase/DesignateSurvivor/DesignateSurvivorDTO.ts @@ -0,0 +1,4 @@ +export interface DesignateSurvivorDTO { + sharedVaultUuid: string + sharedVaultMemberUuid: string +} diff --git a/packages/services/src/Domain/VaultUser/VaultUserService.ts b/packages/services/src/Domain/VaultUser/VaultUserService.ts index 137ca575b..4f242e1ed 100644 --- a/packages/services/src/Domain/VaultUser/VaultUserService.ts +++ b/packages/services/src/Domain/VaultUser/VaultUserService.ts @@ -14,6 +14,7 @@ import { Result } from '@standardnotes/domain-core' import { IsVaultOwner } from './UseCase/IsVaultOwner' import { IsReadonlyVaultMember } from './UseCase/IsReadonlyVaultMember' import { IsVaultAdmin } from './UseCase/IsVaultAdmin' +import { DesignateSurvivor } from './UseCase/DesignateSurvivor/DesignateSurvivor' export class VaultUserService extends AbstractService implements VaultUserServiceInterface { constructor( @@ -26,6 +27,7 @@ export class VaultUserService extends AbstractService imp private _isReadonlyVaultMember: IsReadonlyVaultMember, private _getVault: GetVault, private _leaveVault: LeaveVault, + private designateSurvivorUseCase: DesignateSurvivor, eventBus: InternalEventBusInterface, ) { super(eventBus) @@ -41,6 +43,19 @@ export class VaultUserService extends AbstractService imp ;(this._leaveVault as unknown) = undefined } + async designateSurvivor(sharedVault: SharedVaultListingInterface, userUuid: string): Promise> { + const result = await this.designateSurvivorUseCase.execute({ + sharedVaultMemberUuid: userUuid, + sharedVaultUuid: sharedVault.sharing.sharedVaultUuid, + }) + + if (result.isFailed()) { + return Result.fail(`Could not designate survivor: ${result.getError()}`) + } + + return Result.ok() + } + async invalidateVaultUsersCache(sharedVaultUuid?: string) { if (sharedVaultUuid) { await this._getVaultUsers.execute({ diff --git a/packages/services/src/Domain/VaultUser/VaultUserServiceInterface.ts b/packages/services/src/Domain/VaultUser/VaultUserServiceInterface.ts index a46b3c23e..524501858 100644 --- a/packages/services/src/Domain/VaultUser/VaultUserServiceInterface.ts +++ b/packages/services/src/Domain/VaultUser/VaultUserServiceInterface.ts @@ -12,6 +12,7 @@ export interface VaultUserServiceInterface extends ApplicationServiceInterface> + designateSurvivor(sharedVault: SharedVaultListingInterface, userUuid: string): Promise> leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise isVaultUserOwner(user: SharedVaultUserServerHash): boolean getFormattedMemberPermission(permission: string): string diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 7fa5380db..ef7fc0712 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -203,6 +203,7 @@ export * from './User/UserServiceInterface' export * from './User/UserServiceInterface' export * from './UserEvent/NotificationService' export * from './UserEvent/NotificationServiceEvent' +export * from './Vault/UseCase/AuthorizeVaultDeletion' export * from './Vault/UseCase/ChangeVaultKeyOptions' export * from './Vault/UseCase/ChangeVaultKeyOptionsDTO' export * from './Vault/UseCase/ChangeVaultStorageMode' @@ -231,9 +232,12 @@ export * from './VaultLock/UseCase/ValidateVaultPassword' export * from './VaultLock/VaultLockService' export * from './VaultLock/VaultLockServiceEvent' export * from './VaultLock/VaultLockServiceInterface' -export * from './VaultUser/UseCase/GetVaultContacts' +export * from './VaultUser/UseCase/DesignateSurvivor/DesignateSurvivor' +export * from './VaultUser/UseCase/DesignateSurvivor/DesignateSurvivorDTO' export * from './VaultUser/UseCase/GetVaultContacts' export * from './VaultUser/UseCase/GetVaultUsers' +export * from './VaultUser/UseCase/IsReadonlyVaultMember' +export * from './VaultUser/UseCase/IsVaultAdmin' export * from './VaultUser/UseCase/IsVaultOwner' export * from './VaultUser/UseCase/LeaveSharedVault' export * from './VaultUser/UseCase/RemoveSharedVaultMember' diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index f3b21687d..4a1536de2 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -141,6 +141,10 @@ import { CreateEncryptedBackupFile, SyncLocalVaultsWithRemoteSharedVaults, WebSocketsService, + AuthorizeVaultDeletion, + IsVaultAdmin, + IsReadonlyVaultMember, + DesignateSurvivor, } from '@standardnotes/services' import { ItemManager } from '../../Services/Items/ItemManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager' @@ -158,6 +162,7 @@ import { SharedVaultInvitesServer, SharedVaultServer, SharedVaultUsersServer, + SharedVaultUsersServerInterface, SubscriptionApiService, SubscriptionServer, UserApiService, @@ -171,9 +176,6 @@ import { Logger, isNotUndefined, isDeinitable, LoggerInterface } from '@standard import { EncryptionOperators } from '@standardnotes/encryption' import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' -import { AuthorizeVaultDeletion } from '@standardnotes/services/src/Domain/Vault/UseCase/AuthorizeVaultDeletion' -import { IsVaultAdmin } from '@standardnotes/services/src/Domain/VaultUser/UseCase/IsVaultAdmin' -import { IsReadonlyVaultMember } from '@standardnotes/services/src/Domain/VaultUser/UseCase/IsReadonlyVaultMember' export class Dependencies { private factory = new Map unknown>() @@ -683,6 +685,10 @@ export class Dependencies { return new RemoveVaultMember(this.get(TYPES.SharedVaultUsersServer)) }) + this.factory.set(TYPES.DesignateSurvivor, () => { + return new DesignateSurvivor(this.get(TYPES.SharedVaultUsersServer)) + }) + this.factory.set(TYPES.GetVaultUsers, () => { return new GetVaultUsers( this.get(TYPES.SharedVaultUsersServer), @@ -861,6 +867,7 @@ export class Dependencies { this.get(TYPES.IsReadonlyVaultMember), this.get(TYPES.GetVault), this.get(TYPES.LeaveVault), + this.get(TYPES.DesignateSurvivor), this.get(TYPES.InternalEventBus), ) }) diff --git a/packages/snjs/lib/Application/Dependencies/Types.ts b/packages/snjs/lib/Application/Dependencies/Types.ts index 5c9a485de..08718bcf6 100644 --- a/packages/snjs/lib/Application/Dependencies/Types.ts +++ b/packages/snjs/lib/Application/Dependencies/Types.ts @@ -125,6 +125,7 @@ export const TYPES = { ConvertToSharedVault: Symbol.for('ConvertToSharedVault'), DeleteSharedVault: Symbol.for('DeleteSharedVault'), RemoveVaultMember: Symbol.for('RemoveVaultMember'), + DesignateSurvivor: Symbol.for('DesignateSurvivor'), GetVaultUsers: Symbol.for('GetSharedVaultUsers'), ResendAllMessages: Symbol.for('ResendAllMessages'), ReuploadAllInvites: Symbol.for('ReuploadAllInvites'),