mirror of
https://github.com/standardnotes/server
synced 2026-07-14 00:01:54 -04:00
refactor: vault files
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
-57
@@ -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 }
|
||||
}
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
export type CreateVaultFileReadValetTokenDTO = {
|
||||
userUuid: string
|
||||
vaultUuid: string
|
||||
fileUuid: string
|
||||
remoteIdentifier: string
|
||||
}
|
||||
+96
@@ -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 }
|
||||
}
|
||||
}
|
||||
+10
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user