refactor: vault files

This commit is contained in:
Mo
2023-05-28 14:11:31 -05:00
parent 690f18708a
commit 94deab2ee0
69 changed files with 761 additions and 212 deletions
+6 -6
View File
@@ -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<SubscriptionReassignedEventHandler>(TYPES.SubscriptionReassignedEventHandler)
.to(SubscriptionReassignedEventHandler)
container.bind<UserEmailChangedEventHandler>(TYPES.UserEmailChangedEventHandler).to(UserEmailChangedEventHandler)
container.bind<FileUploadedEventHandler>(TYPES.FileUploadedEventHandler).to(FileUploadedEventHandler)
container.bind<FileRemovedEventHandler>(TYPES.FileRemovedEventHandler).to(FileRemovedEventHandler)
container.bind<UserFileUploadedEventHandler>(TYPES.UserFileUploadedEventHandler).to(UserFileUploadedEventHandler)
container.bind<UserFileRemovedEventHandler>(TYPES.UserFileRemovedEventHandler).to(UserFileRemovedEventHandler)
container
.bind<ListedAccountCreatedEventHandler>(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)],
[
+2 -2
View File
@@ -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'),
@@ -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 {
@@ -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<FileRemovedEvent>
event = {} as jest.Mocked<UserFileRemovedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '1-2-3',
@@ -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<void> {
async handle(event: UserFileRemovedEvent): Promise<void> {
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(event.payload.userUuid)
if (regularSubscription === null) {
@@ -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<FileUploadedEvent>
event = {} as jest.Mocked<UserFileUploadedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '1-2-3',
@@ -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<void> {
async handle(event: UserFileUploadedEvent): Promise<void> {
const user = await this.userRepository.findOneByUuid(event.payload.userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${event.payload.userUuid}`)
@@ -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 {
@@ -0,0 +1,7 @@
export type CreateValetTokenPayload = {
operation: 'read' | 'write' | 'delete' | 'move'
resources: Array<{
remoteIdentifier: string
unencryptedFileSize?: number
}>
}
@@ -1,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { FileRemovedEventPayload } from './FileRemovedEventPayload'
export interface FileRemovedEvent extends DomainEventInterface {
type: 'FILE_REMOVED'
payload: FileRemovedEventPayload
}
@@ -1,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { FileUploadedEventPayload } from './FileUploadedEventPayload'
export interface FileUploadedEvent extends DomainEventInterface {
type: 'FILE_UPLOADED'
payload: FileUploadedEventPayload
}
@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { UserFileRemovedEventPayload } from './UserFileRemovedEventPayload'
export interface UserFileRemovedEvent extends DomainEventInterface {
type: 'USER_FILE_REMOVED'
payload: UserFileRemovedEventPayload
}
@@ -1,4 +1,4 @@
export interface FileRemovedEventPayload {
export interface UserFileRemovedEventPayload {
userUuid: string
regularSubscriptionUuid: string
fileByteSize: number
@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { UserFileUploadedEventPayload } from './UserFileUploadedEventPayload'
export interface UserFileUploadedEvent extends DomainEventInterface {
type: 'USER_FILE_UPLOADED'
payload: UserFileUploadedEventPayload
}
@@ -1,4 +1,4 @@
export interface FileUploadedEventPayload {
export interface UserFileUploadedEventPayload {
userUuid: string
fileByteSize: number
filePath: string
@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { VaultFileRemovedEventPayload } from './VaultFileRemovedEventPayload'
export interface VaultFileRemovedEvent extends DomainEventInterface {
type: 'VAULT_FILE_REMOVED'
payload: VaultFileRemovedEventPayload
}
@@ -0,0 +1,6 @@
export interface VaultFileRemovedEventPayload {
vaultUuid: string
fileByteSize: number
filePath: string
fileName: string
}
@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { VaultFileUploadedEventPayload } from './VaultFileUploadedEventPayload'
export interface VaultFileUploadedEvent extends DomainEventInterface {
type: 'VAULT_FILE_UPLOADED'
payload: VaultFileUploadedEventPayload
}
@@ -0,0 +1,6 @@
export interface VaultFileUploadedEventPayload {
vaultUuid: string
fileByteSize: number
filePath: string
fileName: string
}
+11 -4
View File
@@ -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'
@@ -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)],
@@ -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<Container> {
@@ -98,12 +102,14 @@ export class ContainerConfigLoader {
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(S3FileDownloader)
container.bind<FileUploaderInterface>(TYPES.FileUploader).to(S3FileUploader)
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(S3FileRemover)
container.bind<FileMoverInterface>(TYPES.FileMover).to(S3FileMover)
} else {
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(FSFileDownloader)
container
.bind<FileUploaderInterface>(TYPES.FileUploader)
.toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger)))
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(FSFileRemover)
container.bind<FileMoverInterface>(TYPES.FileMover).to(FSFileMover)
}
if (env.get('SNS_TOPIC_ARN', true)) {
@@ -146,6 +152,7 @@ export class ContainerConfigLoader {
container.bind<FinishUploadSession>(TYPES.FinishUploadSession).to(FinishUploadSession)
container.bind<GetFileMetadata>(TYPES.GetFileMetadata).to(GetFileMetadata)
container.bind<RemoveFile>(TYPES.RemoveFile).to(RemoveFile)
container.bind<MoveFile>(TYPES.MoveFile).to(MoveFile)
container.bind<MarkFilesToBeRemoved>(TYPES.MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
// middleware
+2
View File
@@ -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'),
@@ -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,
@@ -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<results.BadRequestErrorMessageResult | results.JsonResult> {
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<results.BadRequestErrorMessageResult | results.JsonResult> {
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<results.BadRequestErrorMessageResult | results.JsonResult> {
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<results.BadRequestErrorMessageResult | results.JsonResult> {
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<results.BadRequestErrorMessageResult | (() => 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,
@@ -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)
@@ -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,
}
}
}
@@ -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
}
@@ -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,
}),
@@ -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,
}),
@@ -0,0 +1,3 @@
export interface FileMoverInterface {
moveFile(sourcePath: string, destinationPath: string): Promise<void>
}
@@ -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)
@@ -1,4 +1,4 @@
export type CreateUploadSessionDTO = {
userUuid: string
ownerUuid: string
resourceRemoteIdentifier: string
}
@@ -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,
@@ -1,5 +1,6 @@
export type FinishUploadSessionDTO = {
userUuid: string
ownerUuid: string
ownerType: 'user' | 'vault'
resourceRemoteIdentifier: string
uploadBytesUsed: number
uploadBytesLimit: number
@@ -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<GetFileMetadataResponse> {
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.',
@@ -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
}
@@ -16,16 +16,16 @@ export class MarkFilesToBeRemoved implements UseCaseInterface {
async execute(dto: MarkFilesToBeRemovedDTO): Promise<MarkFilesToBeRemovedResponse> {
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,
@@ -1,3 +1,3 @@
export type MarkFilesToBeRemovedDTO = {
userUuid: string
ownerUuid: string
}
@@ -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<FileMoverInterface>
fileRemover.moveFile = jest.fn().mockReturnValue(413)
logger = {} as jest.Mocked<Logger>
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')
})
})
@@ -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<MoveFileResponse> {
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',
}
}
}
}
@@ -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
}
@@ -0,0 +1,8 @@
export type MoveFileResponse =
| {
success: true
}
| {
success: false
message: string
}
@@ -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,
@@ -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
}
@@ -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<StreamDownloadFileResponse> {
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,
@@ -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
}
@@ -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) {
@@ -3,7 +3,7 @@ import { ChunkId } from '../../Upload/ChunkId'
export type UploadFileChunkDTO = {
data: Uint8Array
chunkId: ChunkId
userUuid: string
ownerUuid: string
resourceRemoteIdentifier: string
resourceUnencryptedFileSize: number
}
@@ -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<void> {
const sourceFullPath = `${this.fileUploadPath}/${sourcePath}`
const destinationFullPath = `${this.fileUploadPath}/${destinationPath}`
await promises.rename(sourceFullPath, destinationFullPath)
}
}
@@ -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<void> {
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,
}),
)
}
}
@@ -2,4 +2,5 @@ export enum ValetTokenOperation {
Read = 'read',
Write = 'write',
Delete = 'delete',
Move = 'move',
}
@@ -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
}
}
@@ -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',
@@ -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<CreateVaultFileReadValetToken>(TYPES.CreateVaultFileReadValetToken)
.bind<CreateVaultFileValetToken>(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),
@@ -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'),
@@ -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<VaultFileUploadedEventHandler>(TYPES.VaultFileUploadedEventHandler).to(VaultFileUploadedEventHandler)
container.bind<VaultFileRemovedEventHandler>(TYPES.VaultFileRemovedEventHandler).to(VaultFileRemovedEventHandler)
// Services
container.bind<ContentDecoder>(TYPES.ContentDecoder).toDynamicValue(() => new ContentDecoder())
container.bind<AxiosInstance>(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 =
@@ -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<Vault, VaultProjection>,
@inject(TYPES.VaultUserProjector) private vaultUserProjector: ProjectorInterface<VaultUser, VaultUserProjection>,
@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) {
@@ -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<void> {
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)
}
}
@@ -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<void> {
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)
}
}
@@ -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<VaultValetTokenData>,
@inject(TYPES.VALET_TOKEN_TTL) private valetTokenTTL: number,
) {}
async execute(dto: CreateVaultFileReadValetTokenDTO): Promise<CreateValetTokenResponseData> {
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 }
}
}
@@ -1,6 +0,0 @@
export type CreateVaultFileReadValetTokenDTO = {
userUuid: string
vaultUuid: string
fileUuid: string
remoteIdentifier: string
}
@@ -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<VaultValetTokenData>,
@inject(TYPES.VALET_TOKEN_TTL) private valetTokenTTL: number,
) {}
async execute(dto: CreateVaultFileValetTokenDTO): Promise<CreateValetTokenResponseData> {
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 }
}
}
@@ -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'
}
@@ -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
}
@@ -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',
@@ -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,
},
@@ -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
}
@@ -22,6 +22,8 @@ export class VaultProjector implements ProjectorInterface<Vault, VaultProjection
user_uuid: vault.userUuid,
specified_items_key_uuid: vault.specifiedItemsKeyUuid,
vault_key_timestamp: vault.vaultKeyTimestamp,
file_upload_bytes_used: vault.fileUploadBytesUsed,
file_upload_bytes_limit: vault.fileUploadBytesLimit,
created_at_timestamp: vault.createdAtTimestamp,
updated_at_timestamp: vault.updatedAtTimestamp,
}