chore: add method to designate a shared vault survivor (#2538)

This commit is contained in:
Karol Sójko
2023-09-26 13:48:36 +02:00
committed by GitHub
parent 60f96bc201
commit 2ce908da29
12 changed files with 160 additions and 5 deletions

View File

@@ -0,0 +1,3 @@
export type DesignateSurvivorResponse = {
success: boolean
}

View File

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

View File

@@ -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<HttpResponse<DesignateSurvivorResponse>> {
return this.httpService.post(
SharedVaultUsersPaths.designateSurvivor(params.sharedVaultUuid.value, params.sharedVaultMemberUuid.value),
)
}
getSharedVaultUsers(params: GetSharedVaultUsersRequestParams): Promise<HttpResponse<GetSharedVaultUsersResponse>> {
return this.httpService.get(SharedVaultUsersPaths.getSharedVaultUsers(params.sharedVaultUuid))
}

View File

@@ -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<HttpResponse<GetSharedVaultUsersResponse>>
designateSurvivor(params: {
sharedVaultUuid: Uuid
sharedVaultMemberUuid: Uuid
}): Promise<HttpResponse<DesignateSurvivorResponse>>
deleteSharedVaultUser(
params: DeleteSharedVaultUserRequestParams,
): Promise<HttpResponse<DeleteSharedVaultUserResponse>>

View File

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

View File

@@ -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<void> {
constructor(private sharedVaultUserServer: SharedVaultUsersServerInterface) {}
async execute(dto: DesignateSurvivorDTO): Promise<Result<void>> {
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()
}
}

View File

@@ -0,0 +1,4 @@
export interface DesignateSurvivorDTO {
sharedVaultUuid: string
sharedVaultMemberUuid: string
}

View File

@@ -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<VaultUserServiceEvent> implements VaultUserServiceInterface {
constructor(
@@ -26,6 +27,7 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> 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<VaultUserServiceEvent> imp
;(this._leaveVault as unknown) = undefined
}
async designateSurvivor(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>> {
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({

View File

@@ -12,6 +12,7 @@ export interface VaultUserServiceInterface extends ApplicationServiceInterface<V
isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean
isCurrentUserReadonlyVaultMember(vault: VaultListingInterface): boolean
removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>>
designateSurvivor(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>>
leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
isVaultUserOwner(user: SharedVaultUserServerHash): boolean
getFormattedMemberPermission(permission: string): string

View File

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

View File

@@ -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<symbol, () => unknown>()
@@ -683,6 +685,10 @@ export class Dependencies {
return new RemoveVaultMember(this.get<SharedVaultUsersServer>(TYPES.SharedVaultUsersServer))
})
this.factory.set(TYPES.DesignateSurvivor, () => {
return new DesignateSurvivor(this.get<SharedVaultUsersServerInterface>(TYPES.SharedVaultUsersServer))
})
this.factory.set(TYPES.GetVaultUsers, () => {
return new GetVaultUsers(
this.get<SharedVaultUsersServer>(TYPES.SharedVaultUsersServer),
@@ -861,6 +867,7 @@ export class Dependencies {
this.get<IsReadonlyVaultMember>(TYPES.IsReadonlyVaultMember),
this.get<GetVault>(TYPES.GetVault),
this.get<LeaveVault>(TYPES.LeaveVault),
this.get<DesignateSurvivor>(TYPES.DesignateSurvivor),
this.get<InternalEventBus>(TYPES.InternalEventBus),
)
})

View File

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