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