From 94deab2ee0d2fdf348821ffd8efe5b2f073b43db Mon Sep 17 00:00:00 2001 From: Mo Date: Sun, 28 May 2023 14:11:31 -0500 Subject: [PATCH] refactor: vault files --- packages/auth/src/Bootstrap/Container.ts | 12 +- packages/auth/src/Bootstrap/Types.ts | 4 +- .../src/Controller/ValetTokenController.ts | 3 +- ...ts => UserFileRemovedEventHandler.spec.ts} | 13 +- ...dler.ts => UserFileRemovedEventHandler.ts} | 6 +- ...s => UserFileUploadedEventHandler.spec.ts} | 10 +- ...ler.ts => UserFileUploadedEventHandler.ts} | 6 +- .../CreateValetToken/CreateValetToken.ts | 3 +- .../ValetToken/CreateValetTokenPayload.ts | 7 + .../src/Domain/Event/FileRemovedEvent.ts | 7 - .../src/Domain/Event/FileUploadedEvent.ts | 7 - .../src/Domain/Event/UserFileRemovedEvent.ts | 7 + ...load.ts => UserFileRemovedEventPayload.ts} | 2 +- .../src/Domain/Event/UserFileUploadedEvent.ts | 7 + ...oad.ts => UserFileUploadedEventPayload.ts} | 2 +- .../src/Domain/Event/VaultFileRemovedEvent.ts | 7 + .../Event/VaultFileRemovedEventPayload.ts | 6 + .../Domain/Event/VaultFileUploadedEvent.ts | 7 + .../Event/VaultFileUploadedEventPayload.ts | 6 + packages/domain-events/src/Domain/index.ts | 15 ++- .../event-store/src/Bootstrap/Container.ts | 4 +- packages/files/src/Bootstrap/Container.ts | 7 + packages/files/src/Bootstrap/Types.ts | 2 + .../files/src/Controller/FilesController.ts | 11 +- .../src/Controller/VaultFilesController.ts | 122 ++++++++++++++++-- .../VaultValetTokenAuthMiddleware.ts | 2 + .../src/Domain/Event/DomainEventFactory.ts | 60 ++++++++- .../Event/DomainEventFactoryInterface.ts | 28 +++- .../AccountDeletionRequestedEventHandler.ts | 4 +- ...scriptionInvitationCanceledEventHandler.ts | 4 +- .../src/Domain/Services/FileMoverInterface.ts | 3 + .../CreateUploadSession.ts | 2 +- .../CreateUploadSessionDTO.ts | 2 +- .../FinishUploadSession.ts | 29 +++-- .../FinishUploadSessionDTO.ts | 3 +- .../GetFileMetadata/GetFileMetadata.ts | 7 +- .../GetFileMetadata/GetFileMetadataDTO.ts | 15 +-- .../MarkFilesToBeRemoved.ts | 6 +- .../MarkFilesToBeRemovedDTO.ts | 2 +- .../Domain/UseCase/MoveFile/MoveFile.spec.ts | 35 +++++ .../src/Domain/UseCase/MoveFile/MoveFile.ts | 43 ++++++ .../Domain/UseCase/MoveFile/MoveFileDTO.ts | 19 +++ .../UseCase/MoveFile/MoveFileResponse.ts | 8 ++ .../Domain/UseCase/RemoveFile/RemoveFile.ts | 35 +++-- .../UseCase/RemoveFile/RemoveFileDTO.ts | 13 +- .../StreamDownloadFile/StreamDownloadFile.ts | 9 +- .../StreamDownloadFileDTO.ts | 17 +-- .../UploadFileChunk/UploadFileChunk.ts | 2 +- .../UploadFileChunk/UploadFileChunkDTO.ts | 2 +- packages/files/src/Infra/FS/FSFileMover.ts | 17 +++ packages/files/src/Infra/S3/S3FileMover.ts | 30 +++++ .../src/Domain/Token/ValetTokenOperation.ts | 1 + .../src/Domain/Token/VaultValetTokenData.ts | 7 + .../mysql/1684173017359-create_vault.ts | 8 ++ .../Bootstrap/ServerContainerConfigLoader.ts | 7 +- .../syncing-server/src/Bootstrap/Types.ts | 2 + .../Bootstrap/WorkerContainerConfigLoader.ts | 7 + .../src/Controller/VaultsController.ts | 6 +- .../Handler/VaultFileRemovedEventHandler.ts | 33 +++++ .../Handler/VaultFileUploadedEventHandler.ts | 33 +++++ .../CreateVaultFileReadValetToken.ts | 57 -------- .../CreateVaultFileReadValetTokenDTO.ts | 6 - .../CreateVaultFileValetToken.ts | 96 ++++++++++++++ .../CreateVaultFileValetTokenDTO.ts | 10 ++ .../src/Domain/Vault/Factory/VaultHash.ts | 2 + .../src/Domain/Vault/Model/Vault.ts | 12 ++ .../src/Domain/Vault/Service/VaultService.ts | 2 + .../src/Projection/VaultProjection.ts | 2 + .../src/Projection/VaultProjector.ts | 2 + 69 files changed, 761 insertions(+), 212 deletions(-) rename packages/auth/src/Domain/Handler/{FileRemovedEventHandler.spec.ts => UserFileRemovedEventHandler.spec.ts} (91%) rename packages/auth/src/Domain/Handler/{FileRemovedEventHandler.ts => UserFileRemovedEventHandler.ts} (90%) rename packages/auth/src/Domain/Handler/{FileUploadedEventHandler.spec.ts => UserFileUploadedEventHandler.spec.ts} (93%) rename packages/auth/src/Domain/Handler/{FileUploadedEventHandler.ts => UserFileUploadedEventHandler.ts} (91%) create mode 100644 packages/auth/src/Domain/ValetToken/CreateValetTokenPayload.ts delete mode 100644 packages/domain-events/src/Domain/Event/FileRemovedEvent.ts delete mode 100644 packages/domain-events/src/Domain/Event/FileUploadedEvent.ts create mode 100644 packages/domain-events/src/Domain/Event/UserFileRemovedEvent.ts rename packages/domain-events/src/Domain/Event/{FileRemovedEventPayload.ts => UserFileRemovedEventPayload.ts} (71%) create mode 100644 packages/domain-events/src/Domain/Event/UserFileUploadedEvent.ts rename packages/domain-events/src/Domain/Event/{FileUploadedEventPayload.ts => UserFileUploadedEventPayload.ts} (63%) create mode 100644 packages/domain-events/src/Domain/Event/VaultFileRemovedEvent.ts create mode 100644 packages/domain-events/src/Domain/Event/VaultFileRemovedEventPayload.ts create mode 100644 packages/domain-events/src/Domain/Event/VaultFileUploadedEvent.ts create mode 100644 packages/domain-events/src/Domain/Event/VaultFileUploadedEventPayload.ts create mode 100644 packages/files/src/Domain/Services/FileMoverInterface.ts create mode 100644 packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts create mode 100644 packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts create mode 100644 packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts create mode 100644 packages/files/src/Domain/UseCase/MoveFile/MoveFileResponse.ts create mode 100644 packages/files/src/Infra/FS/FSFileMover.ts create mode 100644 packages/files/src/Infra/S3/S3FileMover.ts create mode 100644 packages/syncing-server/src/Domain/Handler/VaultFileRemovedEventHandler.ts create mode 100644 packages/syncing-server/src/Domain/Handler/VaultFileUploadedEventHandler.ts delete mode 100644 packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetToken.ts delete mode 100644 packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetTokenDTO.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetTokenDTO.ts diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 3c06db77e..e967a33d3 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -139,12 +139,12 @@ import { ValetTokenData, WebSocketConnectionTokenData, } from '@standardnotes/security' -import { FileUploadedEventHandler } from '../Domain/Handler/FileUploadedEventHandler' +import { UserFileUploadedEventHandler } from '../Domain/Handler/UserFileUploadedEventHandler' import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken' import { CreateListedAccount } from '../Domain/UseCase/CreateListedAccount/CreateListedAccount' import { ListedAccountCreatedEventHandler } from '../Domain/Handler/ListedAccountCreatedEventHandler' import { ListedAccountDeletedEventHandler } from '../Domain/Handler/ListedAccountDeletedEventHandler' -import { FileRemovedEventHandler } from '../Domain/Handler/FileRemovedEventHandler' +import { UserFileRemovedEventHandler } from '../Domain/Handler/UserFileRemovedEventHandler' import { UserDisabledSessionUserAgentLoggingEventHandler } from '../Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler' import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface' import { SettingInterpreter } from '../Domain/Setting/SettingInterpreter' @@ -834,8 +834,8 @@ export class ContainerConfigLoader { .bind(TYPES.SubscriptionReassignedEventHandler) .to(SubscriptionReassignedEventHandler) container.bind(TYPES.UserEmailChangedEventHandler).to(UserEmailChangedEventHandler) - container.bind(TYPES.FileUploadedEventHandler).to(FileUploadedEventHandler) - container.bind(TYPES.FileRemovedEventHandler).to(FileRemovedEventHandler) + container.bind(TYPES.UserFileUploadedEventHandler).to(UserFileUploadedEventHandler) + container.bind(TYPES.UserFileRemovedEventHandler).to(UserFileRemovedEventHandler) container .bind(TYPES.ListedAccountCreatedEventHandler) .to(ListedAccountCreatedEventHandler) @@ -873,8 +873,8 @@ export class ContainerConfigLoader { ['EXTENSION_KEY_GRANTED', container.get(TYPES.ExtensionKeyGrantedEventHandler)], ['SUBSCRIPTION_REASSIGNED', container.get(TYPES.SubscriptionReassignedEventHandler)], ['USER_EMAIL_CHANGED', container.get(TYPES.UserEmailChangedEventHandler)], - ['FILE_UPLOADED', container.get(TYPES.FileUploadedEventHandler)], - ['FILE_REMOVED', container.get(TYPES.FileRemovedEventHandler)], + ['USER_FILE_UPLOADED', container.get(TYPES.UserFileUploadedEventHandler)], + ['USER_FILE_REMOVED', container.get(TYPES.UserFileRemovedEventHandler)], ['LISTED_ACCOUNT_CREATED', container.get(TYPES.ListedAccountCreatedEventHandler)], ['LISTED_ACCOUNT_DELETED', container.get(TYPES.ListedAccountDeletedEventHandler)], [ diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index efdff9d00..517c8709c 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -164,8 +164,8 @@ const TYPES = { SubscriptionSyncRequestedEventHandler: Symbol.for('SubscriptionSyncRequestedEventHandler'), ExtensionKeyGrantedEventHandler: Symbol.for('ExtensionKeyGrantedEventHandler'), UserEmailChangedEventHandler: Symbol.for('UserEmailChangedEventHandler'), - FileUploadedEventHandler: Symbol.for('FileUploadedEventHandler'), - FileRemovedEventHandler: Symbol.for('FileRemovedEventHandler'), + UserFileUploadedEventHandler: Symbol.for('UserFileUploadedEventHandler'), + UserFileRemovedEventHandler: Symbol.for('UserFileRemovedEventHandler'), ListedAccountCreatedEventHandler: Symbol.for('ListedAccountCreatedEventHandler'), ListedAccountDeletedEventHandler: Symbol.for('ListedAccountDeletedEventHandler'), UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'), diff --git a/packages/auth/src/Controller/ValetTokenController.ts b/packages/auth/src/Controller/ValetTokenController.ts index 57951a3e3..e1070670e 100644 --- a/packages/auth/src/Controller/ValetTokenController.ts +++ b/packages/auth/src/Controller/ValetTokenController.ts @@ -7,12 +7,13 @@ import { // eslint-disable-next-line @typescript-eslint/no-unused-vars results, } from 'inversify-express-utils' -import { CreateValetTokenPayload, ErrorTag } from '@standardnotes/responses' +import { ErrorTag } from '@standardnotes/responses' import { ValetTokenOperation } from '@standardnotes/security' import { Uuid } from '@standardnotes/domain-core' import TYPES from '../Bootstrap/Types' import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken' +import { CreateValetTokenPayload } from '../Domain/ValetToken/CreateValetTokenPayload' @controller('/valet-tokens', TYPES.ApiGatewayAuthMiddleware) export class ValetTokenController extends BaseHttpController { diff --git a/packages/auth/src/Domain/Handler/FileRemovedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/UserFileRemovedEventHandler.spec.ts similarity index 91% rename from packages/auth/src/Domain/Handler/FileRemovedEventHandler.spec.ts rename to packages/auth/src/Domain/Handler/UserFileRemovedEventHandler.spec.ts index 66d7f497c..b4e784ffc 100644 --- a/packages/auth/src/Domain/Handler/FileRemovedEventHandler.spec.ts +++ b/packages/auth/src/Domain/Handler/UserFileRemovedEventHandler.spec.ts @@ -1,25 +1,26 @@ import 'reflect-metadata' -import { FileRemovedEvent } from '@standardnotes/domain-events' +import { UserFileRemovedEvent } from '@standardnotes/domain-events' import { Logger } from 'winston' import { User } from '../User/User' -import { FileRemovedEventHandler } from './FileRemovedEventHandler' +import { UserFileRemovedEventHandler } from './UserFileRemovedEventHandler' import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' import { UserSubscription } from '../Subscription/UserSubscription' import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface' -describe('FileRemovedEventHandler', () => { +describe('UserFileRemovedEventHandler', () => { let userSubscriptionService: UserSubscriptionServiceInterface let logger: Logger let user: User - let event: FileRemovedEvent + let event: UserFileRemovedEvent let subscriptionSettingService: SubscriptionSettingServiceInterface let regularSubscription: UserSubscription let sharedSubscription: UserSubscription - const createHandler = () => new FileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger) + const createHandler = () => + new UserFileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger) beforeEach(() => { user = { @@ -47,7 +48,7 @@ describe('FileRemovedEventHandler', () => { subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null) subscriptionSettingService.createOrReplace = jest.fn() - event = {} as jest.Mocked + event = {} as jest.Mocked event.createdAt = new Date(1) event.payload = { userUuid: '1-2-3', diff --git a/packages/auth/src/Domain/Handler/FileRemovedEventHandler.ts b/packages/auth/src/Domain/Handler/UserFileRemovedEventHandler.ts similarity index 90% rename from packages/auth/src/Domain/Handler/FileRemovedEventHandler.ts rename to packages/auth/src/Domain/Handler/UserFileRemovedEventHandler.ts index a4c5310c0..f9a1a53be 100644 --- a/packages/auth/src/Domain/Handler/FileRemovedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/UserFileRemovedEventHandler.ts @@ -1,4 +1,4 @@ -import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events' +import { DomainEventHandlerInterface, UserFileRemovedEvent } from '@standardnotes/domain-events' import { SettingName } from '@standardnotes/settings' import { inject, injectable } from 'inversify' import { Logger } from 'winston' @@ -10,14 +10,14 @@ import { UserSubscription } from '../Subscription/UserSubscription' import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface' @injectable() -export class FileRemovedEventHandler implements DomainEventHandlerInterface { +export class UserFileRemovedEventHandler implements DomainEventHandlerInterface { constructor( @inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, @inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface, @inject(TYPES.Logger) private logger: Logger, ) {} - async handle(event: FileRemovedEvent): Promise { + async handle(event: UserFileRemovedEvent): Promise { const { regularSubscription, sharedSubscription } = await this.userSubscriptionService.findRegularSubscriptionForUserUuid(event.payload.userUuid) if (regularSubscription === null) { diff --git a/packages/auth/src/Domain/Handler/FileUploadedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/UserFileUploadedEventHandler.spec.ts similarity index 93% rename from packages/auth/src/Domain/Handler/FileUploadedEventHandler.spec.ts rename to packages/auth/src/Domain/Handler/UserFileUploadedEventHandler.spec.ts index 13752f260..59749be73 100644 --- a/packages/auth/src/Domain/Handler/FileUploadedEventHandler.spec.ts +++ b/packages/auth/src/Domain/Handler/UserFileUploadedEventHandler.spec.ts @@ -1,11 +1,11 @@ import 'reflect-metadata' -import { FileUploadedEvent } from '@standardnotes/domain-events' +import { UserFileUploadedEvent } from '@standardnotes/domain-events' import { Logger } from 'winston' import { User } from '../User/User' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { FileUploadedEventHandler } from './FileUploadedEventHandler' +import { UserFileUploadedEventHandler } from './UserFileUploadedEventHandler' import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' import { UserSubscription } from '../Subscription/UserSubscription' import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface' @@ -16,13 +16,13 @@ describe('FileUploadedEventHandler', () => { let userSubscriptionService: UserSubscriptionServiceInterface let logger: Logger let user: User - let event: FileUploadedEvent + let event: UserFileUploadedEvent let subscriptionSettingService: SubscriptionSettingServiceInterface let regularSubscription: UserSubscription let sharedSubscription: UserSubscription const createHandler = () => - new FileUploadedEventHandler(userRepository, userSubscriptionService, subscriptionSettingService, logger) + new UserFileUploadedEventHandler(userRepository, userSubscriptionService, subscriptionSettingService, logger) beforeEach(() => { user = { @@ -53,7 +53,7 @@ describe('FileUploadedEventHandler', () => { subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null) subscriptionSettingService.createOrReplace = jest.fn() - event = {} as jest.Mocked + event = {} as jest.Mocked event.createdAt = new Date(1) event.payload = { userUuid: '1-2-3', diff --git a/packages/auth/src/Domain/Handler/FileUploadedEventHandler.ts b/packages/auth/src/Domain/Handler/UserFileUploadedEventHandler.ts similarity index 91% rename from packages/auth/src/Domain/Handler/FileUploadedEventHandler.ts rename to packages/auth/src/Domain/Handler/UserFileUploadedEventHandler.ts index 542a6435f..adc6db78e 100644 --- a/packages/auth/src/Domain/Handler/FileUploadedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/UserFileUploadedEventHandler.ts @@ -1,4 +1,4 @@ -import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events' +import { DomainEventHandlerInterface, UserFileUploadedEvent } from '@standardnotes/domain-events' import { SettingName } from '@standardnotes/settings' import { inject, injectable } from 'inversify' import { Logger } from 'winston' @@ -11,7 +11,7 @@ import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscripti import { UserRepositoryInterface } from '../User/UserRepositoryInterface' @injectable() -export class FileUploadedEventHandler implements DomainEventHandlerInterface { +export class UserFileUploadedEventHandler implements DomainEventHandlerInterface { constructor( @inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface, @inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, @@ -19,7 +19,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface { @inject(TYPES.Logger) private logger: Logger, ) {} - async handle(event: FileUploadedEvent): Promise { + async handle(event: UserFileUploadedEvent): Promise { const user = await this.userRepository.findOneByUuid(event.payload.userUuid) if (user === null) { this.logger.warn(`Could not find user with uuid: ${event.payload.userUuid}`) diff --git a/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts b/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts index a7c608a5b..fc6328b36 100644 --- a/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts +++ b/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts @@ -2,7 +2,7 @@ import { inject, injectable } from 'inversify' import { SubscriptionName } from '@standardnotes/common' import { TimerInterface } from '@standardnotes/time' import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security' -import { CreateValetTokenPayload, CreateValetTokenResponseData } from '@standardnotes/responses' +import { CreateValetTokenResponseData } from '@standardnotes/responses' import { SettingName } from '@standardnotes/settings' import TYPES from '../../../Bootstrap/Types' @@ -12,6 +12,7 @@ import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionS import { CreateValetTokenDTO } from './CreateValetTokenDTO' import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface' import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' +import { CreateValetTokenPayload } from '../../ValetToken/CreateValetTokenPayload' @injectable() export class CreateValetToken implements UseCaseInterface { diff --git a/packages/auth/src/Domain/ValetToken/CreateValetTokenPayload.ts b/packages/auth/src/Domain/ValetToken/CreateValetTokenPayload.ts new file mode 100644 index 000000000..1b10ae343 --- /dev/null +++ b/packages/auth/src/Domain/ValetToken/CreateValetTokenPayload.ts @@ -0,0 +1,7 @@ +export type CreateValetTokenPayload = { + operation: 'read' | 'write' | 'delete' | 'move' + resources: Array<{ + remoteIdentifier: string + unencryptedFileSize?: number + }> +} diff --git a/packages/domain-events/src/Domain/Event/FileRemovedEvent.ts b/packages/domain-events/src/Domain/Event/FileRemovedEvent.ts deleted file mode 100644 index 3f7ad681d..000000000 --- a/packages/domain-events/src/Domain/Event/FileRemovedEvent.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DomainEventInterface } from './DomainEventInterface' -import { FileRemovedEventPayload } from './FileRemovedEventPayload' - -export interface FileRemovedEvent extends DomainEventInterface { - type: 'FILE_REMOVED' - payload: FileRemovedEventPayload -} diff --git a/packages/domain-events/src/Domain/Event/FileUploadedEvent.ts b/packages/domain-events/src/Domain/Event/FileUploadedEvent.ts deleted file mode 100644 index 6856bf65e..000000000 --- a/packages/domain-events/src/Domain/Event/FileUploadedEvent.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DomainEventInterface } from './DomainEventInterface' -import { FileUploadedEventPayload } from './FileUploadedEventPayload' - -export interface FileUploadedEvent extends DomainEventInterface { - type: 'FILE_UPLOADED' - payload: FileUploadedEventPayload -} diff --git a/packages/domain-events/src/Domain/Event/UserFileRemovedEvent.ts b/packages/domain-events/src/Domain/Event/UserFileRemovedEvent.ts new file mode 100644 index 000000000..67a025de6 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/UserFileRemovedEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { UserFileRemovedEventPayload } from './UserFileRemovedEventPayload' + +export interface UserFileRemovedEvent extends DomainEventInterface { + type: 'USER_FILE_REMOVED' + payload: UserFileRemovedEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/FileRemovedEventPayload.ts b/packages/domain-events/src/Domain/Event/UserFileRemovedEventPayload.ts similarity index 71% rename from packages/domain-events/src/Domain/Event/FileRemovedEventPayload.ts rename to packages/domain-events/src/Domain/Event/UserFileRemovedEventPayload.ts index ab5e37120..8e56f5c35 100644 --- a/packages/domain-events/src/Domain/Event/FileRemovedEventPayload.ts +++ b/packages/domain-events/src/Domain/Event/UserFileRemovedEventPayload.ts @@ -1,4 +1,4 @@ -export interface FileRemovedEventPayload { +export interface UserFileRemovedEventPayload { userUuid: string regularSubscriptionUuid: string fileByteSize: number diff --git a/packages/domain-events/src/Domain/Event/UserFileUploadedEvent.ts b/packages/domain-events/src/Domain/Event/UserFileUploadedEvent.ts new file mode 100644 index 000000000..c81561604 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/UserFileUploadedEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { UserFileUploadedEventPayload } from './UserFileUploadedEventPayload' + +export interface UserFileUploadedEvent extends DomainEventInterface { + type: 'USER_FILE_UPLOADED' + payload: UserFileUploadedEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/FileUploadedEventPayload.ts b/packages/domain-events/src/Domain/Event/UserFileUploadedEventPayload.ts similarity index 63% rename from packages/domain-events/src/Domain/Event/FileUploadedEventPayload.ts rename to packages/domain-events/src/Domain/Event/UserFileUploadedEventPayload.ts index 6180b9499..27dfdc4b3 100644 --- a/packages/domain-events/src/Domain/Event/FileUploadedEventPayload.ts +++ b/packages/domain-events/src/Domain/Event/UserFileUploadedEventPayload.ts @@ -1,4 +1,4 @@ -export interface FileUploadedEventPayload { +export interface UserFileUploadedEventPayload { userUuid: string fileByteSize: number filePath: string diff --git a/packages/domain-events/src/Domain/Event/VaultFileRemovedEvent.ts b/packages/domain-events/src/Domain/Event/VaultFileRemovedEvent.ts new file mode 100644 index 000000000..e35486a6b --- /dev/null +++ b/packages/domain-events/src/Domain/Event/VaultFileRemovedEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { VaultFileRemovedEventPayload } from './VaultFileRemovedEventPayload' + +export interface VaultFileRemovedEvent extends DomainEventInterface { + type: 'VAULT_FILE_REMOVED' + payload: VaultFileRemovedEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/VaultFileRemovedEventPayload.ts b/packages/domain-events/src/Domain/Event/VaultFileRemovedEventPayload.ts new file mode 100644 index 000000000..2073f6dd4 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/VaultFileRemovedEventPayload.ts @@ -0,0 +1,6 @@ +export interface VaultFileRemovedEventPayload { + vaultUuid: string + fileByteSize: number + filePath: string + fileName: string +} diff --git a/packages/domain-events/src/Domain/Event/VaultFileUploadedEvent.ts b/packages/domain-events/src/Domain/Event/VaultFileUploadedEvent.ts new file mode 100644 index 000000000..3a41eb88b --- /dev/null +++ b/packages/domain-events/src/Domain/Event/VaultFileUploadedEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { VaultFileUploadedEventPayload } from './VaultFileUploadedEventPayload' + +export interface VaultFileUploadedEvent extends DomainEventInterface { + type: 'VAULT_FILE_UPLOADED' + payload: VaultFileUploadedEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/VaultFileUploadedEventPayload.ts b/packages/domain-events/src/Domain/Event/VaultFileUploadedEventPayload.ts new file mode 100644 index 000000000..eb1873f13 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/VaultFileUploadedEventPayload.ts @@ -0,0 +1,6 @@ +export interface VaultFileUploadedEventPayload { + vaultUuid: string + fileByteSize: number + filePath: string + fileName: string +} diff --git a/packages/domain-events/src/Domain/index.ts b/packages/domain-events/src/Domain/index.ts index 814d412ca..3fd7c47fa 100644 --- a/packages/domain-events/src/Domain/index.ts +++ b/packages/domain-events/src/Domain/index.ts @@ -26,10 +26,17 @@ export * from './Event/ExitDiscountWithdrawRequestedEvent' export * from './Event/ExitDiscountWithdrawRequestedEventPayload' export * from './Event/ExtensionKeyGrantedEvent' export * from './Event/ExtensionKeyGrantedEventPayload' -export * from './Event/FileRemovedEvent' -export * from './Event/FileRemovedEventPayload' -export * from './Event/FileUploadedEvent' -export * from './Event/FileUploadedEventPayload' + +export * from './Event/UserFileRemovedEvent' +export * from './Event/UserFileRemovedEventPayload' +export * from './Event/UserFileUploadedEvent' +export * from './Event/UserFileUploadedEventPayload' + +export * from './Event/VaultFileRemovedEvent' +export * from './Event/VaultFileRemovedEventPayload' +export * from './Event/VaultFileUploadedEvent' +export * from './Event/VaultFileUploadedEventPayload' + export * from './Event/ItemDumpedEvent' export * from './Event/ItemDumpedEventPayload' export * from './Event/ItemRevisionCreationRequestedEvent' diff --git a/packages/event-store/src/Bootstrap/Container.ts b/packages/event-store/src/Bootstrap/Container.ts index 0d62f5bdd..843d0f178 100644 --- a/packages/event-store/src/Bootstrap/Container.ts +++ b/packages/event-store/src/Bootstrap/Container.ts @@ -76,8 +76,8 @@ export class ContainerConfigLoader { ['SUBSCRIPTION_REASSIGNED', container.get(TYPES.EventHandler)], ['USER_CREDENTIALS_CHANGED', container.get(TYPES.EventHandler)], ['USER_EMAIL_CHANGED', container.get(TYPES.EventHandler)], - ['FILE_UPLOADED', container.get(TYPES.EventHandler)], - ['FILE_REMOVED', container.get(TYPES.EventHandler)], + ['USER_FILE_UPLOADED', container.get(TYPES.EventHandler)], + ['USER_FILE_REMOVED', container.get(TYPES.EventHandler)], ['LISTED_ACCOUNT_REQUESTED', container.get(TYPES.EventHandler)], ['LISTED_ACCOUNT_CREATED', container.get(TYPES.EventHandler)], ['LISTED_ACCOUNT_DELETED', container.get(TYPES.EventHandler)], diff --git a/packages/files/src/Bootstrap/Container.ts b/packages/files/src/Bootstrap/Container.ts index b45eb5275..b65ab371d 100644 --- a/packages/files/src/Bootstrap/Container.ts +++ b/packages/files/src/Bootstrap/Container.ts @@ -45,6 +45,10 @@ import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountD import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler' import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository' import { VaultValetTokenAuthMiddleware } from '../Controller/VaultValetTokenAuthMiddleware' +import { FileMoverInterface } from '../Domain/Services/FileMoverInterface' +import { S3FileMover } from '../Infra/S3/S3FileMover' +import { FSFileMover } from '../Infra/FS/FSFileMover' +import { MoveFile } from '../Domain/UseCase/MoveFile/MoveFile' export class ContainerConfigLoader { async load(): Promise { @@ -98,12 +102,14 @@ export class ContainerConfigLoader { container.bind(TYPES.FileDownloader).to(S3FileDownloader) container.bind(TYPES.FileUploader).to(S3FileUploader) container.bind(TYPES.FileRemover).to(S3FileRemover) + container.bind(TYPES.FileMover).to(S3FileMover) } else { container.bind(TYPES.FileDownloader).to(FSFileDownloader) container .bind(TYPES.FileUploader) .toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger))) container.bind(TYPES.FileRemover).to(FSFileRemover) + container.bind(TYPES.FileMover).to(FSFileMover) } if (env.get('SNS_TOPIC_ARN', true)) { @@ -146,6 +152,7 @@ export class ContainerConfigLoader { container.bind(TYPES.FinishUploadSession).to(FinishUploadSession) container.bind(TYPES.GetFileMetadata).to(GetFileMetadata) container.bind(TYPES.RemoveFile).to(RemoveFile) + container.bind(TYPES.MoveFile).to(MoveFile) container.bind(TYPES.MarkFilesToBeRemoved).to(MarkFilesToBeRemoved) // middleware diff --git a/packages/files/src/Bootstrap/Types.ts b/packages/files/src/Bootstrap/Types.ts index 06d67e69f..b07e0bb57 100644 --- a/packages/files/src/Bootstrap/Types.ts +++ b/packages/files/src/Bootstrap/Types.ts @@ -13,6 +13,7 @@ const TYPES = { FinishUploadSession: Symbol.for('FinishUploadSession'), GetFileMetadata: Symbol.for('GetFileMetadata'), RemoveFile: Symbol.for('RemoveFile'), + MoveFile: Symbol.for('MoveFile'), MarkFilesToBeRemoved: Symbol.for('MarkFilesToBeRemoved'), // services @@ -23,6 +24,7 @@ const TYPES = { FileUploader: Symbol.for('FileUploader'), FileDownloader: Symbol.for('FileDownloader'), FileRemover: Symbol.for('FileRemover'), + FileMover: Symbol.for('FileMover'), // repositories UploadRepository: Symbol.for('UploadRepository'), diff --git a/packages/files/src/Controller/FilesController.ts b/packages/files/src/Controller/FilesController.ts index 5834756b3..73d5dfd1e 100644 --- a/packages/files/src/Controller/FilesController.ts +++ b/packages/files/src/Controller/FilesController.ts @@ -35,7 +35,7 @@ export class FilesController extends BaseHttpController { } const result = await this.createUploadSession.execute({ - userUuid: response.locals.userUuid, + ownerUuid: response.locals.userUuid, resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier, }) @@ -61,7 +61,7 @@ export class FilesController extends BaseHttpController { } const result = await this.uploadFileChunk.execute({ - userUuid: response.locals.userUuid, + ownerUuid: response.locals.userUuid, resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier, resourceUnencryptedFileSize: response.locals.permittedResources[0].unencryptedFileSize, chunkId, @@ -85,7 +85,8 @@ export class FilesController extends BaseHttpController { } const result = await this.finishUploadSession.execute({ - userUuid: response.locals.userUuid, + ownerUuid: response.locals.userUuid, + ownerType: 'user', resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier, uploadBytesLimit: response.locals.uploadBytesLimit, uploadBytesUsed: response.locals.uploadBytesUsed, @@ -140,7 +141,7 @@ export class FilesController extends BaseHttpController { } const fileMetadata = await this.getFileMetadata.execute({ - userUuid: response.locals.userUuid, + ownerUuid: response.locals.userUuid, resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier, }) @@ -161,7 +162,7 @@ export class FilesController extends BaseHttpController { response.writeHead(206, headers) const result = await this.streamDownloadFile.execute({ - userUuid: response.locals.userUuid, + ownerUuid: response.locals.userUuid, resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier, startRange, endRange, diff --git a/packages/files/src/Controller/VaultFilesController.ts b/packages/files/src/Controller/VaultFilesController.ts index 906ef5c79..970d7802f 100644 --- a/packages/files/src/Controller/VaultFilesController.ts +++ b/packages/files/src/Controller/VaultFilesController.ts @@ -1,28 +1,136 @@ -import { BaseHttpController, controller, httpGet, results } from 'inversify-express-utils' +import { BaseHttpController, controller, httpDelete, httpGet, httpPost, results } from 'inversify-express-utils' import { Request, Response } from 'express' import { inject } from 'inversify' import { Writable } from 'stream' import TYPES from '../Bootstrap/Types' +import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk' import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile' +import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/CreateUploadSession' +import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession' import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata' -import { VaultValetTokenData, ValetTokenOperation } from '@standardnotes/security' +import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile' +import { ValetTokenOperation, VaultValetTokenData } from '@standardnotes/security' @controller('/v1/vault/files', TYPES.VaultValetTokenAuthMiddleware) -export class VaultFilesController extends BaseHttpController { +export class FilesController extends BaseHttpController { constructor( + @inject(TYPES.UploadFileChunk) private uploadFileChunk: UploadFileChunk, + @inject(TYPES.CreateUploadSession) private createUploadSession: CreateUploadSession, + @inject(TYPES.FinishUploadSession) private finishUploadSession: FinishUploadSession, @inject(TYPES.StreamDownloadFile) private streamDownloadFile: StreamDownloadFile, @inject(TYPES.GetFileMetadata) private getFileMetadata: GetFileMetadata, + @inject(TYPES.RemoveFile) private removeFile: RemoveFile, @inject(TYPES.MAX_CHUNK_BYTES) private maxChunkBytes: number, ) { super() } + @httpPost('/upload/create-session') + async startUpload( + _request: Request, + response: Response, + ): Promise { + const locals = response.locals as VaultValetTokenData + if (locals.permittedOperation !== ValetTokenOperation.Write) { + return this.badRequest('Not permitted for this operation') + } + + const result = await this.createUploadSession.execute({ + ownerUuid: locals.vaultUuid, + resourceRemoteIdentifier: locals.remoteIdentifier, + }) + + if (!result.success) { + return this.badRequest(result.message) + } + + return this.json({ success: true, uploadId: result.uploadId }) + } + + @httpPost('/upload/chunk') + async uploadChunk( + request: Request, + response: Response, + ): Promise { + const locals = response.locals as VaultValetTokenData + if (locals.permittedOperation !== ValetTokenOperation.Write) { + return this.badRequest('Not permitted for this operation') + } + + const chunkId = +(request.headers['x-chunk-id'] as string) + if (!chunkId) { + return this.badRequest('Missing x-chunk-id header in request.') + } + + const result = await this.uploadFileChunk.execute({ + ownerUuid: locals.vaultUuid, + resourceRemoteIdentifier: locals.remoteIdentifier, + resourceUnencryptedFileSize: locals.unencryptedFileSize as number, + chunkId, + data: request.body, + }) + + if (!result.success) { + return this.badRequest(result.message) + } + + return this.json({ success: true, message: 'Chunk uploaded successfully' }) + } + + @httpPost('/upload/close-session') + public async finishUpload( + _request: Request, + response: Response, + ): Promise { + const locals = response.locals as VaultValetTokenData + if (locals.permittedOperation !== ValetTokenOperation.Write) { + return this.badRequest('Not permitted for this operation') + } + + const result = await this.finishUploadSession.execute({ + ownerUuid: locals.vaultUuid, + ownerType: 'vault', + resourceRemoteIdentifier: locals.remoteIdentifier, + uploadBytesLimit: locals.uploadBytesLimit, + uploadBytesUsed: locals.uploadBytesUsed, + }) + + if (!result.success) { + return this.badRequest(result.message) + } + + return this.json({ success: true, message: 'File uploaded successfully' }) + } + + @httpDelete('/') + async remove( + _request: Request, + response: Response, + ): Promise { + const locals = response.locals as VaultValetTokenData + if (locals.permittedOperation !== ValetTokenOperation.Delete) { + return this.badRequest('Not permitted for this operation') + } + + const result = await this.removeFile.execute({ + vaultUuid: locals.vaultUuid, + resourceRemoteIdentifier: locals.remoteIdentifier, + }) + + if (!result.success) { + return this.badRequest(result.message) + } + + return this.json({ success: true, message: 'File removed successfully' }) + } + @httpGet('/') async download( request: Request, response: Response, ): Promise Writable)> { - if (response.locals.permittedOperation !== ValetTokenOperation.Read) { + const locals = response.locals as VaultValetTokenData + if (locals.permittedOperation !== ValetTokenOperation.Read) { return this.badRequest('Not permitted for this operation') } @@ -36,10 +144,8 @@ export class VaultFilesController extends BaseHttpController { chunkSize = this.maxChunkBytes } - const locals = response.locals as VaultValetTokenData - const fileMetadata = await this.getFileMetadata.execute({ - vaultUuid: locals.vaultUuid, + ownerUuid: locals.vaultUuid, resourceRemoteIdentifier: locals.remoteIdentifier, }) @@ -60,7 +166,7 @@ export class VaultFilesController extends BaseHttpController { response.writeHead(206, headers) const result = await this.streamDownloadFile.execute({ - vaultUuid: locals.vaultUuid, + ownerUuid: locals.vaultUuid, resourceRemoteIdentifier: locals.remoteIdentifier, startRange, endRange, diff --git a/packages/files/src/Controller/VaultValetTokenAuthMiddleware.ts b/packages/files/src/Controller/VaultValetTokenAuthMiddleware.ts index dbef1a1d7..fff21103d 100644 --- a/packages/files/src/Controller/VaultValetTokenAuthMiddleware.ts +++ b/packages/files/src/Controller/VaultValetTokenAuthMiddleware.ts @@ -64,6 +64,8 @@ export class VaultValetTokenAuthMiddleware extends BaseMiddleware { vaultUuid: valetTokenData.vaultUuid, remoteIdentifier: valetTokenData.remoteIdentifier, permittedOperation: valetTokenData.permittedOperation, + uploadBytesUsed: valetTokenData.uploadBytesUsed, + uploadBytesLimit: valetTokenData.uploadBytesLimit, } Object.assign(response.locals, whitelistedData) diff --git a/packages/files/src/Domain/Event/DomainEventFactory.ts b/packages/files/src/Domain/Event/DomainEventFactory.ts index 478d86c4f..b94c61428 100644 --- a/packages/files/src/Domain/Event/DomainEventFactory.ts +++ b/packages/files/src/Domain/Event/DomainEventFactory.ts @@ -1,4 +1,10 @@ -import { FileUploadedEvent, FileRemovedEvent, DomainEventService } from '@standardnotes/domain-events' +import { + UserFileRemovedEvent, + UserFileUploadedEvent, + DomainEventService, + VaultFileUploadedEvent, + VaultFileRemovedEvent, +} from '@standardnotes/domain-events' import { TimerInterface } from '@standardnotes/time' import { inject, injectable } from 'inversify' @@ -9,15 +15,15 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface' export class DomainEventFactory implements DomainEventFactoryInterface { constructor(@inject(TYPES.Timer) private timer: TimerInterface) {} - createFileRemovedEvent(payload: { + createUserFileRemovedEvent(payload: { userUuid: string filePath: string fileName: string fileByteSize: number regularSubscriptionUuid: string - }): FileRemovedEvent { + }): UserFileRemovedEvent { return { - type: 'FILE_REMOVED', + type: 'USER_FILE_REMOVED', createdAt: this.timer.getUTCDate(), meta: { correlation: { @@ -30,14 +36,14 @@ export class DomainEventFactory implements DomainEventFactoryInterface { } } - createFileUploadedEvent(payload: { + createUserFileUploadedEvent(payload: { userUuid: string filePath: string fileName: string fileByteSize: number - }): FileUploadedEvent { + }): UserFileUploadedEvent { return { - type: 'FILE_UPLOADED', + type: 'USER_FILE_UPLOADED', createdAt: this.timer.getUTCDate(), meta: { correlation: { @@ -49,4 +55,44 @@ export class DomainEventFactory implements DomainEventFactoryInterface { payload, } } + + createVaultFileUploadedEvent(payload: { + vaultUuid: string + filePath: string + fileName: string + fileByteSize: number + }): VaultFileUploadedEvent { + return { + type: 'VAULT_FILE_UPLOADED', + createdAt: this.timer.getUTCDate(), + meta: { + correlation: { + userIdentifier: payload.vaultUuid, + userIdentifierType: 'uuid', + }, + origin: DomainEventService.Files, + }, + payload, + } + } + + createVaultFileRemovedEvent(payload: { + vaultUuid: string + filePath: string + fileName: string + fileByteSize: number + }): VaultFileRemovedEvent { + return { + type: 'VAULT_FILE_REMOVED', + createdAt: this.timer.getUTCDate(), + meta: { + correlation: { + userIdentifier: payload.vaultUuid, + userIdentifierType: 'uuid', + }, + origin: DomainEventService.Files, + }, + payload, + } + } } diff --git a/packages/files/src/Domain/Event/DomainEventFactoryInterface.ts b/packages/files/src/Domain/Event/DomainEventFactoryInterface.ts index da7ac674c..f3b59aeab 100644 --- a/packages/files/src/Domain/Event/DomainEventFactoryInterface.ts +++ b/packages/files/src/Domain/Event/DomainEventFactoryInterface.ts @@ -1,17 +1,35 @@ -import { FileUploadedEvent, FileRemovedEvent } from '@standardnotes/domain-events' +import { + UserFileUploadedEvent, + UserFileRemovedEvent, + VaultFileUploadedEvent, + VaultFileRemovedEvent, +} from '@standardnotes/domain-events' export interface DomainEventFactoryInterface { - createFileUploadedEvent(payload: { + createUserFileUploadedEvent(payload: { userUuid: string filePath: string fileName: string fileByteSize: number - }): FileUploadedEvent - createFileRemovedEvent(payload: { + }): UserFileUploadedEvent + createUserFileRemovedEvent(payload: { userUuid: string filePath: string fileName: string fileByteSize: number regularSubscriptionUuid: string - }): FileRemovedEvent + }): UserFileRemovedEvent + + createVaultFileUploadedEvent(payload: { + vaultUuid: string + filePath: string + fileName: string + fileByteSize: number + }): VaultFileUploadedEvent + createVaultFileRemovedEvent(payload: { + vaultUuid: string + filePath: string + fileName: string + fileByteSize: number + }): VaultFileRemovedEvent } diff --git a/packages/files/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts b/packages/files/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts index 15bfd2146..53d2e3468 100644 --- a/packages/files/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts +++ b/packages/files/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts @@ -23,7 +23,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI } const response = await this.markFilesToBeRemoved.execute({ - userUuid: event.payload.userUuid, + ownerUuid: event.payload.userUuid, }) if (!response.success) { @@ -32,7 +32,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI for (const fileRemoved of response.filesRemoved) { await this.domainEventPublisher.publish( - this.domainEventFactory.createFileRemovedEvent({ + this.domainEventFactory.createUserFileRemovedEvent({ regularSubscriptionUuid: event.payload.regularSubscriptionUuid, ...fileRemoved, }), diff --git a/packages/files/src/Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler.ts b/packages/files/src/Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler.ts index bc7d2ea09..eebd25078 100644 --- a/packages/files/src/Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler.ts +++ b/packages/files/src/Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler.ts @@ -23,7 +23,7 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE } const response = await this.markFilesToBeRemoved.execute({ - userUuid: event.payload.inviteeIdentifier, + ownerUuid: event.payload.inviteeIdentifier, }) if (!response.success) { @@ -32,7 +32,7 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE for (const fileRemoved of response.filesRemoved) { await this.domainEventPublisher.publish( - this.domainEventFactory.createFileRemovedEvent({ + this.domainEventFactory.createUserFileRemovedEvent({ regularSubscriptionUuid: event.payload.inviterSubscriptionUuid, ...fileRemoved, }), diff --git a/packages/files/src/Domain/Services/FileMoverInterface.ts b/packages/files/src/Domain/Services/FileMoverInterface.ts new file mode 100644 index 000000000..e1b38238d --- /dev/null +++ b/packages/files/src/Domain/Services/FileMoverInterface.ts @@ -0,0 +1,3 @@ +export interface FileMoverInterface { + moveFile(sourcePath: string, destinationPath: string): Promise +} diff --git a/packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSession.ts b/packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSession.ts index d6865fe3c..5affe640b 100644 --- a/packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSession.ts +++ b/packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSession.ts @@ -20,7 +20,7 @@ export class CreateUploadSession implements UseCaseInterface { try { this.logger.debug(`Creating upload session for resource: ${dto.resourceRemoteIdentifier}`) - const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}` + const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}` const uploadId = await this.fileUploader.createUploadSession(filePath) diff --git a/packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSessionDTO.ts b/packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSessionDTO.ts index f6a5597e5..177f502ea 100644 --- a/packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSessionDTO.ts +++ b/packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSessionDTO.ts @@ -1,4 +1,4 @@ export type CreateUploadSessionDTO = { - userUuid: string + ownerUuid: string resourceRemoteIdentifier: string } diff --git a/packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSession.ts b/packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSession.ts index 2fbe5724d..6b6bfe636 100644 --- a/packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSession.ts +++ b/packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSession.ts @@ -24,7 +24,7 @@ export class FinishUploadSession implements UseCaseInterface { try { this.logger.debug(`Finishing upload session for resource: ${dto.resourceRemoteIdentifier}`) - const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}` + const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}` const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath) if (uploadId === undefined) { @@ -53,14 +53,25 @@ export class FinishUploadSession implements UseCaseInterface { await this.fileUploader.finishUploadSession(uploadId, filePath, uploadChunkResults) - await this.domainEventPublisher.publish( - this.domainEventFactory.createFileUploadedEvent({ - userUuid: dto.userUuid, - filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`, - fileName: dto.resourceRemoteIdentifier, - fileByteSize: totalFileSize, - }), - ) + if (dto.ownerType === 'user') { + await this.domainEventPublisher.publish( + this.domainEventFactory.createUserFileUploadedEvent({ + userUuid: dto.ownerUuid, + filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`, + fileName: dto.resourceRemoteIdentifier, + fileByteSize: totalFileSize, + }), + ) + } else { + await this.domainEventPublisher.publish( + this.domainEventFactory.createVaultFileUploadedEvent({ + vaultUuid: dto.ownerUuid, + filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`, + fileName: dto.resourceRemoteIdentifier, + fileByteSize: totalFileSize, + }), + ) + } return { success: true, diff --git a/packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSessionDTO.ts b/packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSessionDTO.ts index 47df0dcad..7a7d338ee 100644 --- a/packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSessionDTO.ts +++ b/packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSessionDTO.ts @@ -1,5 +1,6 @@ export type FinishUploadSessionDTO = { - userUuid: string + ownerUuid: string + ownerType: 'user' | 'vault' resourceRemoteIdentifier: string uploadBytesUsed: number uploadBytesLimit: number diff --git a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts b/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts index 51b0e6ffd..32c1a8f8d 100644 --- a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts +++ b/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts @@ -3,7 +3,7 @@ import { Logger } from 'winston' import TYPES from '../../../Bootstrap/Types' import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface' import { UseCaseInterface } from '../UseCaseInterface' -import { GetFileMetadataDTO, isGetFileMetadataVaultOwned } from './GetFileMetadataDTO' +import { GetFileMetadataDTO } from './GetFileMetadataDTO' import { GetFileMetadataResponse } from './GetFileMetadataResponse' @injectable() @@ -14,16 +14,15 @@ export class GetFileMetadata implements UseCaseInterface { ) {} async execute(dto: GetFileMetadataDTO): Promise { - const ownerUuid = isGetFileMetadataVaultOwned(dto) ? dto.vaultUuid : dto.userUuid try { - const size = await this.fileDownloader.getFileSize(`${ownerUuid}/${dto.resourceRemoteIdentifier}`) + const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`) return { success: true, size, } } catch (error) { - this.logger.error(`Could not get file metadata for resource: ${ownerUuid}/${dto.resourceRemoteIdentifier}`) + this.logger.error(`Could not get file metadata for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`) return { success: false, message: 'Could not get file metadata.', diff --git a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataDTO.ts b/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataDTO.ts index 965fd62ac..de8b5632e 100644 --- a/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataDTO.ts +++ b/packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataDTO.ts @@ -1,15 +1,4 @@ -export type GetFileMetadataDTO = GetVaultFileMetadataDTO | GetUserFileMetadataDTO - -export type GetVaultFileMetadataDTO = { - vaultUuid: string +export type GetFileMetadataDTO = { + ownerUuid: string resourceRemoteIdentifier: string } - -export type GetUserFileMetadataDTO = { - userUuid: string - resourceRemoteIdentifier: string -} - -export function isGetFileMetadataVaultOwned(dto: GetFileMetadataDTO): dto is GetVaultFileMetadataDTO { - return (dto as GetVaultFileMetadataDTO).vaultUuid !== undefined -} diff --git a/packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved.ts b/packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved.ts index a6c7af331..54287c474 100644 --- a/packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved.ts +++ b/packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved.ts @@ -16,16 +16,16 @@ export class MarkFilesToBeRemoved implements UseCaseInterface { async execute(dto: MarkFilesToBeRemovedDTO): Promise { try { - this.logger.debug(`Marking files for later removal for user: ${dto.userUuid}`) + this.logger.debug(`Marking files for later removal for user: ${dto.ownerUuid}`) - const filesRemoved = await this.fileRemover.markFilesToBeRemoved(dto.userUuid) + const filesRemoved = await this.fileRemover.markFilesToBeRemoved(dto.ownerUuid) return { success: true, filesRemoved, } } catch (error) { - this.logger.error(`Could not mark resources for removal: ${dto.userUuid} - ${(error as Error).message}`) + this.logger.error(`Could not mark resources for removal: ${dto.ownerUuid} - ${(error as Error).message}`) return { success: false, diff --git a/packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemovedDTO.ts b/packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemovedDTO.ts index ae37df330..2b61ae453 100644 --- a/packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemovedDTO.ts +++ b/packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemovedDTO.ts @@ -1,3 +1,3 @@ export type MarkFilesToBeRemovedDTO = { - userUuid: string + ownerUuid: string } diff --git a/packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts b/packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts new file mode 100644 index 000000000..98d498520 --- /dev/null +++ b/packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts @@ -0,0 +1,35 @@ +import 'reflect-metadata' + +import { Logger } from 'winston' + +import { MoveFile } from './MoveFile' +import { FileMoverInterface } from '../../Services/FileMoverInterface' + +describe('MoveFile', () => { + let fileRemover: FileMoverInterface + + let logger: Logger + + const createUseCase = () => new MoveFile(fileRemover, logger) + + beforeEach(() => { + fileRemover = {} as jest.Mocked + fileRemover.moveFile = jest.fn().mockReturnValue(413) + + logger = {} as jest.Mocked + logger.debug = jest.fn() + logger.error = jest.fn() + logger.warn = jest.fn() + }) + + it('should remove a file', async () => { + await createUseCase().execute({ + resourceRemoteIdentifier: '2-3-4', + fromUserUuid: '1-2-3', + toVaultUuid: '4-5-6', + regularSubscriptionUuid: '3-4-5', + }) + + expect(fileRemover.moveFile).toHaveBeenCalledWith('1-2-3/2-3-4', '4-5-6/2-3-4') + }) +}) diff --git a/packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts b/packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts new file mode 100644 index 000000000..842740f96 --- /dev/null +++ b/packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts @@ -0,0 +1,43 @@ +import { inject, injectable } from 'inversify' +import { Logger } from 'winston' + +import TYPES from '../../../Bootstrap/Types' +import { FileMoverInterface } from '../../Services/FileMoverInterface' +import { UseCaseInterface } from '../UseCaseInterface' +import { MoveFileDTO, isMoveFileFromUserToVaultDTO } from './MoveFileDTO' +import { MoveFileResponse } from './MoveFileResponse' + +@injectable() +export class MoveFile implements UseCaseInterface { + constructor( + @inject(TYPES.FileMover) private fileMover: FileMoverInterface, + @inject(TYPES.Logger) private logger: Logger, + ) {} + + async execute(dto: MoveFileDTO): Promise { + try { + this.logger.debug(`Removing file: ${dto.resourceRemoteIdentifier}`) + + const srcPath = isMoveFileFromUserToVaultDTO(dto) + ? `${dto.fromUserUuid}/${dto.resourceRemoteIdentifier}` + : `${dto.fromVaultUuid}/${dto.resourceRemoteIdentifier}` + + const destPath = isMoveFileFromUserToVaultDTO(dto) + ? `${dto.toVaultUuid}/${dto.resourceRemoteIdentifier}` + : `${dto.toUserUuid}/${dto.resourceRemoteIdentifier}` + + await this.fileMover.moveFile(srcPath, destPath) + + return { + success: true, + } + } catch (error) { + this.logger.error(`Could not remove resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`) + + return { + success: false, + message: 'Could not remove resource', + } + } + } +} diff --git a/packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts b/packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts new file mode 100644 index 000000000..c114aab4a --- /dev/null +++ b/packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts @@ -0,0 +1,19 @@ +export type MoveFileDTO = MoveFileFromUserToVaultDTO | MoveFileFromVaultToUserDTO + +export type MoveFileFromUserToVaultDTO = { + toVaultUuid: string + fromUserUuid: string + resourceRemoteIdentifier: string + regularSubscriptionUuid: string +} + +export type MoveFileFromVaultToUserDTO = { + fromVaultUuid: string + toUserUuid: string + resourceRemoteIdentifier: string + regularSubscriptionUuid: string +} + +export function isMoveFileFromUserToVaultDTO(dto: MoveFileDTO): dto is MoveFileFromUserToVaultDTO { + return (dto as MoveFileFromUserToVaultDTO).fromUserUuid !== undefined +} diff --git a/packages/files/src/Domain/UseCase/MoveFile/MoveFileResponse.ts b/packages/files/src/Domain/UseCase/MoveFile/MoveFileResponse.ts new file mode 100644 index 000000000..9c82f9738 --- /dev/null +++ b/packages/files/src/Domain/UseCase/MoveFile/MoveFileResponse.ts @@ -0,0 +1,8 @@ +export type MoveFileResponse = + | { + success: true + } + | { + success: false + message: string + } diff --git a/packages/files/src/Domain/UseCase/RemoveFile/RemoveFile.ts b/packages/files/src/Domain/UseCase/RemoveFile/RemoveFile.ts index 4e75b09f1..66badd0f2 100644 --- a/packages/files/src/Domain/UseCase/RemoveFile/RemoveFile.ts +++ b/packages/files/src/Domain/UseCase/RemoveFile/RemoveFile.ts @@ -6,7 +6,7 @@ import TYPES from '../../../Bootstrap/Types' import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface' import { FileRemoverInterface } from '../../Services/FileRemoverInterface' import { UseCaseInterface } from '../UseCaseInterface' -import { RemoveFileDTO } from './RemoveFileDTO' +import { RemoveFileDTO, isRemoveUserFileDTO } from './RemoveFileDTO' import { RemoveFileResponse } from './RemoveFileResponse' @injectable() @@ -22,19 +22,32 @@ export class RemoveFile implements UseCaseInterface { try { this.logger.debug(`Removing file: ${dto.resourceRemoteIdentifier}`) - const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}` + const ownerUuid = isRemoveUserFileDTO(dto) ? dto.userUuid : dto.vaultUuid + + const filePath = `${ownerUuid}/${dto.resourceRemoteIdentifier}` const removedFileSize = await this.fileRemover.remove(filePath) - await this.domainEventPublisher.publish( - this.domainEventFactory.createFileRemovedEvent({ - userUuid: dto.userUuid, - filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`, - fileName: dto.resourceRemoteIdentifier, - fileByteSize: removedFileSize, - regularSubscriptionUuid: dto.regularSubscriptionUuid, - }), - ) + if (isRemoveUserFileDTO(dto)) { + await this.domainEventPublisher.publish( + this.domainEventFactory.createUserFileRemovedEvent({ + userUuid: dto.userUuid, + filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`, + fileName: dto.resourceRemoteIdentifier, + fileByteSize: removedFileSize, + regularSubscriptionUuid: dto.regularSubscriptionUuid, + }), + ) + } else { + await this.domainEventPublisher.publish( + this.domainEventFactory.createVaultFileRemovedEvent({ + vaultUuid: dto.vaultUuid, + filePath: `${dto.vaultUuid}/${dto.resourceRemoteIdentifier}`, + fileName: dto.resourceRemoteIdentifier, + fileByteSize: removedFileSize, + }), + ) + } return { success: true, diff --git a/packages/files/src/Domain/UseCase/RemoveFile/RemoveFileDTO.ts b/packages/files/src/Domain/UseCase/RemoveFile/RemoveFileDTO.ts index 84c951bd2..9b655dc8d 100644 --- a/packages/files/src/Domain/UseCase/RemoveFile/RemoveFileDTO.ts +++ b/packages/files/src/Domain/UseCase/RemoveFile/RemoveFileDTO.ts @@ -1,5 +1,16 @@ -export type RemoveFileDTO = { +export type RemoveFileDTO = RemoveUserFileDTO | RemoveVaultFileDTO + +export type RemoveUserFileDTO = { userUuid: string resourceRemoteIdentifier: string regularSubscriptionUuid: string } + +export type RemoveVaultFileDTO = { + vaultUuid: string + resourceRemoteIdentifier: string +} + +export function isRemoveUserFileDTO(dto: RemoveFileDTO): dto is RemoveUserFileDTO { + return (dto as RemoveUserFileDTO).userUuid !== undefined +} diff --git a/packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFile.ts b/packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFile.ts index 5578c5b59..86a313643 100644 --- a/packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFile.ts +++ b/packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFile.ts @@ -3,7 +3,7 @@ import { Logger } from 'winston' import TYPES from '../../../Bootstrap/Types' import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface' import { UseCaseInterface } from '../UseCaseInterface' -import { StreamDownloadFileDTO, isStreamDownloadFileVaultOwned } from './StreamDownloadFileDTO' +import { StreamDownloadFileDTO } from './StreamDownloadFileDTO' import { StreamDownloadFileResponse } from './StreamDownloadFileResponse' @injectable() @@ -14,10 +14,9 @@ export class StreamDownloadFile implements UseCaseInterface { ) {} async execute(dto: StreamDownloadFileDTO): Promise { - const ownerUuid = isStreamDownloadFileVaultOwned(dto) ? dto.vaultUuid : dto.userUuid try { const readStream = await this.fileDownloader.createDownloadStream( - `${ownerUuid}/${dto.resourceRemoteIdentifier}`, + `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`, dto.startRange, dto.endRange, ) @@ -27,7 +26,9 @@ export class StreamDownloadFile implements UseCaseInterface { readStream, } } catch (error) { - this.logger.error(`Could not create a download stream for resource: ${ownerUuid}/${dto.resourceRemoteIdentifier}`) + this.logger.error( + `Could not create a download stream for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`, + ) return { success: false, diff --git a/packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFileDTO.ts b/packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFileDTO.ts index abcd6fd17..664fa5bf9 100644 --- a/packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFileDTO.ts +++ b/packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFileDTO.ts @@ -1,19 +1,6 @@ -export type StreamDownloadFileDTO = StreamDownloadVaultFileDTO | StreamDownloadUserFileDTO - -export type StreamDownloadVaultFileDTO = { - vaultUuid: string +export type StreamDownloadFileDTO = { + ownerUuid: string resourceRemoteIdentifier: string startRange: number endRange: number } - -export type StreamDownloadUserFileDTO = { - userUuid: string - resourceRemoteIdentifier: string - startRange: number - endRange: number -} - -export function isStreamDownloadFileVaultOwned(dto: StreamDownloadFileDTO): dto is StreamDownloadVaultFileDTO { - return (dto as StreamDownloadVaultFileDTO).vaultUuid !== undefined -} diff --git a/packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunk.ts b/packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunk.ts index 5a177a55a..118c416b4 100644 --- a/packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunk.ts +++ b/packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunk.ts @@ -22,7 +22,7 @@ export class UploadFileChunk implements UseCaseInterface { `Starting upload file chunk ${dto.chunkId} with ${dto.data.byteLength} bytes for resource: ${dto.resourceRemoteIdentifier}`, ) - const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}` + const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}` const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath) if (uploadId === undefined) { diff --git a/packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunkDTO.ts b/packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunkDTO.ts index 911aed7d5..8019d1456 100644 --- a/packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunkDTO.ts +++ b/packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunkDTO.ts @@ -3,7 +3,7 @@ import { ChunkId } from '../../Upload/ChunkId' export type UploadFileChunkDTO = { data: Uint8Array chunkId: ChunkId - userUuid: string + ownerUuid: string resourceRemoteIdentifier: string resourceUnencryptedFileSize: number } diff --git a/packages/files/src/Infra/FS/FSFileMover.ts b/packages/files/src/Infra/FS/FSFileMover.ts new file mode 100644 index 000000000..e4baefbeb --- /dev/null +++ b/packages/files/src/Infra/FS/FSFileMover.ts @@ -0,0 +1,17 @@ +import { inject, injectable } from 'inversify' +import { promises } from 'fs' + +import { FileMoverInterface } from '../../Domain/Services/FileMoverInterface' +import TYPES from '../../Bootstrap/Types' + +@injectable() +export class FSFileMover implements FileMoverInterface { + constructor(@inject(TYPES.FILE_UPLOAD_PATH) private fileUploadPath: string) {} + + async moveFile(sourcePath: string, destinationPath: string): Promise { + const sourceFullPath = `${this.fileUploadPath}/${sourcePath}` + const destinationFullPath = `${this.fileUploadPath}/${destinationPath}` + + await promises.rename(sourceFullPath, destinationFullPath) + } +} diff --git a/packages/files/src/Infra/S3/S3FileMover.ts b/packages/files/src/Infra/S3/S3FileMover.ts new file mode 100644 index 000000000..863bc58f3 --- /dev/null +++ b/packages/files/src/Infra/S3/S3FileMover.ts @@ -0,0 +1,30 @@ +import { inject, injectable } from 'inversify' +import { CopyObjectCommand, DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3' + +import TYPES from '../../Bootstrap/Types' +import { FileMoverInterface } from '../../Domain/Services/FileMoverInterface' + +@injectable() +export class S3FileMover implements FileMoverInterface { + constructor( + @inject(TYPES.S3) private s3Client: S3Client, + @inject(TYPES.S3_BUCKET_NAME) private s3BucketName: string, + ) {} + + async moveFile(sourcePath: string, destinationPath: string): Promise { + await this.s3Client.send( + new CopyObjectCommand({ + Bucket: this.s3BucketName, + CopySource: `${this.s3BucketName}/${sourcePath}`, + Key: destinationPath, + }), + ) + + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.s3BucketName, + Key: sourcePath, + }), + ) + } +} diff --git a/packages/security/src/Domain/Token/ValetTokenOperation.ts b/packages/security/src/Domain/Token/ValetTokenOperation.ts index e785e3820..6136ce7bd 100644 --- a/packages/security/src/Domain/Token/ValetTokenOperation.ts +++ b/packages/security/src/Domain/Token/ValetTokenOperation.ts @@ -2,4 +2,5 @@ export enum ValetTokenOperation { Read = 'read', Write = 'write', Delete = 'delete', + Move = 'move', } diff --git a/packages/security/src/Domain/Token/VaultValetTokenData.ts b/packages/security/src/Domain/Token/VaultValetTokenData.ts index fd1c7a0af..8d444106b 100644 --- a/packages/security/src/Domain/Token/VaultValetTokenData.ts +++ b/packages/security/src/Domain/Token/VaultValetTokenData.ts @@ -4,4 +4,11 @@ export type VaultValetTokenData = { vaultUuid: string permittedOperation: ValetTokenOperation remoteIdentifier: string + unencryptedFileSize?: number + uploadBytesUsed: number + uploadBytesLimit: number + moveOperation?: { + type: 'vault-to-user' | 'user-to-vault' + userUuid: string + } } diff --git a/packages/syncing-server/migrations/mysql/1684173017359-create_vault.ts b/packages/syncing-server/migrations/mysql/1684173017359-create_vault.ts index 3f1ce7b73..3ad8e3328 100644 --- a/packages/syncing-server/migrations/mysql/1684173017359-create_vault.ts +++ b/packages/syncing-server/migrations/mysql/1684173017359-create_vault.ts @@ -29,6 +29,14 @@ export class CreateVault1684173017359 implements MigrationInterface { name: 'vault_key_timestamp', type: 'bigint', }), + new TableColumn({ + name: 'file_upload_bytes_used', + type: 'bigint', + }), + new TableColumn({ + name: 'file_upload_bytes_limit', + type: 'bigint', + }), new TableColumn({ name: 'created_at_timestamp', type: 'bigint', diff --git a/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts b/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts index 893283fd7..d180231bb 100644 --- a/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts +++ b/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts @@ -47,7 +47,7 @@ import { ContactServiceInterface } from '../Domain/Contact/Service/ContactServic import { ContactService } from '../Domain/Contact/Service/ContactService' import { ContactFactoryInterface } from '../Domain/Contact/Factory/ContactFactoryInterface' import { SnjsVersionFilter } from '../Domain/Item/SaveRule/SnjsVersionFilter' -import { CreateVaultFileReadValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetToken' +import { CreateVaultFileValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken' export class ServerContainerConfigLoader extends CommonContainerConfigLoader { private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 @@ -115,10 +115,11 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader { }) container - .bind(TYPES.CreateVaultFileReadValetToken) + .bind(TYPES.CreateVaultFileReadValetToken) .toDynamicValue((context: interfaces.Context) => { - return new CreateVaultFileReadValetToken( + return new CreateVaultFileValetToken( context.container.get(TYPES.GetGlobalItem), + context.container.get(TYPES.VaultService), context.container.get(TYPES.VaultUserService), context.container.get(TYPES.ValetTokenEncoder), context.container.get(TYPES.VALET_TOKEN_TTL), diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index 9b3764158..85b0a6b19 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -78,6 +78,8 @@ const TYPES = { DuplicateItemSyncedEventHandler: Symbol.for('DuplicateItemSyncedEventHandler'), EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'), ItemRevisionCreationRequestedEventHandler: Symbol.for('ItemRevisionCreationRequestedEventHandler'), + VaultFileUploadedEventHandler: Symbol.for('VaultFileUploadedEventHandler'), + VaultFileRemovedEventHandler: Symbol.for('VaultFileRemovedEventHandler'), ContentDecoder: Symbol.for('ContentDecoder'), DomainEventPublisher: Symbol.for('DomainEventPublisher'), diff --git a/packages/syncing-server/src/Bootstrap/WorkerContainerConfigLoader.ts b/packages/syncing-server/src/Bootstrap/WorkerContainerConfigLoader.ts index 30216290f..07e409188 100644 --- a/packages/syncing-server/src/Bootstrap/WorkerContainerConfigLoader.ts +++ b/packages/syncing-server/src/Bootstrap/WorkerContainerConfigLoader.ts @@ -31,6 +31,8 @@ import { ItemRevisionCreationRequestedEventHandler } from '../Domain/Handler/Ite import { FSItemBackupService } from '../Infra/FS/FSItemBackupService' import { CommonContainerConfigLoader } from './CommonContainerConfigLoader' import { UserCredentialsChangedEventHandler } from '../Domain/Handler/UserCredentialsChangedEventHandler' +import { VaultFileUploadedEventHandler } from '../Domain/Handler/VaultFileUploadedEventHandler' +import { VaultFileRemovedEventHandler } from '../Domain/Handler/VaultFileRemovedEventHandler' export class WorkerContainerConfigLoader extends CommonContainerConfigLoader { private readonly DEFAULT_FILE_UPLOAD_PATH = `${__dirname}/../../uploads` @@ -143,6 +145,9 @@ export class WorkerContainerConfigLoader extends CommonContainerConfigLoader { ) }) + container.bind(TYPES.VaultFileUploadedEventHandler).to(VaultFileUploadedEventHandler) + container.bind(TYPES.VaultFileRemovedEventHandler).to(VaultFileRemovedEventHandler) + // Services container.bind(TYPES.ContentDecoder).toDynamicValue(() => new ContentDecoder()) container.bind(TYPES.HTTPClient).toDynamicValue(() => axios.create()) @@ -194,6 +199,8 @@ export class WorkerContainerConfigLoader extends CommonContainerConfigLoader { ['EMAIL_BACKUP_REQUESTED', context.container.get(TYPES.EmailBackupRequestedEventHandler)], ['ITEM_REVISION_CREATION_REQUESTED', context.container.get(TYPES.ItemRevisionCreationRequestedEventHandler)], ['USER_CREDENTIALS_CHANGED', container.get(TYPES.UserCredentialsChangedEventHandler)], + ['VAULT_FILE_REMOVED', container.get(TYPES.VaultFileRemovedEventHandler)], + ['VAULT_FILE_UPLOADED', container.get(TYPES.VaultFileUploadedEventHandler)], ]) const handler = diff --git a/packages/syncing-server/src/Controller/VaultsController.ts b/packages/syncing-server/src/Controller/VaultsController.ts index 323362ee8..3405b241f 100644 --- a/packages/syncing-server/src/Controller/VaultsController.ts +++ b/packages/syncing-server/src/Controller/VaultsController.ts @@ -17,7 +17,7 @@ import { Vault } from '../Domain/Vault/Model/Vault' import { VaultProjection } from '../Projection/VaultProjection' import { VaultUserProjection } from '../Projection/VaultUserProjection' import { VaultUserServiceInterface } from '../Domain/VaultUser/Service/VaultUserServiceInterface' -import { CreateVaultFileReadValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetToken' +import { CreateVaultFileValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken' @controller('/vaults') export class VaultsController extends BaseHttpController { @@ -26,7 +26,7 @@ export class VaultsController extends BaseHttpController { @inject(TYPES.VaultUserService) private vaultUserService: VaultUserServiceInterface, @inject(TYPES.VaultProjector) private vaultProjector: ProjectorInterface, @inject(TYPES.VaultUserProjector) private vaultUserProjector: ProjectorInterface, - @inject(TYPES.CreateVaultFileReadValetToken) private createVaultFileReadValetToken: CreateVaultFileReadValetToken, + @inject(TYPES.CreateVaultFileReadValetToken) private createVaultFileReadValetToken: CreateVaultFileValetToken, ) { super() } @@ -52,6 +52,8 @@ export class VaultsController extends BaseHttpController { vaultUuid: request.params.vaultUuid, fileUuid: request.body.file_uuid, remoteIdentifier: request.body.remote_identifier, + moveOperationType: request.body.move_operation_type, + operation: request.body.operation, }) if (valetTokenResult.success === false) { diff --git a/packages/syncing-server/src/Domain/Handler/VaultFileRemovedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/VaultFileRemovedEventHandler.ts new file mode 100644 index 000000000..42a136ca3 --- /dev/null +++ b/packages/syncing-server/src/Domain/Handler/VaultFileRemovedEventHandler.ts @@ -0,0 +1,33 @@ +import { DomainEventHandlerInterface, VaultFileRemovedEvent } from '@standardnotes/domain-events' + +import { inject, injectable } from 'inversify' +import { Logger } from 'winston' + +import TYPES from '../../Bootstrap/Types' +import { VaultServiceInterface } from '../Vault/Service/VaultServiceInterface' +import { VaultsRepositoryInterface } from '../Vault/Repository/VaultRepositoryInterface' + +@injectable() +export class VaultFileRemovedEventHandler implements DomainEventHandlerInterface { + constructor( + @inject(TYPES.VaultService) private vaultService: VaultServiceInterface, + @inject(TYPES.VaultRepository) private vaultRepository: VaultsRepositoryInterface, + @inject(TYPES.Logger) private logger: Logger, + ) {} + + async handle(event: VaultFileRemovedEvent): Promise { + const vault = await this.vaultService.getVault({ + vaultUuid: event.payload.vaultUuid, + }) + + if (vault === null) { + this.logger.warn(`Could not find vault with uuid: ${event.payload.vaultUuid}`) + + return + } + + vault.fileUploadBytesUsed -= event.payload.fileByteSize + + await this.vaultRepository.save(vault) + } +} diff --git a/packages/syncing-server/src/Domain/Handler/VaultFileUploadedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/VaultFileUploadedEventHandler.ts new file mode 100644 index 000000000..1d6925fba --- /dev/null +++ b/packages/syncing-server/src/Domain/Handler/VaultFileUploadedEventHandler.ts @@ -0,0 +1,33 @@ +import { DomainEventHandlerInterface, VaultFileUploadedEvent } from '@standardnotes/domain-events' + +import { inject, injectable } from 'inversify' +import { Logger } from 'winston' + +import TYPES from '../../Bootstrap/Types' +import { VaultServiceInterface } from '../Vault/Service/VaultServiceInterface' +import { VaultsRepositoryInterface } from '../Vault/Repository/VaultRepositoryInterface' + +@injectable() +export class VaultFileUploadedEventHandler implements DomainEventHandlerInterface { + constructor( + @inject(TYPES.VaultService) private vaultService: VaultServiceInterface, + @inject(TYPES.VaultRepository) private vaultRepository: VaultsRepositoryInterface, + @inject(TYPES.Logger) private logger: Logger, + ) {} + + async handle(event: VaultFileUploadedEvent): Promise { + const vault = await this.vaultService.getVault({ + vaultUuid: event.payload.vaultUuid, + }) + + if (vault === null) { + this.logger.warn(`Could not find vault with uuid: ${event.payload.vaultUuid}`) + + return + } + + vault.fileUploadBytesUsed += event.payload.fileByteSize + + await this.vaultRepository.save(vault) + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetToken.ts b/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetToken.ts deleted file mode 100644 index d358bc2b5..000000000 --- a/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetToken.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { inject, injectable } from 'inversify' -import { VaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security' -import { CreateValetTokenResponseData } from '@standardnotes/responses' -import TYPES from '../../../Bootstrap/Types' -import { UseCaseInterface } from '../UseCaseInterface' -import { CreateVaultFileReadValetTokenDTO } from './CreateVaultFileReadValetTokenDTO' -import { GetGlobalItem } from '../GetGlobalItem/GetGlobalItem' -import { VaultUserServiceInterface } from '../../VaultUser/Service/VaultUserServiceInterface' - -@injectable() -export class CreateVaultFileReadValetToken implements UseCaseInterface { - constructor( - @inject(TYPES.GetGlobalItem) private getGlobalItem: GetGlobalItem, - @inject(TYPES.VaultUserService) private vaultUserService: VaultUserServiceInterface, - @inject(TYPES.ValetTokenEncoder) private tokenEncoder: TokenEncoderInterface, - @inject(TYPES.VALET_TOKEN_TTL) private valetTokenTTL: number, - ) {} - - async execute(dto: CreateVaultFileReadValetTokenDTO): Promise { - const itemResponse = await this.getGlobalItem.execute({ - itemUuid: dto.fileUuid, - }) - - if ( - itemResponse.success === false || - !itemResponse.item.vaultUuid || - itemResponse.item.vaultUuid !== dto.vaultUuid - ) { - return { - success: false, - reason: 'invalid-parameters', - } - } - - const vaultUser = await this.vaultUserService.getUserForVault({ - userUuid: dto.userUuid, - vaultUuid: dto.vaultUuid, - }) - - if (!vaultUser) { - return { - success: false, - reason: 'invalid-parameters', - } - } - - const tokenData: VaultValetTokenData = { - vaultUuid: dto.vaultUuid, - permittedOperation: ValetTokenOperation.Read, - remoteIdentifier: dto.remoteIdentifier, - } - - const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL) - - return { success: true, valetToken } - } -} diff --git a/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetTokenDTO.ts b/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetTokenDTO.ts deleted file mode 100644 index cf23e5a26..000000000 --- a/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetTokenDTO.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type CreateVaultFileReadValetTokenDTO = { - userUuid: string - vaultUuid: string - fileUuid: string - remoteIdentifier: string -} diff --git a/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken.ts b/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken.ts new file mode 100644 index 000000000..f7bad547e --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken.ts @@ -0,0 +1,96 @@ +import { inject, injectable } from 'inversify' +import { VaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security' +import { CreateValetTokenResponseData } from '@standardnotes/responses' +import TYPES from '../../../Bootstrap/Types' +import { UseCaseInterface } from '../UseCaseInterface' +import { CreateVaultFileValetTokenDTO } from './CreateVaultFileValetTokenDTO' +import { GetGlobalItem } from '../GetGlobalItem/GetGlobalItem' +import { VaultUserServiceInterface } from '../../VaultUser/Service/VaultUserServiceInterface' +import { VaultServiceInterface } from '../../Vault/Service/VaultServiceInterface' + +@injectable() +export class CreateVaultFileValetToken implements UseCaseInterface { + constructor( + @inject(TYPES.GetGlobalItem) private getGlobalItem: GetGlobalItem, + @inject(TYPES.VaultService) private vaultService: VaultServiceInterface, + @inject(TYPES.VaultUserService) private vaultUserService: VaultUserServiceInterface, + @inject(TYPES.ValetTokenEncoder) private tokenEncoder: TokenEncoderInterface, + @inject(TYPES.VALET_TOKEN_TTL) private valetTokenTTL: number, + ) {} + + async execute(dto: CreateVaultFileValetTokenDTO): Promise { + const itemResponse = await this.getGlobalItem.execute({ + itemUuid: dto.fileUuid, + }) + + const failValue: CreateValetTokenResponseData = { + success: false, + reason: 'invalid-parameters', + } + + if (itemResponse.success === false) { + return failValue + } + + if (!itemResponse.item.vaultUuid || itemResponse.item.vaultUuid !== dto.vaultUuid) { + return failValue + } + + const vaultUser = await this.vaultUserService.getUserForVault({ + userUuid: dto.userUuid, + vaultUuid: dto.vaultUuid, + }) + + if (!vaultUser) { + return failValue + } + + const vault = await this.vaultService.getVault({ + vaultUuid: dto.vaultUuid, + }) + + if (!vault) { + return failValue + } + + const tokenData: VaultValetTokenData = { + vaultUuid: dto.vaultUuid, + permittedOperation: dto.operation, + remoteIdentifier: dto.remoteIdentifier, + uploadBytesUsed: vault.fileUploadBytesUsed, + uploadBytesLimit: vault.fileUploadBytesLimit, + } + + if (dto.operation === ValetTokenOperation.Move) { + if (!dto.moveOperationType) { + return failValue + } + + if (dto.moveOperationType === 'vault-to-user') { + if (vaultUser.permissions === 'read') { + return failValue + } else { + tokenData.moveOperation = { + type: 'vault-to-user', + userUuid: dto.userUuid, + } + } + } else if (dto.moveOperationType === 'user-to-vault') { + if (vaultUser.permissions === 'read') { + return failValue + } else { + tokenData.moveOperation = { + type: 'user-to-vault', + userUuid: dto.userUuid, + } + } + } else { + return failValue + } + } + + const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL) + + return { success: true, valetToken } + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetTokenDTO.ts b/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetTokenDTO.ts new file mode 100644 index 000000000..9bee07260 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetTokenDTO.ts @@ -0,0 +1,10 @@ +import { ValetTokenOperation } from '@standardnotes/security' + +export type CreateVaultFileValetTokenDTO = { + userUuid: string + vaultUuid: string + fileUuid: string + remoteIdentifier: string + operation: ValetTokenOperation + moveOperationType?: 'vault-to-user' | 'user-to-vault' +} diff --git a/packages/syncing-server/src/Domain/Vault/Factory/VaultHash.ts b/packages/syncing-server/src/Domain/Vault/Factory/VaultHash.ts index 11e0289b2..2bce670cc 100644 --- a/packages/syncing-server/src/Domain/Vault/Factory/VaultHash.ts +++ b/packages/syncing-server/src/Domain/Vault/Factory/VaultHash.ts @@ -3,6 +3,8 @@ export type VaultHash = { user_uuid: string specified_items_key_uuid: string vault_key_timestamp: number + file_upload_bytes_used: number + file_upload_bytes_limit: number created_at_timestamp?: number updated_at_timestamp?: number } diff --git a/packages/syncing-server/src/Domain/Vault/Model/Vault.ts b/packages/syncing-server/src/Domain/Vault/Model/Vault.ts index 9ff267f8f..3f043ad70 100644 --- a/packages/syncing-server/src/Domain/Vault/Model/Vault.ts +++ b/packages/syncing-server/src/Domain/Vault/Model/Vault.ts @@ -24,6 +24,18 @@ export class Vault { }) declare vaultKeyTimestamp: number + @Column({ + name: 'file_upload_bytes_used', + type: 'bigint', + }) + declare fileUploadBytesUsed: number + + @Column({ + name: 'file_upload_bytes_limit', + type: 'bigint', + }) + declare fileUploadBytesLimit: number + @Column({ name: 'created_at_timestamp', type: 'bigint', diff --git a/packages/syncing-server/src/Domain/Vault/Service/VaultService.ts b/packages/syncing-server/src/Domain/Vault/Service/VaultService.ts index 455f5bf24..48fd47177 100644 --- a/packages/syncing-server/src/Domain/Vault/Service/VaultService.ts +++ b/packages/syncing-server/src/Domain/Vault/Service/VaultService.ts @@ -29,6 +29,8 @@ export class VaultService implements VaultServiceInterface { user_uuid: dto.userUuid, specified_items_key_uuid: dto.specifiedItemsKeyUuid, vault_key_timestamp: dto.vaultKeyTimestamp, + file_upload_bytes_limit: 1_000_000, + file_upload_bytes_used: 0, created_at_timestamp: timestamp, updated_at_timestamp: timestamp, }, diff --git a/packages/syncing-server/src/Projection/VaultProjection.ts b/packages/syncing-server/src/Projection/VaultProjection.ts index 7988ae418..47cb1470b 100644 --- a/packages/syncing-server/src/Projection/VaultProjection.ts +++ b/packages/syncing-server/src/Projection/VaultProjection.ts @@ -3,6 +3,8 @@ export type VaultProjection = { user_uuid: string specified_items_key_uuid: string vault_key_timestamp: number + file_upload_bytes_used: number + file_upload_bytes_limit: number created_at_timestamp: number updated_at_timestamp: number } diff --git a/packages/syncing-server/src/Projection/VaultProjector.ts b/packages/syncing-server/src/Projection/VaultProjector.ts index c692eab61..59b0da391 100644 --- a/packages/syncing-server/src/Projection/VaultProjector.ts +++ b/packages/syncing-server/src/Projection/VaultProjector.ts @@ -22,6 +22,8 @@ export class VaultProjector implements ProjectorInterface