Compare commits

..

2 Commits

56 changed files with 701 additions and 124 deletions

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.25.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.15...@standardnotes/analytics@2.25.16) (2023-08-23)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.14...@standardnotes/analytics@2.25.15) (2023-08-22)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.25.15",
"version": "2.25.16",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.71.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.0...@standardnotes/api-gateway@1.71.1) (2023-08-23)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.71.0",
"version": "1.71.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.133.0...@standardnotes/auth-server@1.134.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.132.0...@standardnotes/auth-server@1.133.0) (2023-08-22)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.133.0",
"version": "1.134.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -256,6 +256,7 @@ import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAc
import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
export class ContainerConfigLoader {
async load(configuration?: {
@@ -982,6 +983,14 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Logger),
),
)
container
.bind<SharedVaultFileMovedEventHandler>(TYPES.Auth_SharedVaultFileMovedEventHandler)
.toConstantValue(
new SharedVaultFileMovedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
.toConstantValue(
@@ -1045,6 +1054,7 @@ export class ContainerConfigLoader {
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)],
['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],

View File

@@ -27,6 +27,7 @@ export class Service implements AuthServiceInterface {
async activatePremiumFeatures(dto: {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}): Promise<Result<string>> {
if (!this.container) {

View File

@@ -168,6 +168,7 @@ const TYPES = {
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),

View File

@@ -0,0 +1,28 @@
import { DomainEventHandlerInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
export class SharedVaultFileMovedEventHandler implements DomainEventHandlerInterface {
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: SharedVaultFileMovedEvent): Promise<void> {
const subtractResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.from.ownerUuid,
bytesUsed: -event.payload.fileByteSize,
})
if (subtractResult.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${subtractResult.getError()}`)
}
const addResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.to.ownerUuid,
bytesUsed: event.payload.fileByteSize,
})
if (addResult.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${addResult.getError()}`)
}
}
}

View File

@@ -57,7 +57,7 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
subscription,
new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]),
new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
)
return Result.ok('Premium features activated.')

View File

@@ -1,5 +1,6 @@
export interface ActivatePremiumFeaturesDTO {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.12.13](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.12...@standardnotes/domain-events-infra@1.12.13) (2023-08-23)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.11...@standardnotes/domain-events-infra@1.12.12) (2023-08-22)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.12.12",
"version": "1.12.13",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.116.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.1...@standardnotes/domain-events@2.116.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
## [2.115.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.0...@standardnotes/domain-events@2.115.1) (2023-08-22)
**Note:** Version bump only for package @standardnotes/domain-events

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.115.1",
"version": "2.116.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { SharedVaultFileMovedEventPayload } from './SharedVaultFileMovedEventPayload'
export interface SharedVaultFileMovedEvent extends DomainEventInterface {
type: 'SHARED_VAULT_FILE_MOVED'
payload: SharedVaultFileMovedEventPayload
}

View File

@@ -0,0 +1,14 @@
export interface SharedVaultFileMovedEventPayload {
fileByteSize: number
fileName: string
from: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
}

View File

@@ -64,6 +64,8 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
export * from './Event/SharedVaultFileMovedEvent'
export * from './Event/SharedVaultFileMovedEventPayload'
export * from './Event/SharedVaultFileRemovedEvent'
export * from './Event/SharedVaultFileRemovedEventPayload'
export * from './Event/SharedVaultFileUploadedEvent'

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.22](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.21...@standardnotes/event-store@1.11.22) (2023-08-23)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.21](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.20...@standardnotes/event-store@1.11.21) (2023-08-22)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.11.21",
"version": "1.11.22",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.22.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.21.0...@standardnotes/files-server@1.22.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/files/issues/705)) ([205a1ed](https://github.com/standardnotes/files/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.21.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.4...@standardnotes/files-server@1.21.0) (2023-08-22)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.21.0",
"version": "1.22.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -99,7 +99,9 @@ export class ContainerConfigLoader {
container
.bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
.toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
container.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory).to(DomainEventFactory)
container
.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory)
.toConstantValue(new DomainEventFactory(container.get<TimerInterface>(TYPES.Files_Timer)))
if (isConfiguredForInMemoryCache) {
container
@@ -214,9 +216,26 @@ export class ContainerConfigLoader {
container.get(TYPES.Files_DomainEventFactory),
),
)
container.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata).to(GetFileMetadata)
container
.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata)
.toConstantValue(
new GetFileMetadata(
container.get<FileDownloaderInterface>(TYPES.Files_FileDownloader),
container.get<winston.Logger>(TYPES.Files_Logger),
),
)
container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
container.bind<MoveFile>(TYPES.Files_MoveFile).to(MoveFile)
container
.bind<MoveFile>(TYPES.Files_MoveFile)
.toConstantValue(
new MoveFile(
container.get<GetFileMetadata>(TYPES.Files_GetFileMetadata),
container.get<FileMoverInterface>(TYPES.Files_FileMover),
container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
container.get<winston.Logger>(TYPES.Files_Logger),
),
)
container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
// middleware

View File

@@ -4,16 +4,14 @@ import {
DomainEventService,
SharedVaultFileUploadedEvent,
SharedVaultFileRemovedEvent,
SharedVaultFileMovedEvent,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
@injectable()
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Files_Timer) private timer: TimerInterface) {}
constructor(private timer: TimerInterface) {}
createFileRemovedEvent(payload: {
userUuid: string
@@ -56,6 +54,34 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createSharedVaultFileMovedEvent(payload: {
fileByteSize: number
fileName: string
from: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
}): SharedVaultFileMovedEvent {
return {
type: 'SHARED_VAULT_FILE_MOVED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: payload.from.sharedVaultUuid ?? payload.from.ownerUuid,
userIdentifierType: payload.from.sharedVaultUuid ? 'shared-vault-uuid' : 'uuid',
},
origin: DomainEventService.Files,
},
payload,
}
}
createSharedVaultFileUploadedEvent(payload: {
sharedVaultUuid: string
vaultOwnerUuid: string

View File

@@ -3,6 +3,7 @@ import {
FileRemovedEvent,
SharedVaultFileRemovedEvent,
SharedVaultFileUploadedEvent,
SharedVaultFileMovedEvent,
} from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface {
@@ -19,6 +20,20 @@ export interface DomainEventFactoryInterface {
fileByteSize: number
regularSubscriptionUuid: string
}): FileRemovedEvent
createSharedVaultFileMovedEvent(payload: {
fileByteSize: number
fileName: string
from: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
}): SharedVaultFileMovedEvent
createSharedVaultFileUploadedEvent(payload: {
sharedVaultUuid: string
vaultOwnerUuid: string

View File

@@ -1,4 +1,3 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
@@ -19,10 +18,8 @@ describe('GetFileMetadata', () => {
})
it('should return the file metadata', async () => {
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
success: true,
size: 123,
})
const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })
expect(result.getValue()).toEqual(123)
})
it('should not return the file metadata if it fails', async () => {
@@ -30,9 +27,8 @@ describe('GetFileMetadata', () => {
throw new Error('ooops')
})
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
success: false,
message: 'Could not get file metadata.',
})
const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })
expect(result.isFailed()).toBe(true)
})
})

View File

@@ -1,32 +1,20 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { GetFileMetadataDTO } from './GetFileMetadataDTO'
import { GetFileMetadataResponse } from './GetFileMetadataResponse'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
@injectable()
export class GetFileMetadata implements UseCaseInterface {
constructor(
@inject(TYPES.Files_FileDownloader) private fileDownloader: FileDownloaderInterface,
@inject(TYPES.Files_Logger) private logger: Logger,
) {}
export class GetFileMetadata implements UseCaseInterface<number> {
constructor(private fileDownloader: FileDownloaderInterface, private logger: Logger) {}
async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {
async execute(dto: GetFileMetadataDTO): Promise<Result<number>> {
try {
const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
return {
success: true,
size,
}
return Result.ok(size)
} catch (error) {
this.logger.error(`Could not get file metadata for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
return {
success: false,
message: 'Could not get file metadata.',
}
return Result.fail('Could not get file metadata')
}
}
}

View File

@@ -1,9 +0,0 @@
export type GetFileMetadataResponse =
| {
success: true
size: number
}
| {
success: false
message: string
}

View File

@@ -1,18 +1,27 @@
import 'reflect-metadata'
import { DomainEventPublisherInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
import { Result } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { MoveFile } from './MoveFile'
import { FileMoverInterface } from '../../Services/FileMoverInterface'
import { GetFileMetadata } from '../GetFileMetadata/GetFileMetadata'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
describe('MoveFile', () => {
let fileMover: FileMoverInterface
let getFileMetadataUseCase: GetFileMetadata
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let logger: Logger
const createUseCase = () => new MoveFile(fileMover, logger)
const createUseCase = () =>
new MoveFile(getFileMetadataUseCase, fileMover, domainEventPublisher, domainEventFactory, logger)
beforeEach(() => {
getFileMetadataUseCase = {} as jest.Mocked<GetFileMetadata>
getFileMetadataUseCase.execute = jest.fn().mockReturnValue(Result.ok(1234))
fileMover = {} as jest.Mocked<FileMoverInterface>
fileMover.moveFile = jest.fn().mockReturnValue(413)
@@ -20,17 +29,34 @@ describe('MoveFile', () => {
logger.debug = jest.fn()
logger.error = jest.fn()
logger.warn = jest.fn()
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createSharedVaultFileMovedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedVaultFileMovedEvent>)
})
it('should move a file', async () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
fromUuid: '1-2-3',
toUuid: '4-5-6',
moveType: 'shared-vault-to-user',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(fileMover.moveFile).toHaveBeenCalledWith('1-2-3/2-3-4', '4-5-6/2-3-4')
expect(fileMover.moveFile).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000/2-3-4',
'00000000-0000-0000-0000-000000000001/2-3-4',
)
})
it('should indicate an error if moving fails', async () => {
@@ -40,11 +66,174 @@ describe('MoveFile', () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
fromUuid: '1-2-3',
toUuid: '4-5-6',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the from shared vault uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: 'invalid',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the to shared vault uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: 'invalid',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the from owner uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: 'invalid',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the to owner uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: 'invalid',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the file metadata cannot be retrieved', async () => {
getFileMetadataUseCase.execute = jest.fn().mockReturnValue(Result.fail('oops'))
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should move file from user to shared vault', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000002',
},
moveType: 'user-to-shared-vault',
})
expect(fileMover.moveFile).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000/2-3-4',
'00000000-0000-0000-0000-000000000001/2-3-4',
)
expect(result.isFailed()).toEqual(false)
})
it('should move file from shared vault to user', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000002',
},
to: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
moveType: 'shared-vault-to-user',
})
expect(fileMover.moveFile).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000001/2-3-4',
'00000000-0000-0000-0000-000000000000/2-3-4',
)
expect(result.isFailed()).toEqual(false)
})
it('should fail if moving from shared vault to user without shared vault uuid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
moveType: 'shared-vault-to-user',
})
expect(result.isFailed()).toEqual(true)
})
it('should fail if moving from user to shared vault without shared vault uuid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
moveType: 'user-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
})

View File

@@ -1,27 +1,97 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import TYPES from '../../../Bootstrap/Types'
import { FileMoverInterface } from '../../Services/FileMoverInterface'
import { MoveFileDTO } from './MoveFileDTO'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { GetFileMetadata } from '../GetFileMetadata/GetFileMetadata'
@injectable()
export class MoveFile implements UseCaseInterface<boolean> {
constructor(
@inject(TYPES.Files_FileMover) private fileMover: FileMoverInterface,
@inject(TYPES.Files_Logger) private logger: Logger,
private getFileMetadataUseCase: GetFileMetadata,
private fileMover: FileMoverInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private logger: Logger,
) {}
async execute(dto: MoveFileDTO): Promise<Result<boolean>> {
try {
const srcPath = `${dto.fromUuid}/${dto.resourceRemoteIdentifier}`
const destPath = `${dto.toUuid}/${dto.resourceRemoteIdentifier}`
let fromSharedVaultUuid: Uuid | undefined = undefined
if (dto.from.sharedVaultUuid !== undefined) {
const fromSharedVaultUuidOrError = Uuid.create(dto.from.sharedVaultUuid)
if (fromSharedVaultUuidOrError.isFailed()) {
return Result.fail(fromSharedVaultUuidOrError.getError())
}
fromSharedVaultUuid = fromSharedVaultUuidOrError.getValue()
}
let toSharedVaultUuid: Uuid | undefined = undefined
if (dto.to.sharedVaultUuid !== undefined) {
const toSharedVaultUuidOrError = Uuid.create(dto.to.sharedVaultUuid)
if (toSharedVaultUuidOrError.isFailed()) {
return Result.fail(toSharedVaultUuidOrError.getError())
}
toSharedVaultUuid = toSharedVaultUuidOrError.getValue()
}
const fromOwnerUuidOrError = Uuid.create(dto.from.ownerUuid)
if (fromOwnerUuidOrError.isFailed()) {
return Result.fail(fromOwnerUuidOrError.getError())
}
const fromOwnerUuid = fromOwnerUuidOrError.getValue()
const toOwnerUuidOrError = Uuid.create(dto.to.ownerUuid)
if (toOwnerUuidOrError.isFailed()) {
return Result.fail(toOwnerUuidOrError.getError())
}
const toOwnerUuid = toOwnerUuidOrError.getValue()
if (['shared-vault-to-shared-vault', 'shared-vault-to-user'].includes(dto.moveType) && !fromSharedVaultUuid) {
return Result.fail('Source shared vault UUID is required')
}
if (['user-to-shared-vault', 'shared-vault-to-shared-vault'].includes(dto.moveType) && !toSharedVaultUuid) {
return Result.fail('Target shared vault UUID is required')
}
const fromUuid = dto.moveType === 'user-to-shared-vault' ? fromOwnerUuid.value : fromSharedVaultUuid?.value
const toUuid = dto.moveType === 'shared-vault-to-user' ? toOwnerUuid.value : toSharedVaultUuid?.value
const srcPath = `${fromUuid}/${dto.resourceRemoteIdentifier}`
const destPath = `${toUuid}/${dto.resourceRemoteIdentifier}`
this.logger.debug(`Moving file from ${srcPath} to ${destPath}`)
const metadataResultOrError = await this.getFileMetadataUseCase.execute({
resourceRemoteIdentifier: dto.resourceRemoteIdentifier,
ownerUuid: fromUuid as string,
})
if (metadataResultOrError.isFailed()) {
return Result.fail(metadataResultOrError.getError())
}
const fileSize = metadataResultOrError.getValue()
await this.fileMover.moveFile(srcPath, destPath)
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedVaultFileMovedEvent({
fileByteSize: fileSize,
fileName: dto.resourceRemoteIdentifier,
from: {
sharedVaultUuid: fromSharedVaultUuid?.value,
ownerUuid: fromOwnerUuid.value,
filePath: srcPath,
},
to: {
sharedVaultUuid: toSharedVaultUuid?.value,
ownerUuid: toOwnerUuid.value,
filePath: destPath,
},
}),
)
return Result.ok()
} catch (error) {
this.logger.error(`Could not move resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`)

View File

@@ -2,7 +2,13 @@ import { SharedVaultMoveType } from '@standardnotes/security'
export interface MoveFileDTO {
moveType: SharedVaultMoveType
fromUuid: string
toUuid: string
from: {
sharedVaultUuid?: string
ownerUuid: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
}
resourceRemoteIdentifier: string
}

View File

@@ -61,7 +61,7 @@ describe('AnnotatedFilesController', () => {
finishUploadSession.execute = jest.fn().mockReturnValue(Result.ok())
getFileMetadata = {} as jest.Mocked<GetFileMetadata>
getFileMetadata.execute = jest.fn().mockReturnValue({ success: true, size: 555_555 })
getFileMetadata.execute = jest.fn().mockReturnValue(Result.ok(555_555))
removeFile = {} as jest.Mocked<RemoveFile>
removeFile.execute = jest.fn().mockReturnValue(Result.ok())
@@ -183,7 +183,7 @@ describe('AnnotatedFilesController', () => {
request.headers['range'] = 'bytes=0-'
getFileMetadata.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
getFileMetadata.execute = jest.fn().mockReturnValue(Result.fail('error'))
const httpResponse = await createController().download(request, response)

View File

@@ -146,20 +146,21 @@ export class AnnotatedFilesController extends BaseHttpController {
chunkSize = this.maxChunkBytes
}
const fileMetadata = await this.getFileMetadata.execute({
const fileMetadataOrError = await this.getFileMetadata.execute({
ownerUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
})
if (!fileMetadata.success) {
return this.badRequest(fileMetadata.message)
if (fileMetadataOrError.isFailed()) {
return this.badRequest(fileMetadataOrError.getError())
}
const fileSize = fileMetadataOrError.getValue()
const startRange = Number(range.replace(/\D/g, ''))
const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1)
const headers = {
'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': endRange - startRange + 1,
'Content-Type': 'application/octet-stream',

View File

@@ -47,8 +47,8 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
const result = await this.moveFile.execute({
moveType: moveOperation.type,
fromUuid: moveOperation.fromUuid,
toUuid: moveOperation.toUuid,
from: moveOperation.from,
to: moveOperation.to,
resourceRemoteIdentifier: locals.remoteIdentifier,
})
@@ -187,20 +187,21 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
chunkSize = this.maxChunkBytes
}
const fileMetadata = await this.getFileMetadata.execute({
const fileMetadataOrError = await this.getFileMetadata.execute({
ownerUuid: locals.sharedVaultUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
})
if (!fileMetadata.success) {
return this.badRequest(fileMetadata.message)
if (fileMetadataOrError.isFailed()) {
return this.badRequest(fileMetadataOrError.getError())
}
const fileSize = fileMetadataOrError.getValue()
const startRange = Number(range.replace(/\D/g, ''))
const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1)
const headers = {
'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': endRange - startRange + 1,
'Content-Type': 'application/octet-stream',

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.15.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.14.2...@standardnotes/home-server@1.15.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
## [1.14.2](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.14.1...@standardnotes/home-server@1.14.2) (2023-08-22)
**Note:** Version bump only for package @standardnotes/home-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.14.2",
"version": "1.15.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -144,6 +144,7 @@ export class HomeServer implements HomeServerInterface {
void this.activatePremiumFeatures({
username: request.body.username,
subscriptionPlanName: request.body.subscriptionPlanName,
uploadBytesLimit: request.body.uploadBytesLimit,
endsAt: request.body.endsAt ? new Date(request.body.endsAt) : undefined,
}).then((result) => {
if (result.isFailed()) {
@@ -221,6 +222,7 @@ export class HomeServer implements HomeServerInterface {
async activatePremiumFeatures(dto: {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}): Promise<Result<string>> {
if (!this.isRunning() || !this.authService) {

View File

@@ -6,6 +6,7 @@ export interface HomeServerInterface {
activatePremiumFeatures(dto: {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}): Promise<Result<string>>
stop(): Promise<Result<string>>

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.26.10](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.9...@standardnotes/revisions-server@1.26.10) (2023-08-23)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.8...@standardnotes/revisions-server@1.26.9) (2023-08-22)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.26.9",
"version": "1.26.10",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.26](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.25...@standardnotes/scheduler-server@1.20.26) (2023-08-23)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.25](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.24...@standardnotes/scheduler-server@1.20.25) (2023-08-22)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.20.25",
"version": "1.20.26",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.10.0...@standardnotes/security@1.11.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.9.0...@standardnotes/security@1.10.0) (2023-08-22)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/security",
"version": "1.10.0",
"version": "1.11.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -11,7 +11,13 @@ export interface SharedVaultValetTokenData {
uploadBytesLimit?: number
moveOperation?: {
type: SharedVaultMoveType
fromUuid: string
toUuid: string
from: {
sharedVaultUuid?: string
ownerUuid: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
}
}
}

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.83.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.82.0...@standardnotes/syncing-server@1.83.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/syncing-server-js/issues/705)) ([205a1ed](https://github.com/standardnotes/syncing-server-js/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.82.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.81.0...@standardnotes/syncing-server@1.82.0) (2023-08-22)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.82.0",
"version": "1.83.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -155,6 +155,7 @@ import { Logger } from 'winston'
import { ItemRepositoryResolverInterface } from '../Domain/Item/ItemRepositoryResolverInterface'
import { TypeORMItemRepositoryResolver } from '../Infra/TypeORM/TypeORMItemRepositoryResolver'
import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser'
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -872,6 +873,15 @@ export class ContainerConfigLoader {
container.get<winston.Logger>(TYPES.Sync_Logger),
),
)
container
.bind<SharedVaultFileMovedEventHandler>(TYPES.Sync_SharedVaultFileMovedEventHandler)
.toConstantValue(
new SharedVaultFileMovedEventHandler(
container.get<UpdateStorageQuotaUsedInSharedVault>(TYPES.Sync_UpdateStorageQuotaUsedInSharedVault),
container.get<AddNotificationsForUsers>(TYPES.Sync_AddNotificationsForUsers),
container.get<winston.Logger>(TYPES.Sync_Logger),
),
)
// Services
container.bind<ContentDecoder>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
@@ -902,6 +912,10 @@ export class ContainerConfigLoader {
'SHARED_VAULT_FILE_REMOVED',
container.get<SharedVaultFileRemovedEventHandler>(TYPES.Sync_SharedVaultFileRemovedEventHandler),
],
[
'SHARED_VAULT_FILE_MOVED',
container.get<SharedVaultFileMovedEventHandler>(TYPES.Sync_SharedVaultFileMovedEventHandler),
],
])
if (!isConfiguredForHomeServer) {
container.bind(TYPES.Sync_AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))

View File

@@ -90,6 +90,7 @@ const TYPES = {
Sync_ItemRevisionCreationRequestedEventHandler: Symbol.for('Sync_ItemRevisionCreationRequestedEventHandler'),
Sync_SharedVaultFileRemovedEventHandler: Symbol.for('Sync_SharedVaultFileRemovedEventHandler'),
Sync_SharedVaultFileUploadedEventHandler: Symbol.for('Sync_SharedVaultFileUploadedEventHandler'),
Sync_SharedVaultFileMovedEventHandler: Symbol.for('Sync_SharedVaultFileMovedEventHandler'),
// Services
Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),

View File

@@ -0,0 +1,90 @@
import { DomainEventHandlerInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
import { NotificationPayload, NotificationType, Uuid } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { UpdateStorageQuotaUsedInSharedVault } from '../UseCase/SharedVaults/UpdateStorageQuotaUsedInSharedVault/UpdateStorageQuotaUsedInSharedVault'
import { AddNotificationsForUsers } from '../UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers'
export class SharedVaultFileMovedEventHandler implements DomainEventHandlerInterface {
constructor(
private updateStorageQuotaUsedInSharedVaultUseCase: UpdateStorageQuotaUsedInSharedVault,
private addNotificationsForUsers: AddNotificationsForUsers,
private logger: Logger,
) {}
async handle(event: SharedVaultFileMovedEvent): Promise<void> {
if (event.payload.from.sharedVaultUuid !== undefined) {
const sharedVaultUuidOrError = Uuid.create(event.payload.from.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
this.logger.error(sharedVaultUuidOrError.getError())
return
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const subtractResult = await this.updateStorageQuotaUsedInSharedVaultUseCase.execute({
sharedVaultUuid: sharedVaultUuid.value,
bytesUsed: -event.payload.fileByteSize,
})
if (subtractResult.isFailed()) {
this.logger.error(`Failed to update storage quota used in shared vault: ${subtractResult.getError()}`)
return
}
const notificationPayload = NotificationPayload.create({
sharedVaultUuid: sharedVaultUuid,
type: NotificationType.create(NotificationType.TYPES.SharedVaultFileRemoved).getValue(),
version: '1.0',
}).getValue()
const notificationResult = await this.addNotificationsForUsers.execute({
sharedVaultUuid: sharedVaultUuid.value,
type: NotificationType.TYPES.SharedVaultFileRemoved,
payload: notificationPayload,
version: '1.0',
})
if (notificationResult.isFailed()) {
this.logger.error(`Failed to add notification for users: ${notificationResult.getError()}`)
}
}
if (event.payload.to.sharedVaultUuid !== undefined) {
const sharedVaultUuidOrError = Uuid.create(event.payload.to.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
this.logger.error(sharedVaultUuidOrError.getError())
return
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const addResult = await this.updateStorageQuotaUsedInSharedVaultUseCase.execute({
sharedVaultUuid: sharedVaultUuid.value,
bytesUsed: event.payload.fileByteSize,
})
if (addResult.isFailed()) {
this.logger.error(`Failed to update storage quota used in shared vault: ${addResult.getError()}`)
return
}
const notificationPayload = NotificationPayload.create({
sharedVaultUuid: sharedVaultUuid,
type: NotificationType.create(NotificationType.TYPES.SharedVaultFileUploaded).getValue(),
version: '1.0',
}).getValue()
const notificationResult = await this.addNotificationsForUsers.execute({
sharedVaultUuid: sharedVaultUuid.value,
type: NotificationType.TYPES.SharedVaultFileUploaded,
payload: notificationPayload,
version: '1.0',
})
if (notificationResult.isFailed()) {
this.logger.error(`Failed to add notification for users: ${notificationResult.getError()}`)
}
}
}
}

View File

@@ -256,6 +256,23 @@ describe('CreateSharedVaultFileValetToken', () => {
expect(result.getError()).toBe('User does not have permission to perform this operation')
})
it('should return error when target shared vault does not exist for shared-vault-to-shared-vault move operation', async () => {
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValueOnce(sharedVault).mockResolvedValueOnce(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Target shared vault not found')
})
it('should create move valet token for shared-vault-to-shared-vault operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()

View File

@@ -1,9 +1,15 @@
import { SharedVaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security'
import {
SharedVaultMoveType,
SharedVaultValetTokenData,
TokenEncoderInterface,
ValetTokenOperation,
} from '@standardnotes/security'
import { Result, SharedVaultUserPermission, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { CreateSharedVaultFileValetTokenDTO } from './CreateSharedVaultFileValetTokenDTO'
import { SharedVault } from '../../../SharedVault/SharedVault'
export class CreateSharedVaultFileValetToken implements UseCaseInterface<string> {
constructor(
@@ -48,6 +54,7 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
return Result.fail('User does not have permission to perform this operation')
}
let targetSharedVault: SharedVault | null = null
if (dto.operation === ValetTokenOperation.Move) {
if (!dto.moveOperationType) {
return Result.fail('Move operation type is required')
@@ -64,6 +71,11 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
}
const sharedVaultTargetUuid = sharedVaultTargetUuidOrError.getValue()
targetSharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultTargetUuid)
if (!targetSharedVault) {
return Result.fail('Target shared vault not found')
}
const toSharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
userUuid: userUuid,
sharedVaultUuid: sharedVaultTargetUuid,
@@ -83,6 +95,28 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
}
}
const fromSharedVaultUuid = ['shared-vault-to-user', 'shared-vault-to-shared-vault'].includes(
dto.moveOperationType as string,
)
? sharedVaultUuid.value
: undefined
const fromOwnerUuid =
dto.moveOperationType === 'user-to-shared-vault' ? userUuid.value : sharedVault.props.userUuid.value
const toSharedVaultUuid = targetSharedVault
? targetSharedVault.id.toString()
: dto.moveOperationType === 'shared-vault-to-user'
? undefined
: sharedVaultUuid.value
const toOwnerUuid =
dto.moveOperationType === 'user-to-shared-vault'
? sharedVault.props.userUuid.value
: targetSharedVault
? targetSharedVault.props.userUuid.value
: userUuid.value
const tokenData: SharedVaultValetTokenData = {
sharedVaultUuid: dto.sharedVaultUuid,
vaultOwnerUuid: sharedVault.props.userUuid.value,
@@ -91,40 +125,21 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
uploadBytesUsed: sharedVault.props.fileUploadBytesUsed,
uploadBytesLimit: dto.sharedVaultOwnerUploadBytesLimit,
unencryptedFileSize: dto.unencryptedFileSize,
moveOperation: this.createMoveOperationData(dto),
moveOperation: {
type: dto.moveOperationType as SharedVaultMoveType,
from: {
sharedVaultUuid: fromSharedVaultUuid,
ownerUuid: fromOwnerUuid,
},
to: {
sharedVaultUuid: toSharedVaultUuid,
ownerUuid: toOwnerUuid,
},
},
}
const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL)
return Result.ok(valetToken)
}
private createMoveOperationData(dto: CreateSharedVaultFileValetTokenDTO): SharedVaultValetTokenData['moveOperation'] {
if (!dto.moveOperationType) {
return undefined
}
let fromUuid: string
let toUuid: string
switch (dto.moveOperationType) {
case 'shared-vault-to-user':
fromUuid = dto.sharedVaultUuid
toUuid = dto.userUuid
break
case 'user-to-shared-vault':
fromUuid = dto.userUuid
toUuid = dto.sharedVaultUuid
break
case 'shared-vault-to-shared-vault':
fromUuid = dto.sharedVaultUuid
toUuid = dto.sharedVaultToSharedVaultMoveTargetUuid as string
break
}
return {
type: dto.moveOperationType,
fromUuid,
toUuid,
}
}
}

View File

@@ -10,4 +10,5 @@ export interface CreateSharedVaultFileValetTokenDTO {
unencryptedFileSize?: number
moveOperationType?: SharedVaultMoveType
sharedVaultToSharedVaultMoveTargetUuid?: string
sharedVaultToSharedVaultMoveTargetOwnerUuid?: string
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.20](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.19...@standardnotes/websockets-server@1.10.20) (2023-08-23)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.10.19](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.18...@standardnotes/websockets-server@1.10.19) (2023-08-22)
**Note:** Version bump only for package @standardnotes/websockets-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.10.19",
"version": "1.10.20",
"engines": {
"node": ">=18.0.0 <21.0.0"
},