From 205a1ed637b626be13fc656276508f3c7791024f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Wed, 23 Aug 2023 08:09:34 +0200 Subject: [PATCH] feat: add handling file moving and updating storage quota (#705) * feat: add handling file moving and updating storage quota * fix: getting file metada when moving files * fix: missing event handler binding --- packages/auth/src/Bootstrap/Container.ts | 10 + packages/auth/src/Bootstrap/Service.ts | 1 + packages/auth/src/Bootstrap/Types.ts | 1 + .../SharedVaultFileMovedEventHandler.ts | 28 +++ .../ActivatePremiumFeatures.ts | 2 +- .../ActivatePremiumFeaturesDTO.ts | 1 + .../Domain/Event/SharedVaultFileMovedEvent.ts | 7 + .../Event/SharedVaultFileMovedEventPayload.ts | 14 ++ packages/domain-events/src/Domain/index.ts | 2 + packages/files/src/Bootstrap/Container.ts | 25 ++- .../src/Domain/Event/DomainEventFactory.ts | 34 ++- .../Event/DomainEventFactoryInterface.ts | 15 ++ .../GetFileMetadata/GetFileMetadata.spec.ts | 14 +- .../GetFileMetadata/GetFileMetadata.ts | 26 +-- .../GetFileMetadataResponse.ts | 9 - .../Domain/UseCase/MoveFile/MoveFile.spec.ts | 207 +++++++++++++++++- .../src/Domain/UseCase/MoveFile/MoveFile.ts | 86 +++++++- .../Domain/UseCase/MoveFile/MoveFileDTO.ts | 10 +- .../AnnotatedFilesController.spec.ts | 4 +- .../AnnotatedFilesController.ts | 11 +- .../AnnotatedSharedVaultFilesController.ts | 15 +- packages/home-server/src/Server/HomeServer.ts | 2 + .../src/Server/HomeServerInterface.ts | 1 + .../Domain/Token/SharedVaultValetTokenData.ts | 10 +- .../syncing-server/src/Bootstrap/Container.ts | 14 ++ .../syncing-server/src/Bootstrap/Types.ts | 1 + .../SharedVaultFileMovedEventHandler.ts | 90 ++++++++ .../CreateSharedVaultFileValetToken.spec.ts | 17 ++ .../CreateSharedVaultFileValetToken.ts | 77 ++++--- .../CreateSharedVaultFileValetTokenDTO.ts | 1 + 30 files changed, 624 insertions(+), 111 deletions(-) create mode 100644 packages/auth/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts create mode 100644 packages/domain-events/src/Domain/Event/SharedVaultFileMovedEvent.ts create mode 100644 packages/domain-events/src/Domain/Event/SharedVaultFileMovedEventPayload.ts delete mode 100644 packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataResponse.ts create mode 100644 packages/syncing-server/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index d82d2d2be..ce0dbdb2f 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -256,6 +256,7 @@ import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAc import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser' import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler' import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler' +import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler' export class ContainerConfigLoader { async load(configuration?: { @@ -982,6 +983,14 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_Logger), ), ) + container + .bind(TYPES.Auth_SharedVaultFileMovedEventHandler) + .toConstantValue( + new SharedVaultFileMovedEventHandler( + container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser), + container.get(TYPES.Auth_Logger), + ), + ) container .bind(TYPES.Auth_FileRemovedEventHandler) .toConstantValue( @@ -1045,6 +1054,7 @@ export class ContainerConfigLoader { ['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)], ['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)], ['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)], + ['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)], ['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)], ['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)], ['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)], diff --git a/packages/auth/src/Bootstrap/Service.ts b/packages/auth/src/Bootstrap/Service.ts index 037d73482..8c6594660 100644 --- a/packages/auth/src/Bootstrap/Service.ts +++ b/packages/auth/src/Bootstrap/Service.ts @@ -27,6 +27,7 @@ export class Service implements AuthServiceInterface { async activatePremiumFeatures(dto: { username: string subscriptionPlanName?: string + uploadBytesLimit?: number endsAt?: Date }): Promise> { if (!this.container) { diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index d845c95ee..21b366479 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -168,6 +168,7 @@ const TYPES = { Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'), Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'), Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'), + Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'), Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'), Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'), Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'), diff --git a/packages/auth/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts b/packages/auth/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts new file mode 100644 index 000000000..4e3917010 --- /dev/null +++ b/packages/auth/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts @@ -0,0 +1,28 @@ +import { DomainEventHandlerInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events' +import { Logger } from 'winston' + +import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser' + +export class SharedVaultFileMovedEventHandler implements DomainEventHandlerInterface { + constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {} + + async handle(event: SharedVaultFileMovedEvent): Promise { + const subtractResult = await this.updateStorageQuotaUsedForUserUseCase.execute({ + userUuid: event.payload.from.ownerUuid, + bytesUsed: -event.payload.fileByteSize, + }) + + if (subtractResult.isFailed()) { + this.logger.error(`Failed to update storage quota used for user: ${subtractResult.getError()}`) + } + + const addResult = await this.updateStorageQuotaUsedForUserUseCase.execute({ + userUuid: event.payload.to.ownerUuid, + bytesUsed: event.payload.fileByteSize, + }) + + if (addResult.isFailed()) { + this.logger.error(`Failed to update storage quota used for user: ${addResult.getError()}`) + } + } +} diff --git a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts index 54e468f8b..c21c9f134 100644 --- a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts +++ b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts @@ -57,7 +57,7 @@ export class ActivatePremiumFeatures implements UseCaseInterface { await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription( subscription, - new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]), + new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]), ) return Result.ok('Premium features activated.') diff --git a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeaturesDTO.ts b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeaturesDTO.ts index 711bf0ec5..d72a92469 100644 --- a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeaturesDTO.ts +++ b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeaturesDTO.ts @@ -1,5 +1,6 @@ export interface ActivatePremiumFeaturesDTO { username: string subscriptionPlanName?: string + uploadBytesLimit?: number endsAt?: Date } diff --git a/packages/domain-events/src/Domain/Event/SharedVaultFileMovedEvent.ts b/packages/domain-events/src/Domain/Event/SharedVaultFileMovedEvent.ts new file mode 100644 index 000000000..ce2ecd3e7 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/SharedVaultFileMovedEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { SharedVaultFileMovedEventPayload } from './SharedVaultFileMovedEventPayload' + +export interface SharedVaultFileMovedEvent extends DomainEventInterface { + type: 'SHARED_VAULT_FILE_MOVED' + payload: SharedVaultFileMovedEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/SharedVaultFileMovedEventPayload.ts b/packages/domain-events/src/Domain/Event/SharedVaultFileMovedEventPayload.ts new file mode 100644 index 000000000..f88a8156a --- /dev/null +++ b/packages/domain-events/src/Domain/Event/SharedVaultFileMovedEventPayload.ts @@ -0,0 +1,14 @@ +export interface SharedVaultFileMovedEventPayload { + fileByteSize: number + fileName: string + from: { + sharedVaultUuid?: string + ownerUuid: string + filePath: string + } + to: { + sharedVaultUuid?: string + ownerUuid: string + filePath: string + } +} diff --git a/packages/domain-events/src/Domain/index.ts b/packages/domain-events/src/Domain/index.ts index 577e35f39..fc6461cd7 100644 --- a/packages/domain-events/src/Domain/index.ts +++ b/packages/domain-events/src/Domain/index.ts @@ -64,6 +64,8 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent' export * from './Event/SharedSubscriptionInvitationCanceledEventPayload' export * from './Event/SharedSubscriptionInvitationCreatedEvent' export * from './Event/SharedSubscriptionInvitationCreatedEventPayload' +export * from './Event/SharedVaultFileMovedEvent' +export * from './Event/SharedVaultFileMovedEventPayload' export * from './Event/SharedVaultFileRemovedEvent' export * from './Event/SharedVaultFileRemovedEventPayload' export * from './Event/SharedVaultFileUploadedEvent' diff --git a/packages/files/src/Bootstrap/Container.ts b/packages/files/src/Bootstrap/Container.ts index 4dd0b4be9..d179d1631 100644 --- a/packages/files/src/Bootstrap/Container.ts +++ b/packages/files/src/Bootstrap/Container.ts @@ -99,7 +99,9 @@ export class ContainerConfigLoader { container .bind>(TYPES.Files_ValetTokenDecoder) .toConstantValue(new TokenDecoder(container.get(TYPES.Files_VALET_TOKEN_SECRET))) - container.bind(TYPES.Files_DomainEventFactory).to(DomainEventFactory) + container + .bind(TYPES.Files_DomainEventFactory) + .toConstantValue(new DomainEventFactory(container.get(TYPES.Files_Timer))) if (isConfiguredForInMemoryCache) { container @@ -214,9 +216,26 @@ export class ContainerConfigLoader { container.get(TYPES.Files_DomainEventFactory), ), ) - container.bind(TYPES.Files_GetFileMetadata).to(GetFileMetadata) + container + .bind(TYPES.Files_GetFileMetadata) + .toConstantValue( + new GetFileMetadata( + container.get(TYPES.Files_FileDownloader), + container.get(TYPES.Files_Logger), + ), + ) container.bind(TYPES.Files_RemoveFile).to(RemoveFile) - container.bind(TYPES.Files_MoveFile).to(MoveFile) + container + .bind(TYPES.Files_MoveFile) + .toConstantValue( + new MoveFile( + container.get(TYPES.Files_GetFileMetadata), + container.get(TYPES.Files_FileMover), + container.get(TYPES.Files_DomainEventPublisher), + container.get(TYPES.Files_DomainEventFactory), + container.get(TYPES.Files_Logger), + ), + ) container.bind(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved) // middleware diff --git a/packages/files/src/Domain/Event/DomainEventFactory.ts b/packages/files/src/Domain/Event/DomainEventFactory.ts index e5b523c68..326c79025 100644 --- a/packages/files/src/Domain/Event/DomainEventFactory.ts +++ b/packages/files/src/Domain/Event/DomainEventFactory.ts @@ -4,16 +4,14 @@ import { DomainEventService, SharedVaultFileUploadedEvent, SharedVaultFileRemovedEvent, + SharedVaultFileMovedEvent, } from '@standardnotes/domain-events' import { TimerInterface } from '@standardnotes/time' -import { inject, injectable } from 'inversify' -import TYPES from '../../Bootstrap/Types' import { DomainEventFactoryInterface } from './DomainEventFactoryInterface' -@injectable() export class DomainEventFactory implements DomainEventFactoryInterface { - constructor(@inject(TYPES.Files_Timer) private timer: TimerInterface) {} + constructor(private timer: TimerInterface) {} createFileRemovedEvent(payload: { userUuid: string @@ -56,6 +54,34 @@ export class DomainEventFactory implements DomainEventFactoryInterface { } } + createSharedVaultFileMovedEvent(payload: { + fileByteSize: number + fileName: string + from: { + sharedVaultUuid?: string + ownerUuid: string + filePath: string + } + to: { + sharedVaultUuid?: string + ownerUuid: string + filePath: string + } + }): SharedVaultFileMovedEvent { + return { + type: 'SHARED_VAULT_FILE_MOVED', + createdAt: this.timer.getUTCDate(), + meta: { + correlation: { + userIdentifier: payload.from.sharedVaultUuid ?? payload.from.ownerUuid, + userIdentifierType: payload.from.sharedVaultUuid ? 'shared-vault-uuid' : 'uuid', + }, + origin: DomainEventService.Files, + }, + payload, + } + } + createSharedVaultFileUploadedEvent(payload: { sharedVaultUuid: string vaultOwnerUuid: string diff --git a/packages/files/src/Domain/Event/DomainEventFactoryInterface.ts b/packages/files/src/Domain/Event/DomainEventFactoryInterface.ts index 9d4faf8fb..423c377af 100644 --- a/packages/files/src/Domain/Event/DomainEventFactoryInterface.ts +++ b/packages/files/src/Domain/Event/DomainEventFactoryInterface.ts @@ -3,6 +3,7 @@ import { FileRemovedEvent, SharedVaultFileRemovedEvent, SharedVaultFileUploadedEvent, + SharedVaultFileMovedEvent, } from '@standardnotes/domain-events' export interface DomainEventFactoryInterface { @@ -19,6 +20,20 @@ export interface DomainEventFactoryInterface { fileByteSize: number regularSubscriptionUuid: string }): FileRemovedEvent + createSharedVaultFileMovedEvent(payload: { + fileByteSize: number + fileName: string + from: { + sharedVaultUuid?: string + ownerUuid: string + filePath: string + } + to: { + sharedVaultUuid?: string + ownerUuid: string + filePath: string + } + }): SharedVaultFileMovedEvent createSharedVaultFileUploadedEvent(payload: { sharedVaultUuid: string vaultOwnerUuid: string diff --git a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.spec.ts b/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.spec.ts index eb105fd2e..cb2de9b04 100644 --- a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.spec.ts +++ b/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.spec.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata' import { Logger } from 'winston' import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface' @@ -19,10 +18,8 @@ describe('GetFileMetadata', () => { }) it('should return the file metadata', async () => { - expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({ - success: true, - size: 123, - }) + const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' }) + expect(result.getValue()).toEqual(123) }) it('should not return the file metadata if it fails', async () => { @@ -30,9 +27,8 @@ describe('GetFileMetadata', () => { throw new Error('ooops') }) - expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({ - success: false, - message: 'Could not get file metadata.', - }) + const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' }) + + expect(result.isFailed()).toBe(true) }) }) diff --git a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts b/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts index ab38b1fd4..1be579936 100644 --- a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts +++ b/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts @@ -1,32 +1,20 @@ -import { inject, injectable } from 'inversify' import { Logger } from 'winston' -import TYPES from '../../../Bootstrap/Types' import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface' -import { UseCaseInterface } from '../UseCaseInterface' import { GetFileMetadataDTO } from './GetFileMetadataDTO' -import { GetFileMetadataResponse } from './GetFileMetadataResponse' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' -@injectable() -export class GetFileMetadata implements UseCaseInterface { - constructor( - @inject(TYPES.Files_FileDownloader) private fileDownloader: FileDownloaderInterface, - @inject(TYPES.Files_Logger) private logger: Logger, - ) {} +export class GetFileMetadata implements UseCaseInterface { + constructor(private fileDownloader: FileDownloaderInterface, private logger: Logger) {} - async execute(dto: GetFileMetadataDTO): Promise { + async execute(dto: GetFileMetadataDTO): Promise> { try { const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`) - return { - success: true, - size, - } + return Result.ok(size) } catch (error) { this.logger.error(`Could not get file metadata for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`) - return { - success: false, - message: 'Could not get file metadata.', - } + + return Result.fail('Could not get file metadata') } } } diff --git a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataResponse.ts b/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataResponse.ts deleted file mode 100644 index a1f18812a..000000000 --- a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type GetFileMetadataResponse = - | { - success: true - size: number - } - | { - success: false - message: string - } diff --git a/packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts b/packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts index 5febb3fdd..43e3dc766 100644 --- a/packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts +++ b/packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts @@ -1,18 +1,27 @@ -import 'reflect-metadata' - +import { DomainEventPublisherInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events' +import { Result } from '@standardnotes/domain-core' import { Logger } from 'winston' import { MoveFile } from './MoveFile' import { FileMoverInterface } from '../../Services/FileMoverInterface' +import { GetFileMetadata } from '../GetFileMetadata/GetFileMetadata' +import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface' describe('MoveFile', () => { let fileMover: FileMoverInterface + let getFileMetadataUseCase: GetFileMetadata + let domainEventPublisher: DomainEventPublisherInterface + let domainEventFactory: DomainEventFactoryInterface let logger: Logger - const createUseCase = () => new MoveFile(fileMover, logger) + const createUseCase = () => + new MoveFile(getFileMetadataUseCase, fileMover, domainEventPublisher, domainEventFactory, logger) beforeEach(() => { + getFileMetadataUseCase = {} as jest.Mocked + getFileMetadataUseCase.execute = jest.fn().mockReturnValue(Result.ok(1234)) + fileMover = {} as jest.Mocked fileMover.moveFile = jest.fn().mockReturnValue(413) @@ -20,17 +29,34 @@ describe('MoveFile', () => { logger.debug = jest.fn() logger.error = jest.fn() logger.warn = jest.fn() + + domainEventPublisher = {} as jest.Mocked + domainEventPublisher.publish = jest.fn() + + domainEventFactory = {} as jest.Mocked + domainEventFactory.createSharedVaultFileMovedEvent = jest + .fn() + .mockReturnValue({} as jest.Mocked) }) it('should move a file', async () => { await createUseCase().execute({ resourceRemoteIdentifier: '2-3-4', - fromUuid: '1-2-3', - toUuid: '4-5-6', - moveType: 'shared-vault-to-user', + from: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + to: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000001', + ownerUuid: '00000000-0000-0000-0000-000000000001', + }, + moveType: 'shared-vault-to-shared-vault', }) - expect(fileMover.moveFile).toHaveBeenCalledWith('1-2-3/2-3-4', '4-5-6/2-3-4') + expect(fileMover.moveFile).toHaveBeenCalledWith( + '00000000-0000-0000-0000-000000000000/2-3-4', + '00000000-0000-0000-0000-000000000001/2-3-4', + ) }) it('should indicate an error if moving fails', async () => { @@ -40,11 +66,174 @@ describe('MoveFile', () => { const result = await createUseCase().execute({ resourceRemoteIdentifier: '2-3-4', - fromUuid: '1-2-3', - toUuid: '4-5-6', + from: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + to: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000001', + ownerUuid: '00000000-0000-0000-0000-000000000001', + }, + moveType: 'shared-vault-to-shared-vault', + }) + + expect(result.isFailed()).toEqual(true) + }) + + it('should return an error if the from shared vault uuid is invalid', async () => { + const result = await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + from: { + sharedVaultUuid: 'invalid', + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + to: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000001', + ownerUuid: '00000000-0000-0000-0000-000000000001', + }, + moveType: 'shared-vault-to-shared-vault', + }) + + expect(result.isFailed()).toEqual(true) + }) + + it('should return an error if the to shared vault uuid is invalid', async () => { + const result = await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + from: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + to: { + sharedVaultUuid: 'invalid', + ownerUuid: '00000000-0000-0000-0000-000000000001', + }, + moveType: 'shared-vault-to-shared-vault', + }) + + expect(result.isFailed()).toEqual(true) + }) + + it('should return an error if the from owner uuid is invalid', async () => { + const result = await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + from: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + ownerUuid: 'invalid', + }, + to: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000001', + ownerUuid: '00000000-0000-0000-0000-000000000001', + }, + moveType: 'shared-vault-to-shared-vault', + }) + + expect(result.isFailed()).toEqual(true) + }) + + it('should return an error if the to owner uuid is invalid', async () => { + const result = await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + from: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + to: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000001', + ownerUuid: 'invalid', + }, + moveType: 'shared-vault-to-shared-vault', + }) + + expect(result.isFailed()).toEqual(true) + }) + + it('should return an error if the file metadata cannot be retrieved', async () => { + getFileMetadataUseCase.execute = jest.fn().mockReturnValue(Result.fail('oops')) + + const result = await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + from: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + to: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000001', + ownerUuid: '00000000-0000-0000-0000-000000000001', + }, + moveType: 'shared-vault-to-shared-vault', + }) + + expect(result.isFailed()).toEqual(true) + }) + + it('should move file from user to shared vault', async () => { + const result = await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + from: { + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + to: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000001', + ownerUuid: '00000000-0000-0000-0000-000000000002', + }, + moveType: 'user-to-shared-vault', + }) + + expect(fileMover.moveFile).toHaveBeenCalledWith( + '00000000-0000-0000-0000-000000000000/2-3-4', + '00000000-0000-0000-0000-000000000001/2-3-4', + ) + expect(result.isFailed()).toEqual(false) + }) + + it('should move file from shared vault to user', async () => { + const result = await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + from: { + sharedVaultUuid: '00000000-0000-0000-0000-000000000001', + ownerUuid: '00000000-0000-0000-0000-000000000002', + }, + to: { + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + moveType: 'shared-vault-to-user', + }) + + expect(fileMover.moveFile).toHaveBeenCalledWith( + '00000000-0000-0000-0000-000000000001/2-3-4', + '00000000-0000-0000-0000-000000000000/2-3-4', + ) + expect(result.isFailed()).toEqual(false) + }) + + it('should fail if moving from shared vault to user without shared vault uuid', async () => { + const result = await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + from: { + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + to: { + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, moveType: 'shared-vault-to-user', }) expect(result.isFailed()).toEqual(true) }) + + it('should fail if moving from user to shared vault without shared vault uuid', async () => { + const result = await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + from: { + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + to: { + ownerUuid: '00000000-0000-0000-0000-000000000000', + }, + moveType: 'user-to-shared-vault', + }) + + expect(result.isFailed()).toEqual(true) + }) }) diff --git a/packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts b/packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts index a4e594a7c..cd0090ee5 100644 --- a/packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts +++ b/packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts @@ -1,27 +1,97 @@ -import { inject, injectable } from 'inversify' import { Logger } from 'winston' +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { DomainEventPublisherInterface } from '@standardnotes/domain-events' -import TYPES from '../../../Bootstrap/Types' import { FileMoverInterface } from '../../Services/FileMoverInterface' import { MoveFileDTO } from './MoveFileDTO' -import { Result, UseCaseInterface } from '@standardnotes/domain-core' +import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface' +import { GetFileMetadata } from '../GetFileMetadata/GetFileMetadata' -@injectable() export class MoveFile implements UseCaseInterface { constructor( - @inject(TYPES.Files_FileMover) private fileMover: FileMoverInterface, - @inject(TYPES.Files_Logger) private logger: Logger, + private getFileMetadataUseCase: GetFileMetadata, + private fileMover: FileMoverInterface, + private domainEventPublisher: DomainEventPublisherInterface, + private domainEventFactory: DomainEventFactoryInterface, + private logger: Logger, ) {} async execute(dto: MoveFileDTO): Promise> { try { - const srcPath = `${dto.fromUuid}/${dto.resourceRemoteIdentifier}` - const destPath = `${dto.toUuid}/${dto.resourceRemoteIdentifier}` + let fromSharedVaultUuid: Uuid | undefined = undefined + if (dto.from.sharedVaultUuid !== undefined) { + const fromSharedVaultUuidOrError = Uuid.create(dto.from.sharedVaultUuid) + if (fromSharedVaultUuidOrError.isFailed()) { + return Result.fail(fromSharedVaultUuidOrError.getError()) + } + fromSharedVaultUuid = fromSharedVaultUuidOrError.getValue() + } + + let toSharedVaultUuid: Uuid | undefined = undefined + if (dto.to.sharedVaultUuid !== undefined) { + const toSharedVaultUuidOrError = Uuid.create(dto.to.sharedVaultUuid) + if (toSharedVaultUuidOrError.isFailed()) { + return Result.fail(toSharedVaultUuidOrError.getError()) + } + toSharedVaultUuid = toSharedVaultUuidOrError.getValue() + } + + const fromOwnerUuidOrError = Uuid.create(dto.from.ownerUuid) + if (fromOwnerUuidOrError.isFailed()) { + return Result.fail(fromOwnerUuidOrError.getError()) + } + const fromOwnerUuid = fromOwnerUuidOrError.getValue() + + const toOwnerUuidOrError = Uuid.create(dto.to.ownerUuid) + if (toOwnerUuidOrError.isFailed()) { + return Result.fail(toOwnerUuidOrError.getError()) + } + const toOwnerUuid = toOwnerUuidOrError.getValue() + + if (['shared-vault-to-shared-vault', 'shared-vault-to-user'].includes(dto.moveType) && !fromSharedVaultUuid) { + return Result.fail('Source shared vault UUID is required') + } + + if (['user-to-shared-vault', 'shared-vault-to-shared-vault'].includes(dto.moveType) && !toSharedVaultUuid) { + return Result.fail('Target shared vault UUID is required') + } + + const fromUuid = dto.moveType === 'user-to-shared-vault' ? fromOwnerUuid.value : fromSharedVaultUuid?.value + const toUuid = dto.moveType === 'shared-vault-to-user' ? toOwnerUuid.value : toSharedVaultUuid?.value + + const srcPath = `${fromUuid}/${dto.resourceRemoteIdentifier}` + const destPath = `${toUuid}/${dto.resourceRemoteIdentifier}` this.logger.debug(`Moving file from ${srcPath} to ${destPath}`) + const metadataResultOrError = await this.getFileMetadataUseCase.execute({ + resourceRemoteIdentifier: dto.resourceRemoteIdentifier, + ownerUuid: fromUuid as string, + }) + if (metadataResultOrError.isFailed()) { + return Result.fail(metadataResultOrError.getError()) + } + const fileSize = metadataResultOrError.getValue() + await this.fileMover.moveFile(srcPath, destPath) + await this.domainEventPublisher.publish( + this.domainEventFactory.createSharedVaultFileMovedEvent({ + fileByteSize: fileSize, + fileName: dto.resourceRemoteIdentifier, + from: { + sharedVaultUuid: fromSharedVaultUuid?.value, + ownerUuid: fromOwnerUuid.value, + filePath: srcPath, + }, + to: { + sharedVaultUuid: toSharedVaultUuid?.value, + ownerUuid: toOwnerUuid.value, + filePath: destPath, + }, + }), + ) + return Result.ok() } catch (error) { this.logger.error(`Could not move resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`) diff --git a/packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts b/packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts index dfd7ee189..308f4d7b8 100644 --- a/packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts +++ b/packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts @@ -2,7 +2,13 @@ import { SharedVaultMoveType } from '@standardnotes/security' export interface MoveFileDTO { moveType: SharedVaultMoveType - fromUuid: string - toUuid: string + from: { + sharedVaultUuid?: string + ownerUuid: string + } + to: { + sharedVaultUuid?: string + ownerUuid: string + } resourceRemoteIdentifier: string } diff --git a/packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.spec.ts b/packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.spec.ts index e9a6b84d8..511f7245d 100644 --- a/packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.spec.ts +++ b/packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.spec.ts @@ -61,7 +61,7 @@ describe('AnnotatedFilesController', () => { finishUploadSession.execute = jest.fn().mockReturnValue(Result.ok()) getFileMetadata = {} as jest.Mocked - getFileMetadata.execute = jest.fn().mockReturnValue({ success: true, size: 555_555 }) + getFileMetadata.execute = jest.fn().mockReturnValue(Result.ok(555_555)) removeFile = {} as jest.Mocked removeFile.execute = jest.fn().mockReturnValue(Result.ok()) @@ -183,7 +183,7 @@ describe('AnnotatedFilesController', () => { request.headers['range'] = 'bytes=0-' - getFileMetadata.execute = jest.fn().mockReturnValue({ success: false, message: 'error' }) + getFileMetadata.execute = jest.fn().mockReturnValue(Result.fail('error')) const httpResponse = await createController().download(request, response) diff --git a/packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.ts b/packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.ts index 3ac033f25..305f17ff3 100644 --- a/packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.ts +++ b/packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.ts @@ -146,20 +146,21 @@ export class AnnotatedFilesController extends BaseHttpController { chunkSize = this.maxChunkBytes } - const fileMetadata = await this.getFileMetadata.execute({ + const fileMetadataOrError = await this.getFileMetadata.execute({ ownerUuid: response.locals.userUuid, resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier, }) - if (!fileMetadata.success) { - return this.badRequest(fileMetadata.message) + if (fileMetadataOrError.isFailed()) { + return this.badRequest(fileMetadataOrError.getError()) } + const fileSize = fileMetadataOrError.getValue() const startRange = Number(range.replace(/\D/g, '')) - const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1) + const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1) const headers = { - 'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`, + 'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': endRange - startRange + 1, 'Content-Type': 'application/octet-stream', diff --git a/packages/files/src/Infra/InversifyExpress/AnnotatedSharedVaultFilesController.ts b/packages/files/src/Infra/InversifyExpress/AnnotatedSharedVaultFilesController.ts index 07f6d7340..c8c1914db 100644 --- a/packages/files/src/Infra/InversifyExpress/AnnotatedSharedVaultFilesController.ts +++ b/packages/files/src/Infra/InversifyExpress/AnnotatedSharedVaultFilesController.ts @@ -47,8 +47,8 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController { const result = await this.moveFile.execute({ moveType: moveOperation.type, - fromUuid: moveOperation.fromUuid, - toUuid: moveOperation.toUuid, + from: moveOperation.from, + to: moveOperation.to, resourceRemoteIdentifier: locals.remoteIdentifier, }) @@ -187,20 +187,21 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController { chunkSize = this.maxChunkBytes } - const fileMetadata = await this.getFileMetadata.execute({ + const fileMetadataOrError = await this.getFileMetadata.execute({ ownerUuid: locals.sharedVaultUuid, resourceRemoteIdentifier: locals.remoteIdentifier, }) - if (!fileMetadata.success) { - return this.badRequest(fileMetadata.message) + if (fileMetadataOrError.isFailed()) { + return this.badRequest(fileMetadataOrError.getError()) } + const fileSize = fileMetadataOrError.getValue() const startRange = Number(range.replace(/\D/g, '')) - const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1) + const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1) const headers = { - 'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`, + 'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': endRange - startRange + 1, 'Content-Type': 'application/octet-stream', diff --git a/packages/home-server/src/Server/HomeServer.ts b/packages/home-server/src/Server/HomeServer.ts index 04e29f2f1..820fe8be5 100644 --- a/packages/home-server/src/Server/HomeServer.ts +++ b/packages/home-server/src/Server/HomeServer.ts @@ -144,6 +144,7 @@ export class HomeServer implements HomeServerInterface { void this.activatePremiumFeatures({ username: request.body.username, subscriptionPlanName: request.body.subscriptionPlanName, + uploadBytesLimit: request.body.uploadBytesLimit, endsAt: request.body.endsAt ? new Date(request.body.endsAt) : undefined, }).then((result) => { if (result.isFailed()) { @@ -221,6 +222,7 @@ export class HomeServer implements HomeServerInterface { async activatePremiumFeatures(dto: { username: string subscriptionPlanName?: string + uploadBytesLimit?: number endsAt?: Date }): Promise> { if (!this.isRunning() || !this.authService) { diff --git a/packages/home-server/src/Server/HomeServerInterface.ts b/packages/home-server/src/Server/HomeServerInterface.ts index 95908808c..7201ce084 100644 --- a/packages/home-server/src/Server/HomeServerInterface.ts +++ b/packages/home-server/src/Server/HomeServerInterface.ts @@ -6,6 +6,7 @@ export interface HomeServerInterface { activatePremiumFeatures(dto: { username: string subscriptionPlanName?: string + uploadBytesLimit?: number endsAt?: Date }): Promise> stop(): Promise> diff --git a/packages/security/src/Domain/Token/SharedVaultValetTokenData.ts b/packages/security/src/Domain/Token/SharedVaultValetTokenData.ts index eb3f3953b..db5358f74 100644 --- a/packages/security/src/Domain/Token/SharedVaultValetTokenData.ts +++ b/packages/security/src/Domain/Token/SharedVaultValetTokenData.ts @@ -11,7 +11,13 @@ export interface SharedVaultValetTokenData { uploadBytesLimit?: number moveOperation?: { type: SharedVaultMoveType - fromUuid: string - toUuid: string + from: { + sharedVaultUuid?: string + ownerUuid: string + } + to: { + sharedVaultUuid?: string + ownerUuid: string + } } } diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index 0367902fd..749b3ec07 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/packages/syncing-server/src/Bootstrap/Container.ts @@ -155,6 +155,7 @@ import { Logger } from 'winston' import { ItemRepositoryResolverInterface } from '../Domain/Item/ItemRepositoryResolverInterface' import { TypeORMItemRepositoryResolver } from '../Infra/TypeORM/TypeORMItemRepositoryResolver' import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser' +import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler' export class ContainerConfigLoader { private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 @@ -872,6 +873,15 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_Logger), ), ) + container + .bind(TYPES.Sync_SharedVaultFileMovedEventHandler) + .toConstantValue( + new SharedVaultFileMovedEventHandler( + container.get(TYPES.Sync_UpdateStorageQuotaUsedInSharedVault), + container.get(TYPES.Sync_AddNotificationsForUsers), + container.get(TYPES.Sync_Logger), + ), + ) // Services container.bind(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder()) @@ -902,6 +912,10 @@ export class ContainerConfigLoader { 'SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Sync_SharedVaultFileRemovedEventHandler), ], + [ + 'SHARED_VAULT_FILE_MOVED', + container.get(TYPES.Sync_SharedVaultFileMovedEventHandler), + ], ]) if (!isConfiguredForHomeServer) { container.bind(TYPES.Sync_AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL')) diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index bd57efe8a..471acd327 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -90,6 +90,7 @@ const TYPES = { Sync_ItemRevisionCreationRequestedEventHandler: Symbol.for('Sync_ItemRevisionCreationRequestedEventHandler'), Sync_SharedVaultFileRemovedEventHandler: Symbol.for('Sync_SharedVaultFileRemovedEventHandler'), Sync_SharedVaultFileUploadedEventHandler: Symbol.for('Sync_SharedVaultFileUploadedEventHandler'), + Sync_SharedVaultFileMovedEventHandler: Symbol.for('Sync_SharedVaultFileMovedEventHandler'), // Services Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'), Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'), diff --git a/packages/syncing-server/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts new file mode 100644 index 000000000..307c15fbe --- /dev/null +++ b/packages/syncing-server/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts @@ -0,0 +1,90 @@ +import { DomainEventHandlerInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events' +import { NotificationPayload, NotificationType, Uuid } from '@standardnotes/domain-core' +import { Logger } from 'winston' + +import { UpdateStorageQuotaUsedInSharedVault } from '../UseCase/SharedVaults/UpdateStorageQuotaUsedInSharedVault/UpdateStorageQuotaUsedInSharedVault' +import { AddNotificationsForUsers } from '../UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers' + +export class SharedVaultFileMovedEventHandler implements DomainEventHandlerInterface { + constructor( + private updateStorageQuotaUsedInSharedVaultUseCase: UpdateStorageQuotaUsedInSharedVault, + private addNotificationsForUsers: AddNotificationsForUsers, + private logger: Logger, + ) {} + + async handle(event: SharedVaultFileMovedEvent): Promise { + if (event.payload.from.sharedVaultUuid !== undefined) { + const sharedVaultUuidOrError = Uuid.create(event.payload.from.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + this.logger.error(sharedVaultUuidOrError.getError()) + + return + } + const sharedVaultUuid = sharedVaultUuidOrError.getValue() + + const subtractResult = await this.updateStorageQuotaUsedInSharedVaultUseCase.execute({ + sharedVaultUuid: sharedVaultUuid.value, + bytesUsed: -event.payload.fileByteSize, + }) + + if (subtractResult.isFailed()) { + this.logger.error(`Failed to update storage quota used in shared vault: ${subtractResult.getError()}`) + + return + } + + const notificationPayload = NotificationPayload.create({ + sharedVaultUuid: sharedVaultUuid, + type: NotificationType.create(NotificationType.TYPES.SharedVaultFileRemoved).getValue(), + version: '1.0', + }).getValue() + + const notificationResult = await this.addNotificationsForUsers.execute({ + sharedVaultUuid: sharedVaultUuid.value, + type: NotificationType.TYPES.SharedVaultFileRemoved, + payload: notificationPayload, + version: '1.0', + }) + if (notificationResult.isFailed()) { + this.logger.error(`Failed to add notification for users: ${notificationResult.getError()}`) + } + } + + if (event.payload.to.sharedVaultUuid !== undefined) { + const sharedVaultUuidOrError = Uuid.create(event.payload.to.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + this.logger.error(sharedVaultUuidOrError.getError()) + + return + } + const sharedVaultUuid = sharedVaultUuidOrError.getValue() + + const addResult = await this.updateStorageQuotaUsedInSharedVaultUseCase.execute({ + sharedVaultUuid: sharedVaultUuid.value, + bytesUsed: event.payload.fileByteSize, + }) + + if (addResult.isFailed()) { + this.logger.error(`Failed to update storage quota used in shared vault: ${addResult.getError()}`) + + return + } + + const notificationPayload = NotificationPayload.create({ + sharedVaultUuid: sharedVaultUuid, + type: NotificationType.create(NotificationType.TYPES.SharedVaultFileUploaded).getValue(), + version: '1.0', + }).getValue() + + const notificationResult = await this.addNotificationsForUsers.execute({ + sharedVaultUuid: sharedVaultUuid.value, + type: NotificationType.TYPES.SharedVaultFileUploaded, + payload: notificationPayload, + version: '1.0', + }) + if (notificationResult.isFailed()) { + this.logger.error(`Failed to add notification for users: ${notificationResult.getError()}`) + } + } + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts index 71e20b5d4..3d023893f 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts @@ -256,6 +256,23 @@ describe('CreateSharedVaultFileValetToken', () => { expect(result.getError()).toBe('User does not have permission to perform this operation') }) + it('should return error when target shared vault does not exist for shared-vault-to-shared-vault move operation', async () => { + sharedVaultRepository.findByUuid = jest.fn().mockResolvedValueOnce(sharedVault).mockResolvedValueOnce(null) + + const useCase = createUseCase() + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + remoteIdentifier: 'remote-identifier', + operation: ValetTokenOperation.Move, + moveOperationType: 'shared-vault-to-shared-vault', + sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Target shared vault not found') + }) + it('should create move valet token for shared-vault-to-shared-vault operation', async () => { sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest .fn() diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.ts index 1f0a4d4a9..04fac40be 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.ts @@ -1,9 +1,15 @@ -import { SharedVaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security' +import { + SharedVaultMoveType, + SharedVaultValetTokenData, + TokenEncoderInterface, + ValetTokenOperation, +} from '@standardnotes/security' import { Result, SharedVaultUserPermission, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { CreateSharedVaultFileValetTokenDTO } from './CreateSharedVaultFileValetTokenDTO' +import { SharedVault } from '../../../SharedVault/SharedVault' export class CreateSharedVaultFileValetToken implements UseCaseInterface { constructor( @@ -48,6 +54,7 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface return Result.fail('User does not have permission to perform this operation') } + let targetSharedVault: SharedVault | null = null if (dto.operation === ValetTokenOperation.Move) { if (!dto.moveOperationType) { return Result.fail('Move operation type is required') @@ -64,6 +71,11 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface } const sharedVaultTargetUuid = sharedVaultTargetUuidOrError.getValue() + targetSharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultTargetUuid) + if (!targetSharedVault) { + return Result.fail('Target shared vault not found') + } + const toSharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({ userUuid: userUuid, sharedVaultUuid: sharedVaultTargetUuid, @@ -83,6 +95,28 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface } } + const fromSharedVaultUuid = ['shared-vault-to-user', 'shared-vault-to-shared-vault'].includes( + dto.moveOperationType as string, + ) + ? sharedVaultUuid.value + : undefined + + const fromOwnerUuid = + dto.moveOperationType === 'user-to-shared-vault' ? userUuid.value : sharedVault.props.userUuid.value + + const toSharedVaultUuid = targetSharedVault + ? targetSharedVault.id.toString() + : dto.moveOperationType === 'shared-vault-to-user' + ? undefined + : sharedVaultUuid.value + + const toOwnerUuid = + dto.moveOperationType === 'user-to-shared-vault' + ? sharedVault.props.userUuid.value + : targetSharedVault + ? targetSharedVault.props.userUuid.value + : userUuid.value + const tokenData: SharedVaultValetTokenData = { sharedVaultUuid: dto.sharedVaultUuid, vaultOwnerUuid: sharedVault.props.userUuid.value, @@ -91,40 +125,21 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface uploadBytesUsed: sharedVault.props.fileUploadBytesUsed, uploadBytesLimit: dto.sharedVaultOwnerUploadBytesLimit, unencryptedFileSize: dto.unencryptedFileSize, - moveOperation: this.createMoveOperationData(dto), + moveOperation: { + type: dto.moveOperationType as SharedVaultMoveType, + from: { + sharedVaultUuid: fromSharedVaultUuid, + ownerUuid: fromOwnerUuid, + }, + to: { + sharedVaultUuid: toSharedVaultUuid, + ownerUuid: toOwnerUuid, + }, + }, } const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL) return Result.ok(valetToken) } - - private createMoveOperationData(dto: CreateSharedVaultFileValetTokenDTO): SharedVaultValetTokenData['moveOperation'] { - if (!dto.moveOperationType) { - return undefined - } - - let fromUuid: string - let toUuid: string - switch (dto.moveOperationType) { - case 'shared-vault-to-user': - fromUuid = dto.sharedVaultUuid - toUuid = dto.userUuid - break - case 'user-to-shared-vault': - fromUuid = dto.userUuid - toUuid = dto.sharedVaultUuid - break - case 'shared-vault-to-shared-vault': - fromUuid = dto.sharedVaultUuid - toUuid = dto.sharedVaultToSharedVaultMoveTargetUuid as string - break - } - - return { - type: dto.moveOperationType, - fromUuid, - toUuid, - } - } } diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetTokenDTO.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetTokenDTO.ts index 0db1a598f..131d7c7d8 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetTokenDTO.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetTokenDTO.ts @@ -10,4 +10,5 @@ export interface CreateSharedVaultFileValetTokenDTO { unencryptedFileSize?: number moveOperationType?: SharedVaultMoveType sharedVaultToSharedVaultMoveTargetUuid?: string + sharedVaultToSharedVaultMoveTargetOwnerUuid?: string }