mirror of
https://github.com/standardnotes/server
synced 2026-08-04 09:18:51 -04:00
refactor: vault files
This commit is contained in:
@@ -139,12 +139,12 @@ import {
|
|||||||
ValetTokenData,
|
ValetTokenData,
|
||||||
WebSocketConnectionTokenData,
|
WebSocketConnectionTokenData,
|
||||||
} from '@standardnotes/security'
|
} from '@standardnotes/security'
|
||||||
import { FileUploadedEventHandler } from '../Domain/Handler/FileUploadedEventHandler'
|
import { UserFileUploadedEventHandler } from '../Domain/Handler/UserFileUploadedEventHandler'
|
||||||
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||||
import { CreateListedAccount } from '../Domain/UseCase/CreateListedAccount/CreateListedAccount'
|
import { CreateListedAccount } from '../Domain/UseCase/CreateListedAccount/CreateListedAccount'
|
||||||
import { ListedAccountCreatedEventHandler } from '../Domain/Handler/ListedAccountCreatedEventHandler'
|
import { ListedAccountCreatedEventHandler } from '../Domain/Handler/ListedAccountCreatedEventHandler'
|
||||||
import { ListedAccountDeletedEventHandler } from '../Domain/Handler/ListedAccountDeletedEventHandler'
|
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 { UserDisabledSessionUserAgentLoggingEventHandler } from '../Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler'
|
||||||
import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface'
|
import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface'
|
||||||
import { SettingInterpreter } from '../Domain/Setting/SettingInterpreter'
|
import { SettingInterpreter } from '../Domain/Setting/SettingInterpreter'
|
||||||
@@ -834,8 +834,8 @@ export class ContainerConfigLoader {
|
|||||||
.bind<SubscriptionReassignedEventHandler>(TYPES.SubscriptionReassignedEventHandler)
|
.bind<SubscriptionReassignedEventHandler>(TYPES.SubscriptionReassignedEventHandler)
|
||||||
.to(SubscriptionReassignedEventHandler)
|
.to(SubscriptionReassignedEventHandler)
|
||||||
container.bind<UserEmailChangedEventHandler>(TYPES.UserEmailChangedEventHandler).to(UserEmailChangedEventHandler)
|
container.bind<UserEmailChangedEventHandler>(TYPES.UserEmailChangedEventHandler).to(UserEmailChangedEventHandler)
|
||||||
container.bind<FileUploadedEventHandler>(TYPES.FileUploadedEventHandler).to(FileUploadedEventHandler)
|
container.bind<UserFileUploadedEventHandler>(TYPES.UserFileUploadedEventHandler).to(UserFileUploadedEventHandler)
|
||||||
container.bind<FileRemovedEventHandler>(TYPES.FileRemovedEventHandler).to(FileRemovedEventHandler)
|
container.bind<UserFileRemovedEventHandler>(TYPES.UserFileRemovedEventHandler).to(UserFileRemovedEventHandler)
|
||||||
container
|
container
|
||||||
.bind<ListedAccountCreatedEventHandler>(TYPES.ListedAccountCreatedEventHandler)
|
.bind<ListedAccountCreatedEventHandler>(TYPES.ListedAccountCreatedEventHandler)
|
||||||
.to(ListedAccountCreatedEventHandler)
|
.to(ListedAccountCreatedEventHandler)
|
||||||
@@ -873,8 +873,8 @@ export class ContainerConfigLoader {
|
|||||||
['EXTENSION_KEY_GRANTED', container.get(TYPES.ExtensionKeyGrantedEventHandler)],
|
['EXTENSION_KEY_GRANTED', container.get(TYPES.ExtensionKeyGrantedEventHandler)],
|
||||||
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.SubscriptionReassignedEventHandler)],
|
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.SubscriptionReassignedEventHandler)],
|
||||||
['USER_EMAIL_CHANGED', container.get(TYPES.UserEmailChangedEventHandler)],
|
['USER_EMAIL_CHANGED', container.get(TYPES.UserEmailChangedEventHandler)],
|
||||||
['FILE_UPLOADED', container.get(TYPES.FileUploadedEventHandler)],
|
['USER_FILE_UPLOADED', container.get(TYPES.UserFileUploadedEventHandler)],
|
||||||
['FILE_REMOVED', container.get(TYPES.FileRemovedEventHandler)],
|
['USER_FILE_REMOVED', container.get(TYPES.UserFileRemovedEventHandler)],
|
||||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.ListedAccountCreatedEventHandler)],
|
['LISTED_ACCOUNT_CREATED', container.get(TYPES.ListedAccountCreatedEventHandler)],
|
||||||
['LISTED_ACCOUNT_DELETED', container.get(TYPES.ListedAccountDeletedEventHandler)],
|
['LISTED_ACCOUNT_DELETED', container.get(TYPES.ListedAccountDeletedEventHandler)],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ const TYPES = {
|
|||||||
SubscriptionSyncRequestedEventHandler: Symbol.for('SubscriptionSyncRequestedEventHandler'),
|
SubscriptionSyncRequestedEventHandler: Symbol.for('SubscriptionSyncRequestedEventHandler'),
|
||||||
ExtensionKeyGrantedEventHandler: Symbol.for('ExtensionKeyGrantedEventHandler'),
|
ExtensionKeyGrantedEventHandler: Symbol.for('ExtensionKeyGrantedEventHandler'),
|
||||||
UserEmailChangedEventHandler: Symbol.for('UserEmailChangedEventHandler'),
|
UserEmailChangedEventHandler: Symbol.for('UserEmailChangedEventHandler'),
|
||||||
FileUploadedEventHandler: Symbol.for('FileUploadedEventHandler'),
|
UserFileUploadedEventHandler: Symbol.for('UserFileUploadedEventHandler'),
|
||||||
FileRemovedEventHandler: Symbol.for('FileRemovedEventHandler'),
|
UserFileRemovedEventHandler: Symbol.for('UserFileRemovedEventHandler'),
|
||||||
ListedAccountCreatedEventHandler: Symbol.for('ListedAccountCreatedEventHandler'),
|
ListedAccountCreatedEventHandler: Symbol.for('ListedAccountCreatedEventHandler'),
|
||||||
ListedAccountDeletedEventHandler: Symbol.for('ListedAccountDeletedEventHandler'),
|
ListedAccountDeletedEventHandler: Symbol.for('ListedAccountDeletedEventHandler'),
|
||||||
UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'),
|
UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'),
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
results,
|
results,
|
||||||
} from 'inversify-express-utils'
|
} from 'inversify-express-utils'
|
||||||
import { CreateValetTokenPayload, ErrorTag } from '@standardnotes/responses'
|
import { ErrorTag } from '@standardnotes/responses'
|
||||||
import { ValetTokenOperation } from '@standardnotes/security'
|
import { ValetTokenOperation } from '@standardnotes/security'
|
||||||
import { Uuid } from '@standardnotes/domain-core'
|
import { Uuid } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import TYPES from '../Bootstrap/Types'
|
import TYPES from '../Bootstrap/Types'
|
||||||
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||||
|
import { CreateValetTokenPayload } from '../Domain/ValetToken/CreateValetTokenPayload'
|
||||||
|
|
||||||
@controller('/valet-tokens', TYPES.ApiGatewayAuthMiddleware)
|
@controller('/valet-tokens', TYPES.ApiGatewayAuthMiddleware)
|
||||||
export class ValetTokenController extends BaseHttpController {
|
export class ValetTokenController extends BaseHttpController {
|
||||||
|
|||||||
+7
-6
@@ -1,25 +1,26 @@
|
|||||||
import 'reflect-metadata'
|
import 'reflect-metadata'
|
||||||
|
|
||||||
import { FileRemovedEvent } from '@standardnotes/domain-events'
|
import { UserFileRemovedEvent } from '@standardnotes/domain-events'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
import { User } from '../User/User'
|
import { User } from '../User/User'
|
||||||
import { FileRemovedEventHandler } from './FileRemovedEventHandler'
|
import { UserFileRemovedEventHandler } from './UserFileRemovedEventHandler'
|
||||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||||
|
|
||||||
describe('FileRemovedEventHandler', () => {
|
describe('UserFileRemovedEventHandler', () => {
|
||||||
let userSubscriptionService: UserSubscriptionServiceInterface
|
let userSubscriptionService: UserSubscriptionServiceInterface
|
||||||
let logger: Logger
|
let logger: Logger
|
||||||
let user: User
|
let user: User
|
||||||
let event: FileRemovedEvent
|
let event: UserFileRemovedEvent
|
||||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||||
let regularSubscription: UserSubscription
|
let regularSubscription: UserSubscription
|
||||||
let sharedSubscription: UserSubscription
|
let sharedSubscription: UserSubscription
|
||||||
|
|
||||||
const createHandler = () => new FileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger)
|
const createHandler = () =>
|
||||||
|
new UserFileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger)
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = {
|
user = {
|
||||||
@@ -47,7 +48,7 @@ describe('FileRemovedEventHandler', () => {
|
|||||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||||
subscriptionSettingService.createOrReplace = jest.fn()
|
subscriptionSettingService.createOrReplace = jest.fn()
|
||||||
|
|
||||||
event = {} as jest.Mocked<FileRemovedEvent>
|
event = {} as jest.Mocked<UserFileRemovedEvent>
|
||||||
event.createdAt = new Date(1)
|
event.createdAt = new Date(1)
|
||||||
event.payload = {
|
event.payload = {
|
||||||
userUuid: '1-2-3',
|
userUuid: '1-2-3',
|
||||||
+3
-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 { SettingName } from '@standardnotes/settings'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
@@ -10,14 +10,14 @@ import { UserSubscription } from '../Subscription/UserSubscription'
|
|||||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class FileRemovedEventHandler implements DomainEventHandlerInterface {
|
export class UserFileRemovedEventHandler implements DomainEventHandlerInterface {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||||
@inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
@inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: FileRemovedEvent): Promise<void> {
|
async handle(event: UserFileRemovedEvent): Promise<void> {
|
||||||
const { regularSubscription, sharedSubscription } =
|
const { regularSubscription, sharedSubscription } =
|
||||||
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(event.payload.userUuid)
|
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(event.payload.userUuid)
|
||||||
if (regularSubscription === null) {
|
if (regularSubscription === null) {
|
||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
import 'reflect-metadata'
|
import 'reflect-metadata'
|
||||||
|
|
||||||
import { FileUploadedEvent } from '@standardnotes/domain-events'
|
import { UserFileUploadedEvent } from '@standardnotes/domain-events'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
import { User } from '../User/User'
|
import { User } from '../User/User'
|
||||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||||
import { FileUploadedEventHandler } from './FileUploadedEventHandler'
|
import { UserFileUploadedEventHandler } from './UserFileUploadedEventHandler'
|
||||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||||
@@ -16,13 +16,13 @@ describe('FileUploadedEventHandler', () => {
|
|||||||
let userSubscriptionService: UserSubscriptionServiceInterface
|
let userSubscriptionService: UserSubscriptionServiceInterface
|
||||||
let logger: Logger
|
let logger: Logger
|
||||||
let user: User
|
let user: User
|
||||||
let event: FileUploadedEvent
|
let event: UserFileUploadedEvent
|
||||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||||
let regularSubscription: UserSubscription
|
let regularSubscription: UserSubscription
|
||||||
let sharedSubscription: UserSubscription
|
let sharedSubscription: UserSubscription
|
||||||
|
|
||||||
const createHandler = () =>
|
const createHandler = () =>
|
||||||
new FileUploadedEventHandler(userRepository, userSubscriptionService, subscriptionSettingService, logger)
|
new UserFileUploadedEventHandler(userRepository, userSubscriptionService, subscriptionSettingService, logger)
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = {
|
user = {
|
||||||
@@ -53,7 +53,7 @@ describe('FileUploadedEventHandler', () => {
|
|||||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||||
subscriptionSettingService.createOrReplace = jest.fn()
|
subscriptionSettingService.createOrReplace = jest.fn()
|
||||||
|
|
||||||
event = {} as jest.Mocked<FileUploadedEvent>
|
event = {} as jest.Mocked<UserFileUploadedEvent>
|
||||||
event.createdAt = new Date(1)
|
event.createdAt = new Date(1)
|
||||||
event.payload = {
|
event.payload = {
|
||||||
userUuid: '1-2-3',
|
userUuid: '1-2-3',
|
||||||
+3
-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 { SettingName } from '@standardnotes/settings'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
@@ -11,7 +11,7 @@ import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscripti
|
|||||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class FileUploadedEventHandler implements DomainEventHandlerInterface {
|
export class UserFileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||||
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||||
@@ -19,7 +19,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface {
|
|||||||
@inject(TYPES.Logger) private logger: Logger,
|
@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)
|
const user = await this.userRepository.findOneByUuid(event.payload.userUuid)
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
this.logger.warn(`Could not find user with uuid: ${event.payload.userUuid}`)
|
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 { SubscriptionName } from '@standardnotes/common'
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
|
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
|
||||||
import { CreateValetTokenPayload, CreateValetTokenResponseData } from '@standardnotes/responses'
|
import { CreateValetTokenResponseData } from '@standardnotes/responses'
|
||||||
import { SettingName } from '@standardnotes/settings'
|
import { SettingName } from '@standardnotes/settings'
|
||||||
|
|
||||||
import TYPES from '../../../Bootstrap/Types'
|
import TYPES from '../../../Bootstrap/Types'
|
||||||
@@ -12,6 +12,7 @@ import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionS
|
|||||||
import { CreateValetTokenDTO } from './CreateValetTokenDTO'
|
import { CreateValetTokenDTO } from './CreateValetTokenDTO'
|
||||||
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
|
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||||
|
import { CreateValetTokenPayload } from '../../ValetToken/CreateValetTokenPayload'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CreateValetToken implements UseCaseInterface {
|
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
-1
@@ -1,4 +1,4 @@
|
|||||||
export interface FileRemovedEventPayload {
|
export interface UserFileRemovedEventPayload {
|
||||||
userUuid: string
|
userUuid: string
|
||||||
regularSubscriptionUuid: string
|
regularSubscriptionUuid: string
|
||||||
fileByteSize: number
|
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
-1
@@ -1,4 +1,4 @@
|
|||||||
export interface FileUploadedEventPayload {
|
export interface UserFileUploadedEventPayload {
|
||||||
userUuid: string
|
userUuid: string
|
||||||
fileByteSize: number
|
fileByteSize: number
|
||||||
filePath: string
|
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
|
||||||
|
}
|
||||||
@@ -26,10 +26,17 @@ export * from './Event/ExitDiscountWithdrawRequestedEvent'
|
|||||||
export * from './Event/ExitDiscountWithdrawRequestedEventPayload'
|
export * from './Event/ExitDiscountWithdrawRequestedEventPayload'
|
||||||
export * from './Event/ExtensionKeyGrantedEvent'
|
export * from './Event/ExtensionKeyGrantedEvent'
|
||||||
export * from './Event/ExtensionKeyGrantedEventPayload'
|
export * from './Event/ExtensionKeyGrantedEventPayload'
|
||||||
export * from './Event/FileRemovedEvent'
|
|
||||||
export * from './Event/FileRemovedEventPayload'
|
export * from './Event/UserFileRemovedEvent'
|
||||||
export * from './Event/FileUploadedEvent'
|
export * from './Event/UserFileRemovedEventPayload'
|
||||||
export * from './Event/FileUploadedEventPayload'
|
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/ItemDumpedEvent'
|
||||||
export * from './Event/ItemDumpedEventPayload'
|
export * from './Event/ItemDumpedEventPayload'
|
||||||
export * from './Event/ItemRevisionCreationRequestedEvent'
|
export * from './Event/ItemRevisionCreationRequestedEvent'
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ export class ContainerConfigLoader {
|
|||||||
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.EventHandler)],
|
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.EventHandler)],
|
||||||
['USER_CREDENTIALS_CHANGED', container.get(TYPES.EventHandler)],
|
['USER_CREDENTIALS_CHANGED', container.get(TYPES.EventHandler)],
|
||||||
['USER_EMAIL_CHANGED', container.get(TYPES.EventHandler)],
|
['USER_EMAIL_CHANGED', container.get(TYPES.EventHandler)],
|
||||||
['FILE_UPLOADED', container.get(TYPES.EventHandler)],
|
['USER_FILE_UPLOADED', container.get(TYPES.EventHandler)],
|
||||||
['FILE_REMOVED', container.get(TYPES.EventHandler)],
|
['USER_FILE_REMOVED', container.get(TYPES.EventHandler)],
|
||||||
['LISTED_ACCOUNT_REQUESTED', container.get(TYPES.EventHandler)],
|
['LISTED_ACCOUNT_REQUESTED', container.get(TYPES.EventHandler)],
|
||||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.EventHandler)],
|
['LISTED_ACCOUNT_CREATED', container.get(TYPES.EventHandler)],
|
||||||
['LISTED_ACCOUNT_DELETED', 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 { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
|
||||||
import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
|
import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
|
||||||
import { VaultValetTokenAuthMiddleware } from '../Controller/VaultValetTokenAuthMiddleware'
|
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 {
|
export class ContainerConfigLoader {
|
||||||
async load(): Promise<Container> {
|
async load(): Promise<Container> {
|
||||||
@@ -98,12 +102,14 @@ export class ContainerConfigLoader {
|
|||||||
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(S3FileDownloader)
|
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(S3FileDownloader)
|
||||||
container.bind<FileUploaderInterface>(TYPES.FileUploader).to(S3FileUploader)
|
container.bind<FileUploaderInterface>(TYPES.FileUploader).to(S3FileUploader)
|
||||||
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(S3FileRemover)
|
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(S3FileRemover)
|
||||||
|
container.bind<FileMoverInterface>(TYPES.FileMover).to(S3FileMover)
|
||||||
} else {
|
} else {
|
||||||
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(FSFileDownloader)
|
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(FSFileDownloader)
|
||||||
container
|
container
|
||||||
.bind<FileUploaderInterface>(TYPES.FileUploader)
|
.bind<FileUploaderInterface>(TYPES.FileUploader)
|
||||||
.toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger)))
|
.toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger)))
|
||||||
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(FSFileRemover)
|
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(FSFileRemover)
|
||||||
|
container.bind<FileMoverInterface>(TYPES.FileMover).to(FSFileMover)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||||
@@ -146,6 +152,7 @@ export class ContainerConfigLoader {
|
|||||||
container.bind<FinishUploadSession>(TYPES.FinishUploadSession).to(FinishUploadSession)
|
container.bind<FinishUploadSession>(TYPES.FinishUploadSession).to(FinishUploadSession)
|
||||||
container.bind<GetFileMetadata>(TYPES.GetFileMetadata).to(GetFileMetadata)
|
container.bind<GetFileMetadata>(TYPES.GetFileMetadata).to(GetFileMetadata)
|
||||||
container.bind<RemoveFile>(TYPES.RemoveFile).to(RemoveFile)
|
container.bind<RemoveFile>(TYPES.RemoveFile).to(RemoveFile)
|
||||||
|
container.bind<MoveFile>(TYPES.MoveFile).to(MoveFile)
|
||||||
container.bind<MarkFilesToBeRemoved>(TYPES.MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
|
container.bind<MarkFilesToBeRemoved>(TYPES.MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
|
||||||
|
|
||||||
// middleware
|
// middleware
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const TYPES = {
|
|||||||
FinishUploadSession: Symbol.for('FinishUploadSession'),
|
FinishUploadSession: Symbol.for('FinishUploadSession'),
|
||||||
GetFileMetadata: Symbol.for('GetFileMetadata'),
|
GetFileMetadata: Symbol.for('GetFileMetadata'),
|
||||||
RemoveFile: Symbol.for('RemoveFile'),
|
RemoveFile: Symbol.for('RemoveFile'),
|
||||||
|
MoveFile: Symbol.for('MoveFile'),
|
||||||
MarkFilesToBeRemoved: Symbol.for('MarkFilesToBeRemoved'),
|
MarkFilesToBeRemoved: Symbol.for('MarkFilesToBeRemoved'),
|
||||||
|
|
||||||
// services
|
// services
|
||||||
@@ -23,6 +24,7 @@ const TYPES = {
|
|||||||
FileUploader: Symbol.for('FileUploader'),
|
FileUploader: Symbol.for('FileUploader'),
|
||||||
FileDownloader: Symbol.for('FileDownloader'),
|
FileDownloader: Symbol.for('FileDownloader'),
|
||||||
FileRemover: Symbol.for('FileRemover'),
|
FileRemover: Symbol.for('FileRemover'),
|
||||||
|
FileMover: Symbol.for('FileMover'),
|
||||||
|
|
||||||
// repositories
|
// repositories
|
||||||
UploadRepository: Symbol.for('UploadRepository'),
|
UploadRepository: Symbol.for('UploadRepository'),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class FilesController extends BaseHttpController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.createUploadSession.execute({
|
const result = await this.createUploadSession.execute({
|
||||||
userUuid: response.locals.userUuid,
|
ownerUuid: response.locals.userUuid,
|
||||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export class FilesController extends BaseHttpController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.uploadFileChunk.execute({
|
const result = await this.uploadFileChunk.execute({
|
||||||
userUuid: response.locals.userUuid,
|
ownerUuid: response.locals.userUuid,
|
||||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||||
resourceUnencryptedFileSize: response.locals.permittedResources[0].unencryptedFileSize,
|
resourceUnencryptedFileSize: response.locals.permittedResources[0].unencryptedFileSize,
|
||||||
chunkId,
|
chunkId,
|
||||||
@@ -85,7 +85,8 @@ export class FilesController extends BaseHttpController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.finishUploadSession.execute({
|
const result = await this.finishUploadSession.execute({
|
||||||
userUuid: response.locals.userUuid,
|
ownerUuid: response.locals.userUuid,
|
||||||
|
ownerType: 'user',
|
||||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||||
uploadBytesLimit: response.locals.uploadBytesLimit,
|
uploadBytesLimit: response.locals.uploadBytesLimit,
|
||||||
uploadBytesUsed: response.locals.uploadBytesUsed,
|
uploadBytesUsed: response.locals.uploadBytesUsed,
|
||||||
@@ -140,7 +141,7 @@ export class FilesController extends BaseHttpController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileMetadata = await this.getFileMetadata.execute({
|
const fileMetadata = await this.getFileMetadata.execute({
|
||||||
userUuid: response.locals.userUuid,
|
ownerUuid: response.locals.userUuid,
|
||||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ export class FilesController extends BaseHttpController {
|
|||||||
response.writeHead(206, headers)
|
response.writeHead(206, headers)
|
||||||
|
|
||||||
const result = await this.streamDownloadFile.execute({
|
const result = await this.streamDownloadFile.execute({
|
||||||
userUuid: response.locals.userUuid,
|
ownerUuid: response.locals.userUuid,
|
||||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||||
startRange,
|
startRange,
|
||||||
endRange,
|
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 { Request, Response } from 'express'
|
||||||
import { inject } from 'inversify'
|
import { inject } from 'inversify'
|
||||||
import { Writable } from 'stream'
|
import { Writable } from 'stream'
|
||||||
import TYPES from '../Bootstrap/Types'
|
import TYPES from '../Bootstrap/Types'
|
||||||
|
import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
|
||||||
import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
|
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 { 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)
|
@controller('/v1/vault/files', TYPES.VaultValetTokenAuthMiddleware)
|
||||||
export class VaultFilesController extends BaseHttpController {
|
export class FilesController extends BaseHttpController {
|
||||||
constructor(
|
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.StreamDownloadFile) private streamDownloadFile: StreamDownloadFile,
|
||||||
@inject(TYPES.GetFileMetadata) private getFileMetadata: GetFileMetadata,
|
@inject(TYPES.GetFileMetadata) private getFileMetadata: GetFileMetadata,
|
||||||
|
@inject(TYPES.RemoveFile) private removeFile: RemoveFile,
|
||||||
@inject(TYPES.MAX_CHUNK_BYTES) private maxChunkBytes: number,
|
@inject(TYPES.MAX_CHUNK_BYTES) private maxChunkBytes: number,
|
||||||
) {
|
) {
|
||||||
super()
|
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('/')
|
@httpGet('/')
|
||||||
async download(
|
async download(
|
||||||
request: Request,
|
request: Request,
|
||||||
response: Response,
|
response: Response,
|
||||||
): Promise<results.BadRequestErrorMessageResult | (() => Writable)> {
|
): 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')
|
return this.badRequest('Not permitted for this operation')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,10 +144,8 @@ export class VaultFilesController extends BaseHttpController {
|
|||||||
chunkSize = this.maxChunkBytes
|
chunkSize = this.maxChunkBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
const locals = response.locals as VaultValetTokenData
|
|
||||||
|
|
||||||
const fileMetadata = await this.getFileMetadata.execute({
|
const fileMetadata = await this.getFileMetadata.execute({
|
||||||
vaultUuid: locals.vaultUuid,
|
ownerUuid: locals.vaultUuid,
|
||||||
resourceRemoteIdentifier: locals.remoteIdentifier,
|
resourceRemoteIdentifier: locals.remoteIdentifier,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -60,7 +166,7 @@ export class VaultFilesController extends BaseHttpController {
|
|||||||
response.writeHead(206, headers)
|
response.writeHead(206, headers)
|
||||||
|
|
||||||
const result = await this.streamDownloadFile.execute({
|
const result = await this.streamDownloadFile.execute({
|
||||||
vaultUuid: locals.vaultUuid,
|
ownerUuid: locals.vaultUuid,
|
||||||
resourceRemoteIdentifier: locals.remoteIdentifier,
|
resourceRemoteIdentifier: locals.remoteIdentifier,
|
||||||
startRange,
|
startRange,
|
||||||
endRange,
|
endRange,
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export class VaultValetTokenAuthMiddleware extends BaseMiddleware {
|
|||||||
vaultUuid: valetTokenData.vaultUuid,
|
vaultUuid: valetTokenData.vaultUuid,
|
||||||
remoteIdentifier: valetTokenData.remoteIdentifier,
|
remoteIdentifier: valetTokenData.remoteIdentifier,
|
||||||
permittedOperation: valetTokenData.permittedOperation,
|
permittedOperation: valetTokenData.permittedOperation,
|
||||||
|
uploadBytesUsed: valetTokenData.uploadBytesUsed,
|
||||||
|
uploadBytesLimit: valetTokenData.uploadBytesLimit,
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(response.locals, whitelistedData)
|
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 { TimerInterface } from '@standardnotes/time'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable } from 'inversify'
|
||||||
|
|
||||||
@@ -9,15 +15,15 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
|||||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||||
|
|
||||||
createFileRemovedEvent(payload: {
|
createUserFileRemovedEvent(payload: {
|
||||||
userUuid: string
|
userUuid: string
|
||||||
filePath: string
|
filePath: string
|
||||||
fileName: string
|
fileName: string
|
||||||
fileByteSize: number
|
fileByteSize: number
|
||||||
regularSubscriptionUuid: string
|
regularSubscriptionUuid: string
|
||||||
}): FileRemovedEvent {
|
}): UserFileRemovedEvent {
|
||||||
return {
|
return {
|
||||||
type: 'FILE_REMOVED',
|
type: 'USER_FILE_REMOVED',
|
||||||
createdAt: this.timer.getUTCDate(),
|
createdAt: this.timer.getUTCDate(),
|
||||||
meta: {
|
meta: {
|
||||||
correlation: {
|
correlation: {
|
||||||
@@ -30,14 +36,14 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createFileUploadedEvent(payload: {
|
createUserFileUploadedEvent(payload: {
|
||||||
userUuid: string
|
userUuid: string
|
||||||
filePath: string
|
filePath: string
|
||||||
fileName: string
|
fileName: string
|
||||||
fileByteSize: number
|
fileByteSize: number
|
||||||
}): FileUploadedEvent {
|
}): UserFileUploadedEvent {
|
||||||
return {
|
return {
|
||||||
type: 'FILE_UPLOADED',
|
type: 'USER_FILE_UPLOADED',
|
||||||
createdAt: this.timer.getUTCDate(),
|
createdAt: this.timer.getUTCDate(),
|
||||||
meta: {
|
meta: {
|
||||||
correlation: {
|
correlation: {
|
||||||
@@ -49,4 +55,44 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
|||||||
payload,
|
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 {
|
export interface DomainEventFactoryInterface {
|
||||||
createFileUploadedEvent(payload: {
|
createUserFileUploadedEvent(payload: {
|
||||||
userUuid: string
|
userUuid: string
|
||||||
filePath: string
|
filePath: string
|
||||||
fileName: string
|
fileName: string
|
||||||
fileByteSize: number
|
fileByteSize: number
|
||||||
}): FileUploadedEvent
|
}): UserFileUploadedEvent
|
||||||
createFileRemovedEvent(payload: {
|
createUserFileRemovedEvent(payload: {
|
||||||
userUuid: string
|
userUuid: string
|
||||||
filePath: string
|
filePath: string
|
||||||
fileName: string
|
fileName: string
|
||||||
fileByteSize: number
|
fileByteSize: number
|
||||||
regularSubscriptionUuid: string
|
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({
|
const response = await this.markFilesToBeRemoved.execute({
|
||||||
userUuid: event.payload.userUuid,
|
ownerUuid: event.payload.userUuid,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
@@ -32,7 +32,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
|||||||
|
|
||||||
for (const fileRemoved of response.filesRemoved) {
|
for (const fileRemoved of response.filesRemoved) {
|
||||||
await this.domainEventPublisher.publish(
|
await this.domainEventPublisher.publish(
|
||||||
this.domainEventFactory.createFileRemovedEvent({
|
this.domainEventFactory.createUserFileRemovedEvent({
|
||||||
regularSubscriptionUuid: event.payload.regularSubscriptionUuid,
|
regularSubscriptionUuid: event.payload.regularSubscriptionUuid,
|
||||||
...fileRemoved,
|
...fileRemoved,
|
||||||
}),
|
}),
|
||||||
|
|||||||
+2
-2
@@ -23,7 +23,7 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.markFilesToBeRemoved.execute({
|
const response = await this.markFilesToBeRemoved.execute({
|
||||||
userUuid: event.payload.inviteeIdentifier,
|
ownerUuid: event.payload.inviteeIdentifier,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
@@ -32,7 +32,7 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE
|
|||||||
|
|
||||||
for (const fileRemoved of response.filesRemoved) {
|
for (const fileRemoved of response.filesRemoved) {
|
||||||
await this.domainEventPublisher.publish(
|
await this.domainEventPublisher.publish(
|
||||||
this.domainEventFactory.createFileRemovedEvent({
|
this.domainEventFactory.createUserFileRemovedEvent({
|
||||||
regularSubscriptionUuid: event.payload.inviterSubscriptionUuid,
|
regularSubscriptionUuid: event.payload.inviterSubscriptionUuid,
|
||||||
...fileRemoved,
|
...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 {
|
try {
|
||||||
this.logger.debug(`Creating upload session for resource: ${dto.resourceRemoteIdentifier}`)
|
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)
|
const uploadId = await this.fileUploader.createUploadSession(filePath)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type CreateUploadSessionDTO = {
|
export type CreateUploadSessionDTO = {
|
||||||
userUuid: string
|
ownerUuid: string
|
||||||
resourceRemoteIdentifier: string
|
resourceRemoteIdentifier: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class FinishUploadSession implements UseCaseInterface {
|
|||||||
try {
|
try {
|
||||||
this.logger.debug(`Finishing upload session for resource: ${dto.resourceRemoteIdentifier}`)
|
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)
|
const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath)
|
||||||
if (uploadId === undefined) {
|
if (uploadId === undefined) {
|
||||||
@@ -53,14 +53,25 @@ export class FinishUploadSession implements UseCaseInterface {
|
|||||||
|
|
||||||
await this.fileUploader.finishUploadSession(uploadId, filePath, uploadChunkResults)
|
await this.fileUploader.finishUploadSession(uploadId, filePath, uploadChunkResults)
|
||||||
|
|
||||||
await this.domainEventPublisher.publish(
|
if (dto.ownerType === 'user') {
|
||||||
this.domainEventFactory.createFileUploadedEvent({
|
await this.domainEventPublisher.publish(
|
||||||
userUuid: dto.userUuid,
|
this.domainEventFactory.createUserFileUploadedEvent({
|
||||||
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
|
userUuid: dto.ownerUuid,
|
||||||
fileName: dto.resourceRemoteIdentifier,
|
filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
||||||
fileByteSize: totalFileSize,
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type FinishUploadSessionDTO = {
|
export type FinishUploadSessionDTO = {
|
||||||
userUuid: string
|
ownerUuid: string
|
||||||
|
ownerType: 'user' | 'vault'
|
||||||
resourceRemoteIdentifier: string
|
resourceRemoteIdentifier: string
|
||||||
uploadBytesUsed: number
|
uploadBytesUsed: number
|
||||||
uploadBytesLimit: number
|
uploadBytesLimit: number
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Logger } from 'winston'
|
|||||||
import TYPES from '../../../Bootstrap/Types'
|
import TYPES from '../../../Bootstrap/Types'
|
||||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||||
import { UseCaseInterface } from '../UseCaseInterface'
|
import { UseCaseInterface } from '../UseCaseInterface'
|
||||||
import { GetFileMetadataDTO, isGetFileMetadataVaultOwned } from './GetFileMetadataDTO'
|
import { GetFileMetadataDTO } from './GetFileMetadataDTO'
|
||||||
import { GetFileMetadataResponse } from './GetFileMetadataResponse'
|
import { GetFileMetadataResponse } from './GetFileMetadataResponse'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@@ -14,16 +14,15 @@ export class GetFileMetadata implements UseCaseInterface {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {
|
async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {
|
||||||
const ownerUuid = isGetFileMetadataVaultOwned(dto) ? dto.vaultUuid : dto.userUuid
|
|
||||||
try {
|
try {
|
||||||
const size = await this.fileDownloader.getFileSize(`${ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
size,
|
size,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Could not get file metadata.',
|
message: 'Could not get file metadata.',
|
||||||
|
|||||||
@@ -1,15 +1,4 @@
|
|||||||
export type GetFileMetadataDTO = GetVaultFileMetadataDTO | GetUserFileMetadataDTO
|
export type GetFileMetadataDTO = {
|
||||||
|
ownerUuid: string
|
||||||
export type GetVaultFileMetadataDTO = {
|
|
||||||
vaultUuid: string
|
|
||||||
resourceRemoteIdentifier: 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> {
|
async execute(dto: MarkFilesToBeRemovedDTO): Promise<MarkFilesToBeRemovedResponse> {
|
||||||
try {
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
filesRemoved,
|
filesRemoved,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export type MarkFilesToBeRemovedDTO = {
|
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 { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||||
import { FileRemoverInterface } from '../../Services/FileRemoverInterface'
|
import { FileRemoverInterface } from '../../Services/FileRemoverInterface'
|
||||||
import { UseCaseInterface } from '../UseCaseInterface'
|
import { UseCaseInterface } from '../UseCaseInterface'
|
||||||
import { RemoveFileDTO } from './RemoveFileDTO'
|
import { RemoveFileDTO, isRemoveUserFileDTO } from './RemoveFileDTO'
|
||||||
import { RemoveFileResponse } from './RemoveFileResponse'
|
import { RemoveFileResponse } from './RemoveFileResponse'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@@ -22,19 +22,32 @@ export class RemoveFile implements UseCaseInterface {
|
|||||||
try {
|
try {
|
||||||
this.logger.debug(`Removing file: ${dto.resourceRemoteIdentifier}`)
|
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)
|
const removedFileSize = await this.fileRemover.remove(filePath)
|
||||||
|
|
||||||
await this.domainEventPublisher.publish(
|
if (isRemoveUserFileDTO(dto)) {
|
||||||
this.domainEventFactory.createFileRemovedEvent({
|
await this.domainEventPublisher.publish(
|
||||||
userUuid: dto.userUuid,
|
this.domainEventFactory.createUserFileRemovedEvent({
|
||||||
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
|
userUuid: dto.userUuid,
|
||||||
fileName: dto.resourceRemoteIdentifier,
|
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
|
||||||
fileByteSize: removedFileSize,
|
fileName: dto.resourceRemoteIdentifier,
|
||||||
regularSubscriptionUuid: dto.regularSubscriptionUuid,
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
export type RemoveFileDTO = {
|
export type RemoveFileDTO = RemoveUserFileDTO | RemoveVaultFileDTO
|
||||||
|
|
||||||
|
export type RemoveUserFileDTO = {
|
||||||
userUuid: string
|
userUuid: string
|
||||||
resourceRemoteIdentifier: string
|
resourceRemoteIdentifier: string
|
||||||
regularSubscriptionUuid: 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 TYPES from '../../../Bootstrap/Types'
|
||||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||||
import { UseCaseInterface } from '../UseCaseInterface'
|
import { UseCaseInterface } from '../UseCaseInterface'
|
||||||
import { StreamDownloadFileDTO, isStreamDownloadFileVaultOwned } from './StreamDownloadFileDTO'
|
import { StreamDownloadFileDTO } from './StreamDownloadFileDTO'
|
||||||
import { StreamDownloadFileResponse } from './StreamDownloadFileResponse'
|
import { StreamDownloadFileResponse } from './StreamDownloadFileResponse'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@@ -14,10 +14,9 @@ export class StreamDownloadFile implements UseCaseInterface {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(dto: StreamDownloadFileDTO): Promise<StreamDownloadFileResponse> {
|
async execute(dto: StreamDownloadFileDTO): Promise<StreamDownloadFileResponse> {
|
||||||
const ownerUuid = isStreamDownloadFileVaultOwned(dto) ? dto.vaultUuid : dto.userUuid
|
|
||||||
try {
|
try {
|
||||||
const readStream = await this.fileDownloader.createDownloadStream(
|
const readStream = await this.fileDownloader.createDownloadStream(
|
||||||
`${ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
||||||
dto.startRange,
|
dto.startRange,
|
||||||
dto.endRange,
|
dto.endRange,
|
||||||
)
|
)
|
||||||
@@ -27,7 +26,9 @@ export class StreamDownloadFile implements UseCaseInterface {
|
|||||||
readStream,
|
readStream,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
export type StreamDownloadFileDTO = StreamDownloadVaultFileDTO | StreamDownloadUserFileDTO
|
export type StreamDownloadFileDTO = {
|
||||||
|
ownerUuid: string
|
||||||
export type StreamDownloadVaultFileDTO = {
|
|
||||||
vaultUuid: string
|
|
||||||
resourceRemoteIdentifier: string
|
resourceRemoteIdentifier: string
|
||||||
startRange: number
|
startRange: number
|
||||||
endRange: 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}`,
|
`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)
|
const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath)
|
||||||
if (uploadId === undefined) {
|
if (uploadId === undefined) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ChunkId } from '../../Upload/ChunkId'
|
|||||||
export type UploadFileChunkDTO = {
|
export type UploadFileChunkDTO = {
|
||||||
data: Uint8Array
|
data: Uint8Array
|
||||||
chunkId: ChunkId
|
chunkId: ChunkId
|
||||||
userUuid: string
|
ownerUuid: string
|
||||||
resourceRemoteIdentifier: string
|
resourceRemoteIdentifier: string
|
||||||
resourceUnencryptedFileSize: number
|
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',
|
Read = 'read',
|
||||||
Write = 'write',
|
Write = 'write',
|
||||||
Delete = 'delete',
|
Delete = 'delete',
|
||||||
|
Move = 'move',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,11 @@ export type VaultValetTokenData = {
|
|||||||
vaultUuid: string
|
vaultUuid: string
|
||||||
permittedOperation: ValetTokenOperation
|
permittedOperation: ValetTokenOperation
|
||||||
remoteIdentifier: string
|
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',
|
name: 'vault_key_timestamp',
|
||||||
type: 'bigint',
|
type: 'bigint',
|
||||||
}),
|
}),
|
||||||
|
new TableColumn({
|
||||||
|
name: 'file_upload_bytes_used',
|
||||||
|
type: 'bigint',
|
||||||
|
}),
|
||||||
|
new TableColumn({
|
||||||
|
name: 'file_upload_bytes_limit',
|
||||||
|
type: 'bigint',
|
||||||
|
}),
|
||||||
new TableColumn({
|
new TableColumn({
|
||||||
name: 'created_at_timestamp',
|
name: 'created_at_timestamp',
|
||||||
type: 'bigint',
|
type: 'bigint',
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import { ContactServiceInterface } from '../Domain/Contact/Service/ContactServic
|
|||||||
import { ContactService } from '../Domain/Contact/Service/ContactService'
|
import { ContactService } from '../Domain/Contact/Service/ContactService'
|
||||||
import { ContactFactoryInterface } from '../Domain/Contact/Factory/ContactFactoryInterface'
|
import { ContactFactoryInterface } from '../Domain/Contact/Factory/ContactFactoryInterface'
|
||||||
import { SnjsVersionFilter } from '../Domain/Item/SaveRule/SnjsVersionFilter'
|
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 {
|
export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
|
||||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||||
@@ -115,10 +115,11 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
|
|||||||
})
|
})
|
||||||
|
|
||||||
container
|
container
|
||||||
.bind<CreateVaultFileReadValetToken>(TYPES.CreateVaultFileReadValetToken)
|
.bind<CreateVaultFileValetToken>(TYPES.CreateVaultFileReadValetToken)
|
||||||
.toDynamicValue((context: interfaces.Context) => {
|
.toDynamicValue((context: interfaces.Context) => {
|
||||||
return new CreateVaultFileReadValetToken(
|
return new CreateVaultFileValetToken(
|
||||||
context.container.get(TYPES.GetGlobalItem),
|
context.container.get(TYPES.GetGlobalItem),
|
||||||
|
context.container.get(TYPES.VaultService),
|
||||||
context.container.get(TYPES.VaultUserService),
|
context.container.get(TYPES.VaultUserService),
|
||||||
context.container.get(TYPES.ValetTokenEncoder),
|
context.container.get(TYPES.ValetTokenEncoder),
|
||||||
context.container.get(TYPES.VALET_TOKEN_TTL),
|
context.container.get(TYPES.VALET_TOKEN_TTL),
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ const TYPES = {
|
|||||||
DuplicateItemSyncedEventHandler: Symbol.for('DuplicateItemSyncedEventHandler'),
|
DuplicateItemSyncedEventHandler: Symbol.for('DuplicateItemSyncedEventHandler'),
|
||||||
EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'),
|
EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'),
|
||||||
ItemRevisionCreationRequestedEventHandler: Symbol.for('ItemRevisionCreationRequestedEventHandler'),
|
ItemRevisionCreationRequestedEventHandler: Symbol.for('ItemRevisionCreationRequestedEventHandler'),
|
||||||
|
VaultFileUploadedEventHandler: Symbol.for('VaultFileUploadedEventHandler'),
|
||||||
|
VaultFileRemovedEventHandler: Symbol.for('VaultFileRemovedEventHandler'),
|
||||||
|
|
||||||
ContentDecoder: Symbol.for('ContentDecoder'),
|
ContentDecoder: Symbol.for('ContentDecoder'),
|
||||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import { ItemRevisionCreationRequestedEventHandler } from '../Domain/Handler/Ite
|
|||||||
import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
|
import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
|
||||||
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
|
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
|
||||||
import { UserCredentialsChangedEventHandler } from '../Domain/Handler/UserCredentialsChangedEventHandler'
|
import { UserCredentialsChangedEventHandler } from '../Domain/Handler/UserCredentialsChangedEventHandler'
|
||||||
|
import { VaultFileUploadedEventHandler } from '../Domain/Handler/VaultFileUploadedEventHandler'
|
||||||
|
import { VaultFileRemovedEventHandler } from '../Domain/Handler/VaultFileRemovedEventHandler'
|
||||||
|
|
||||||
export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
|
export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
|
||||||
private readonly DEFAULT_FILE_UPLOAD_PATH = `${__dirname}/../../uploads`
|
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
|
// Services
|
||||||
container.bind<ContentDecoder>(TYPES.ContentDecoder).toDynamicValue(() => new ContentDecoder())
|
container.bind<ContentDecoder>(TYPES.ContentDecoder).toDynamicValue(() => new ContentDecoder())
|
||||||
container.bind<AxiosInstance>(TYPES.HTTPClient).toDynamicValue(() => axios.create())
|
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)],
|
['EMAIL_BACKUP_REQUESTED', context.container.get(TYPES.EmailBackupRequestedEventHandler)],
|
||||||
['ITEM_REVISION_CREATION_REQUESTED', context.container.get(TYPES.ItemRevisionCreationRequestedEventHandler)],
|
['ITEM_REVISION_CREATION_REQUESTED', context.container.get(TYPES.ItemRevisionCreationRequestedEventHandler)],
|
||||||
['USER_CREDENTIALS_CHANGED', container.get(TYPES.UserCredentialsChangedEventHandler)],
|
['USER_CREDENTIALS_CHANGED', container.get(TYPES.UserCredentialsChangedEventHandler)],
|
||||||
|
['VAULT_FILE_REMOVED', container.get(TYPES.VaultFileRemovedEventHandler)],
|
||||||
|
['VAULT_FILE_UPLOADED', container.get(TYPES.VaultFileUploadedEventHandler)],
|
||||||
])
|
])
|
||||||
|
|
||||||
const handler =
|
const handler =
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { Vault } from '../Domain/Vault/Model/Vault'
|
|||||||
import { VaultProjection } from '../Projection/VaultProjection'
|
import { VaultProjection } from '../Projection/VaultProjection'
|
||||||
import { VaultUserProjection } from '../Projection/VaultUserProjection'
|
import { VaultUserProjection } from '../Projection/VaultUserProjection'
|
||||||
import { VaultUserServiceInterface } from '../Domain/VaultUser/Service/VaultUserServiceInterface'
|
import { VaultUserServiceInterface } from '../Domain/VaultUser/Service/VaultUserServiceInterface'
|
||||||
import { CreateVaultFileReadValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetToken'
|
import { CreateVaultFileValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken'
|
||||||
|
|
||||||
@controller('/vaults')
|
@controller('/vaults')
|
||||||
export class VaultsController extends BaseHttpController {
|
export class VaultsController extends BaseHttpController {
|
||||||
@@ -26,7 +26,7 @@ export class VaultsController extends BaseHttpController {
|
|||||||
@inject(TYPES.VaultUserService) private vaultUserService: VaultUserServiceInterface,
|
@inject(TYPES.VaultUserService) private vaultUserService: VaultUserServiceInterface,
|
||||||
@inject(TYPES.VaultProjector) private vaultProjector: ProjectorInterface<Vault, VaultProjection>,
|
@inject(TYPES.VaultProjector) private vaultProjector: ProjectorInterface<Vault, VaultProjection>,
|
||||||
@inject(TYPES.VaultUserProjector) private vaultUserProjector: ProjectorInterface<VaultUser, VaultUserProjection>,
|
@inject(TYPES.VaultUserProjector) private vaultUserProjector: ProjectorInterface<VaultUser, VaultUserProjection>,
|
||||||
@inject(TYPES.CreateVaultFileReadValetToken) private createVaultFileReadValetToken: CreateVaultFileReadValetToken,
|
@inject(TYPES.CreateVaultFileReadValetToken) private createVaultFileReadValetToken: CreateVaultFileValetToken,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,8 @@ export class VaultsController extends BaseHttpController {
|
|||||||
vaultUuid: request.params.vaultUuid,
|
vaultUuid: request.params.vaultUuid,
|
||||||
fileUuid: request.body.file_uuid,
|
fileUuid: request.body.file_uuid,
|
||||||
remoteIdentifier: request.body.remote_identifier,
|
remoteIdentifier: request.body.remote_identifier,
|
||||||
|
moveOperationType: request.body.move_operation_type,
|
||||||
|
operation: request.body.operation,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (valetTokenResult.success === false) {
|
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
|
user_uuid: string
|
||||||
specified_items_key_uuid: string
|
specified_items_key_uuid: string
|
||||||
vault_key_timestamp: number
|
vault_key_timestamp: number
|
||||||
|
file_upload_bytes_used: number
|
||||||
|
file_upload_bytes_limit: number
|
||||||
created_at_timestamp?: number
|
created_at_timestamp?: number
|
||||||
updated_at_timestamp?: number
|
updated_at_timestamp?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,18 @@ export class Vault {
|
|||||||
})
|
})
|
||||||
declare vaultKeyTimestamp: number
|
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({
|
@Column({
|
||||||
name: 'created_at_timestamp',
|
name: 'created_at_timestamp',
|
||||||
type: 'bigint',
|
type: 'bigint',
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export class VaultService implements VaultServiceInterface {
|
|||||||
user_uuid: dto.userUuid,
|
user_uuid: dto.userUuid,
|
||||||
specified_items_key_uuid: dto.specifiedItemsKeyUuid,
|
specified_items_key_uuid: dto.specifiedItemsKeyUuid,
|
||||||
vault_key_timestamp: dto.vaultKeyTimestamp,
|
vault_key_timestamp: dto.vaultKeyTimestamp,
|
||||||
|
file_upload_bytes_limit: 1_000_000,
|
||||||
|
file_upload_bytes_used: 0,
|
||||||
created_at_timestamp: timestamp,
|
created_at_timestamp: timestamp,
|
||||||
updated_at_timestamp: timestamp,
|
updated_at_timestamp: timestamp,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export type VaultProjection = {
|
|||||||
user_uuid: string
|
user_uuid: string
|
||||||
specified_items_key_uuid: string
|
specified_items_key_uuid: string
|
||||||
vault_key_timestamp: number
|
vault_key_timestamp: number
|
||||||
|
file_upload_bytes_used: number
|
||||||
|
file_upload_bytes_limit: number
|
||||||
created_at_timestamp: number
|
created_at_timestamp: number
|
||||||
updated_at_timestamp: number
|
updated_at_timestamp: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export class VaultProjector implements ProjectorInterface<Vault, VaultProjection
|
|||||||
user_uuid: vault.userUuid,
|
user_uuid: vault.userUuid,
|
||||||
specified_items_key_uuid: vault.specifiedItemsKeyUuid,
|
specified_items_key_uuid: vault.specifiedItemsKeyUuid,
|
||||||
vault_key_timestamp: vault.vaultKeyTimestamp,
|
vault_key_timestamp: vault.vaultKeyTimestamp,
|
||||||
|
file_upload_bytes_used: vault.fileUploadBytesUsed,
|
||||||
|
file_upload_bytes_limit: vault.fileUploadBytesLimit,
|
||||||
created_at_timestamp: vault.createdAtTimestamp,
|
created_at_timestamp: vault.createdAtTimestamp,
|
||||||
updated_at_timestamp: vault.updatedAtTimestamp,
|
updated_at_timestamp: vault.updatedAtTimestamp,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user