Compare commits

...

8 Commits

Author SHA1 Message Date
standardci
b510284e01 chore(release): publish new version
- @standardnotes/analytics@2.25.16
 - @standardnotes/api-gateway@1.71.1
 - @standardnotes/auth-server@1.134.0
 - @standardnotes/domain-events-infra@1.12.13
 - @standardnotes/domain-events@2.116.0
 - @standardnotes/event-store@1.11.22
 - @standardnotes/files-server@1.22.0
 - @standardnotes/home-server@1.15.0
 - @standardnotes/revisions-server@1.26.10
 - @standardnotes/scheduler-server@1.20.26
 - @standardnotes/security@1.11.0
 - @standardnotes/syncing-server@1.83.0
 - @standardnotes/websockets-server@1.10.20
2023-08-23 06:47:38 +00:00
Karol Sójko
205a1ed637 feat: add handling file moving and updating storage quota (#705)
* feat: add handling file moving and updating storage quota

* fix: getting file metada when moving files

* fix: missing event handler binding
2023-08-23 08:09:34 +02:00
standardci
2073c735a5 chore(release): publish new version
- @standardnotes/analytics@2.25.15
 - @standardnotes/api-gateway@1.71.0
 - @standardnotes/auth-server@1.133.0
 - @standardnotes/domain-events-infra@1.12.12
 - @standardnotes/domain-events@2.115.1
 - @standardnotes/event-store@1.11.21
 - @standardnotes/files-server@1.21.0
 - @standardnotes/home-server@1.14.2
 - @standardnotes/revisions-server@1.26.9
 - @standardnotes/scheduler-server@1.20.25
 - @standardnotes/security@1.10.0
 - @standardnotes/syncing-server@1.82.0
 - @standardnotes/websockets-server@1.10.19
2023-08-22 09:23:30 +00:00
Karol Sójko
34085ac6fb feat: consider shared vault owner quota when uploading files to shared vault (#704)
* fix(auth): updating storage quota on shared subscriptions

* fix(syncing-server): turn shared vault and key associations into value objects

* feat: consider shared vault owner quota when uploading files to shared vault

* fix: add passing x-shared-vault-owner-context value

* fix: refactor creating cross service token to not throw errors

* fix: caching cross service token

* fix: missing header in http service proxy
2023-08-22 10:49:58 +02:00
standardci
3d6559921b chore(release): publish new version
- @standardnotes/home-server@1.14.1
 - @standardnotes/scheduler-server@1.20.24
 - @standardnotes/syncing-server@1.81.0
2023-08-21 09:00:52 +00:00
Karol Sójko
15a7f0e71a fix(syncing-server): DocumentDB retry writes support (#703)
* fix(syncing-server): DocumentDB retry writes support

* fix: auth source for mongo
2023-08-21 10:25:56 +02:00
Karol Sójko
3e56243d6f fix(scheduler): remove exit interview form link (#702) 2023-08-21 08:42:21 +02:00
Karol Sójko
032fcb938d feat(syncing-server): add use case for migrating items from one database to another (#701) 2023-08-18 17:25:24 +02:00
116 changed files with 1870 additions and 674 deletions

View File

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

View File

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

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.71.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.0...@standardnotes/api-gateway@1.71.1) (2023-08-23)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/api-gateway/issues/704)) ([34085ac](https://github.com/standardnotes/api-gateway/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
## [1.70.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.4...@standardnotes/api-gateway@1.70.5) (2023-08-18)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

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

View File

@@ -27,16 +27,23 @@ export abstract class AuthMiddleware extends BaseMiddleware {
}
const authHeaderValue = request.headers.authorization as string
const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
const cacheKey = `${authHeaderValue}${
sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
}`
try {
let crossServiceTokenFetchedFromCache = true
let crossServiceToken = null
if (this.crossServiceTokenCacheTTL) {
crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue)
crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
}
if (crossServiceToken === null) {
const authResponse = await this.serviceProxy.validateSession(authHeaderValue)
const authResponse = await this.serviceProxy.validateSession({
authorization: authHeaderValue,
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
})
if (!this.handleSessionValidationResponse(authResponse, response, next)) {
return
@@ -52,7 +59,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({
authorizationHeaderValue: authHeaderValue,
key: cacheKey,
encodedCrossServiceToken: crossServiceToken,
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
userUuid: decodedToken.user.uuid,
@@ -62,6 +69,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
} catch (error) {
const errorMessage = (error as AxiosError).isAxiosError
? JSON.stringify((error as AxiosError).response?.data)

View File

@@ -12,29 +12,29 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
constructor(private timer: TimerInterface) {}
async set(dto: {
authorizationHeaderValue: string
key: string
encodedCrossServiceToken: string
expiresAtInSeconds: number
userUuid: string
}): Promise<void> {
let userAuthHeaders = []
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
if (userAuthHeadersJSON) {
userAuthHeaders = JSON.parse(userAuthHeadersJSON)
let userKeys = []
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
if (userKeysJSON) {
userKeys = JSON.parse(userKeysJSON)
}
userAuthHeaders.push(dto.authorizationHeaderValue)
userKeys.push(dto.key)
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userAuthHeaders))
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userKeys))
this.crossServiceTokenTTLCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
}
async get(authorizationHeaderValue: string): Promise<string | null> {
async get(key: string): Promise<string | null> {
this.invalidateExpiredTokens()
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${authorizationHeaderValue}`)
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${key}`)
if (!cachedToken) {
return null
}
@@ -43,15 +43,15 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
}
async invalidate(userUuid: string): Promise<void> {
let userAuthorizationHeaderValues = []
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
if (userAuthHeadersJSON) {
userAuthorizationHeaderValues = JSON.parse(userAuthHeadersJSON)
let userKeyValues = []
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
if (userKeysJSON) {
userKeyValues = JSON.parse(userKeysJSON)
}
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
this.crossServiceTokenCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
for (const key of userKeyValues) {
this.crossServiceTokenCache.delete(`${this.PREFIX}:${key}`)
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${key}`)
}
this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
this.crossServiceTokenTTLCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)

View File

@@ -12,32 +12,32 @@ export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterf
constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
async set(dto: {
authorizationHeaderValue: string
key: string
encodedCrossServiceToken: string
expiresAtInSeconds: number
userUuid: string
}): Promise<void> {
const pipeline = this.redisClient.pipeline()
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.authorizationHeaderValue)
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.key)
pipeline.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
pipeline.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
pipeline.expireat(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
await pipeline.exec()
}
async get(authorizationHeaderValue: string): Promise<string | null> {
return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`)
async get(key: string): Promise<string | null> {
return this.redisClient.get(`${this.PREFIX}:${key}`)
}
async invalidate(userUuid: string): Promise<void> {
const userAuthorizationHeaderValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
const userKeyValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
const pipeline = this.redisClient.pipeline()
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`)
for (const key of userKeyValues) {
pipeline.del(`${this.PREFIX}:${key}`)
}
pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)

View File

@@ -1,10 +1,10 @@
export interface CrossServiceTokenCacheInterface {
set(dto: {
authorizationHeaderValue: string
key: string
encodedCrossServiceToken: string
expiresAtInSeconds: number
userUuid: string
}): Promise<void>
get(authorizationHeaderValue: string): Promise<string | null>
get(key: string): Promise<string | null>
invalidate(userUuid: string): Promise<void>
}

View File

@@ -24,14 +24,16 @@ export class HttpServiceProxy implements ServiceProxyInterface {
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
) {}
async validateSession(
authorizationHeaderValue: string,
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
async validateSession(headers: {
authorization: string
sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authResponse = await this.httpClient.request({
method: 'POST',
headers: {
Authorization: authorizationHeaderValue,
Authorization: headers.authorization,
Accept: 'application/json',
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
validateStatus: (status: number) => {
return status >= 200 && status < 500

View File

@@ -50,7 +50,7 @@ export interface ServiceProxyInterface {
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
): Promise<void>
validateSession(authorizationHeaderValue: string): Promise<{
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
status: number
data: unknown
headers: {

View File

@@ -6,9 +6,10 @@ import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
export class DirectCallServiceProxy implements ServiceProxyInterface {
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
async validateSession(
authorizationHeaderValue: string,
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
async validateSession(headers: {
authorization: string
sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
if (!authService) {
throw new Error('Auth service not found')
@@ -17,7 +18,8 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
const serviceResponse = (await authService.handleRequest(
{
headers: {
authorization: authorizationHeaderValue,
authorization: headers.authorization,
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
} as never,
{} as never,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,8 @@ import { Role } from '../../Role/Role'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
import { GetSetting } from '../GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('CreateCrossServiceToken', () => {
let userProjector: ProjectorInterface<User>
@@ -15,6 +17,7 @@ describe('CreateCrossServiceToken', () => {
let roleProjector: ProjectorInterface<Role>
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
let userRepository: UserRepositoryInterface
let getSettingUseCase: GetSetting
const jwtTTL = 60
let session: Session
@@ -22,7 +25,15 @@ describe('CreateCrossServiceToken', () => {
let role: Role
const createUseCase = () =>
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
new CreateCrossServiceToken(
userProjector,
sessionProjector,
roleProjector,
tokenEncoder,
userRepository,
jwtTTL,
getSettingUseCase,
)
beforeEach(() => {
session = {} as jest.Mocked<Session>
@@ -50,6 +61,9 @@ describe('CreateCrossServiceToken', () => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
getSettingUseCase = {} as jest.Mocked<GetSetting>
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } }))
})
it('should create a cross service token for user', async () => {
@@ -125,28 +139,74 @@ describe('CreateCrossServiceToken', () => {
it('should throw an error if user does not exist', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
let caughtError = null
try {
await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
} catch (error) {
caughtError = error
}
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(caughtError).not.toBeNull()
expect(result.isFailed()).toBeTruthy()
})
it('should throw an error if user uuid is invalid', async () => {
let caughtError = null
try {
await createUseCase().execute({
userUuid: 'invalid',
})
} catch (error) {
caughtError = error
}
const result = await createUseCase().execute({
userUuid: 'invalid',
})
expect(caughtError).not.toBeNull()
expect(result.isFailed()).toBeTruthy()
})
describe('shared vault context', () => {
it('should add shared vault context if shared vault owner uuid is provided', async () => {
await createUseCase().execute({
user,
session,
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
shared_vault_owner_context: {
upload_bytes_limit: 100,
},
user: {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
},
60,
)
})
it('should throw an error if shared vault owner context is sensitive', async () => {
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sensitive: true }))
const result = await createUseCase().execute({
user,
session,
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
})
it('should throw an error if it fails to retrieve shared vault owner setting', async () => {
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const result = await createUseCase().execute({
user,
session,
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
})
})
})

View File

@@ -1,5 +1,6 @@
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
@@ -7,14 +8,13 @@ import { Role } from '../../Role/Role'
import { Session } from '../../Session/Session'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
import { Uuid } from '@standardnotes/domain-core'
import { GetSetting } from '../GetSetting/GetSetting'
import { SettingName } from '@standardnotes/settings'
@injectable()
export class CreateCrossServiceToken implements UseCaseInterface {
export class CreateCrossServiceToken implements UseCaseInterface<string> {
constructor(
@inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>,
@inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface<Session>,
@@ -22,14 +22,16 @@ export class CreateCrossServiceToken implements UseCaseInterface {
@inject(TYPES.Auth_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
@inject(TYPES.Auth_GetSetting)
private getSettingUseCase: GetSetting,
) {}
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
async execute(dto: CreateCrossServiceTokenDTO): Promise<Result<string>> {
let user: User | undefined | null = dto.user
if (user === undefined && dto.userUuid !== undefined) {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error(userUuidOrError.getError())
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
@@ -37,7 +39,7 @@ export class CreateCrossServiceToken implements UseCaseInterface {
}
if (!user) {
throw new Error(`Could not find user with uuid ${dto.userUuid}`)
return Result.fail(`Could not find user with uuid ${dto.userUuid}`)
}
const roles = await user.roles
@@ -45,15 +47,33 @@ export class CreateCrossServiceToken implements UseCaseInterface {
const authTokenData: CrossServiceTokenData = {
user: this.projectUser(user),
roles: this.projectRoles(roles),
shared_vault_owner_context: undefined,
}
if (dto.sharedVaultOwnerContext !== undefined) {
const uploadBytesLimitSettingOrError = await this.getSettingUseCase.execute({
settingName: SettingName.NAMES.FileUploadBytesLimit,
userUuid: dto.sharedVaultOwnerContext,
})
if (uploadBytesLimitSettingOrError.isFailed()) {
return Result.fail(uploadBytesLimitSettingOrError.getError())
}
const uploadBytesLimitSetting = uploadBytesLimitSettingOrError.getValue()
if (uploadBytesLimitSetting.sensitive) {
return Result.fail('Shared vault owner upload bytes limit setting is sensitive!')
}
const uploadBytesLimit = parseInt(uploadBytesLimitSetting.setting.value as string)
authTokenData.shared_vault_owner_context = {
upload_bytes_limit: uploadBytesLimit,
}
}
if (dto.session !== undefined) {
authTokenData.session = this.projectSession(dto.session)
}
return {
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
}
return Result.ok(this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL))
}
private projectUser(user: User): { uuid: string; email: string } {

View File

@@ -6,6 +6,7 @@ export type CreateCrossServiceTokenDTO = Either<
{
user: User
session?: Session
sharedVaultOwnerContext?: string
},
{
userUuid: string

View File

@@ -1,3 +0,0 @@
export type CreateCrossServiceTokenResponse = {
token: string
}

View File

@@ -73,35 +73,30 @@ describe('GetSetting', () => {
describe('no subscription', () => {
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.DropboxBackupFrequency,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
})
it('should not find a setting if the setting name is invalid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({
success: false,
error: {
message: 'Invalid setting name: invalid',
},
})
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })
expect(result.isFailed()).toBeTruthy()
})
it('should not get a setting for user if it does not exist', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: false,
error: {
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
},
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.DropboxBackupFrequency,
})
expect(result.isFailed()).toBeTruthy()
})
it('should not retrieve a sensitive setting for user', async () => {
@@ -112,21 +107,19 @@ describe('GetSetting', () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
success: true,
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
sensitive: true,
})
})
it('should not retrieve a subscription setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'No subscription found.',
},
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeTruthy()
})
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
@@ -137,14 +130,13 @@ describe('GetSetting', () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(
await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MfaSecret,
allowSensitiveRetrieval: true,
}),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MfaSecret,
allowSensitiveRetrieval: true,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
@@ -159,10 +151,12 @@ describe('GetSetting', () => {
})
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
@@ -171,14 +165,11 @@ describe('GetSetting', () => {
it('should not get a suscription setting for user if it does not exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
},
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeTruthy()
})
it('should not retrieve a sensitive subscription setting for user', async () => {
@@ -188,10 +179,12 @@ describe('GetSetting', () => {
.fn()
.mockReturnValue(subscriptionSetting)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
sensitive: true,
})
})
@@ -205,10 +198,12 @@ describe('GetSetting', () => {
})
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
@@ -221,10 +216,12 @@ describe('GetSetting', () => {
})
it('should find a regular subscription only setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.FileUploadBytesLimit,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})

View File

@@ -1,7 +1,7 @@
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { UseCaseInterface } from '../UseCaseInterface'
import TYPES from '../../../Bootstrap/Types'
import { SettingProjector } from '../../../Projection/SettingProjector'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
@@ -14,7 +14,7 @@ import { GetSettingResponse } from './GetSettingResponse'
import { UserSubscription } from '../../Subscription/UserSubscription'
@injectable()
export class GetSetting implements UseCaseInterface {
export class GetSetting implements UseCaseInterface<GetSettingResponse> {
constructor(
@inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector,
@inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
@@ -24,15 +24,10 @@ export class GetSetting implements UseCaseInterface {
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
) {}
async execute(dto: GetSettingDto): Promise<GetSettingResponse> {
async execute(dto: GetSettingDto): Promise<Result<GetSettingResponse>> {
const settingNameOrError = SettingName.create(dto.settingName)
if (settingNameOrError.isFailed()) {
return {
success: false,
error: {
message: settingNameOrError.getError(),
},
}
return Result.fail(settingNameOrError.getError())
}
const settingName = settingNameOrError.getValue()
@@ -47,12 +42,7 @@ export class GetSetting implements UseCaseInterface {
}
if (!subscription) {
return {
success: false,
error: {
message: 'No subscription found.',
},
}
return Result.fail('No subscription found.')
}
const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
@@ -62,28 +52,21 @@ export class GetSetting implements UseCaseInterface {
})
if (subscriptionSetting === null) {
return {
success: false,
error: {
message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`)
}
if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) {
return {
success: true,
return Result.ok({
sensitive: true,
}
})
}
const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting)
return {
success: true,
return Result.ok({
userUuid: dto.userUuid,
setting: simpleSubscriptionSetting,
}
})
}
const setting = await this.settingService.findSettingWithDecryptedValue({
@@ -92,27 +75,20 @@ export class GetSetting implements UseCaseInterface {
})
if (setting === null) {
return {
success: false,
error: {
message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
return Result.fail(`Setting ${settingName.value} for user ${dto.userUuid} not found!`)
}
if (setting.sensitive && !dto.allowSensitiveRetrieval) {
return {
success: true,
return Result.ok({
sensitive: true,
}
})
}
const simpleSetting = await this.settingProjector.projectSimple(setting)
return {
success: true,
return Result.ok({
userUuid: dto.userUuid,
setting: simpleSetting,
}
})
}
}

View File

@@ -1,18 +1,13 @@
import { Either } from '@standardnotes/common'
import { SimpleSetting } from '../../Setting/SimpleSetting'
export type GetSettingResponse =
| {
success: true
userUuid: string
setting: SimpleSetting
}
| {
success: true
sensitive: true
}
| {
success: false
error: {
message: string
}
}
export type GetSettingResponse = Either<
{
userUuid: string
setting: SimpleSetting
},
{
sensitive: true
}
>

View File

@@ -7,7 +7,6 @@ import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO'
import { User } from '../../User/User'
export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
constructor(
@@ -34,23 +33,20 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
}
await this.updateUploadBytesUsedSetting(regularSubscription, user, dto.bytesUsed)
await this.updateUploadBytesUsedSetting(regularSubscription, dto.bytesUsed)
if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, user, dto.bytesUsed)
await this.updateUploadBytesUsedSetting(sharedSubscription, dto.bytesUsed)
}
return Result.ok()
}
private async updateUploadBytesUsedSetting(
subscription: UserSubscription,
user: User,
bytesUsed: number,
): Promise<void> {
private async updateUploadBytesUsedSetting(subscription: UserSubscription, bytesUsed: number): Promise<void> {
let bytesAlreadyUsed = '0'
const subscriptionUser = await subscription.user
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid,
userUuid: subscriptionUser.uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
})
@@ -60,7 +56,7 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
user,
user: subscriptionUser,
props: {
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(),

View File

@@ -7,6 +7,7 @@ import { results } from 'inversify-express-utils'
import { User } from '../../Domain/User/User'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedInternalController', () => {
let getUserFeatures: GetUserFeatures
@@ -73,7 +74,7 @@ describe('AnnotatedInternalController', () => {
request.params.userUuid = '1-2-3'
request.params.settingName = 'foobar'
getSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
const httpResponse = <results.JsonResult>await createController().getSetting(request)
const result = await httpResponse.executeAsync()
@@ -91,7 +92,7 @@ describe('AnnotatedInternalController', () => {
request.params.userUuid = '1-2-3'
request.params.settingName = 'foobar'
getSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.JsonResult>await createController().getSetting(request)
const result = await httpResponse.executeAsync()

View File

@@ -36,16 +36,26 @@ export class AnnotatedInternalController extends BaseHttpController {
@httpGet('/users/:userUuid/settings/:settingName')
async getSetting(request: Request): Promise<results.JsonResult> {
const result = await this.doGetSetting.execute({
const resultOrError = await this.doGetSetting.execute({
userUuid: request.params.userUuid,
settingName: request.params.settingName,
allowSensitiveRetrieval: true,
})
if (result.success) {
return this.json(result)
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json(result, 400)
return this.json({
success: true,
...resultOrError.getValue(),
})
}
}

View File

@@ -11,6 +11,7 @@ import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossService
import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Session } from '../../Domain/Session/Session'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSessionsController', () => {
let getActiveSessionsForUser: GetActiveSessionsForUser
@@ -45,7 +46,7 @@ describe('AnnotatedSessionsController', () => {
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken>
createCrossServiceToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
createCrossServiceToken.execute = jest.fn().mockReturnValue(Result.ok('foobar'))
request = {
params: {},

View File

@@ -10,6 +10,7 @@ import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
import { User } from '../../Domain/User/User'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSettingsController', () => {
let deleteSetting: DeleteSetting
@@ -85,7 +86,7 @@ describe('AnnotatedSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync()
@@ -119,7 +120,7 @@ describe('AnnotatedSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync()

View File

@@ -6,6 +6,7 @@ import { results } from 'inversify-express-utils'
import { AnnotatedSubscriptionSettingsController } from './AnnotatedSubscriptionSettingsController'
import { User } from '../../Domain/User/User'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSubscriptionSettingsController', () => {
let getSetting: GetSetting
@@ -41,7 +42,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync()
@@ -58,7 +59,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync()

View File

@@ -45,12 +45,25 @@ export class BaseSessionsController extends BaseHttpController {
const user = authenticateRequestResponse.user as User
const result = await this.createCrossServiceToken.execute({
const sharedVaultOwnerContext = request.headers['x-shared-vault-owner-context'] as string | undefined
const resultOrError = await this.createCrossServiceToken.execute({
user,
session: authenticateRequestResponse.session,
sharedVaultOwnerContext,
})
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json({ authToken: result.token })
return this.json({ authToken: resultOrError.getValue() })
}
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {

View File

@@ -58,13 +58,22 @@ export class BaseSettingsController extends BaseHttpController {
}
const { userUuid, settingName } = request.params
const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
if (result.success) {
return this.json(result)
const resultOrError = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json(result, 400)
return this.json({
success: true,
...resultOrError.getValue(),
})
}
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {

View File

@@ -14,15 +14,25 @@ export class BaseSubscriptionSettingsController extends BaseHttpController {
}
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.doGetSetting.execute({
const resultOrError = await this.doGetSetting.execute({
userUuid: response.locals.user.uuid,
settingName: request.params.subscriptionSettingName.toUpperCase(),
})
if (result.success) {
return this.json(result)
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json(result, 400)
return this.json({
success: true,
...resultOrError.getValue(),
})
}
}

View File

@@ -46,10 +46,20 @@ export class BaseWebSocketsController extends BaseHttpController {
)
}
const result = await this.createCrossServiceToken.execute({
const resultOrError = await this.createCrossServiceToken.execute({
userUuid: token.userUuid,
})
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json({ authToken: result.token })
return this.json({ authToken: resultOrError.getValue() })
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.22.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.21.0...@standardnotes/files-server@1.22.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/files/issues/705)) ([205a1ed](https://github.com/standardnotes/files/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.21.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.4...@standardnotes/files-server@1.21.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/files/issues/704)) ([34085ac](https://github.com/standardnotes/files/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
## [1.20.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.3...@standardnotes/files-server@1.20.4) (2023-08-18)
**Note:** Version bump only for package @standardnotes/files-server

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { SharedVaultValetTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { SharedVaultValetTokenData, TokenDecoderInterface, ValetTokenOperation } from '@standardnotes/security'
import { Uuid } from '@standardnotes/domain-core'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
@@ -61,6 +61,17 @@ export class SharedVaultValetTokenAuthMiddleware extends BaseMiddleware {
return
}
if (this.userHasNoSpaceToUpload(valetTokenData)) {
response.status(403).send({
error: {
tag: 'no-space',
message: 'The file you are trying to upload is too big. Please ask the vault owner to upgrade subscription',
},
})
return
}
const whitelistedData: SharedVaultValetTokenData = {
sharedVaultUuid: valetTokenData.sharedVaultUuid,
vaultOwnerUuid: valetTokenData.vaultOwnerUuid,
@@ -79,4 +90,32 @@ export class SharedVaultValetTokenAuthMiddleware extends BaseMiddleware {
return next(error)
}
}
private userHasNoSpaceToUpload(valetTokenData: SharedVaultValetTokenData) {
if (![ValetTokenOperation.Write, ValetTokenOperation.Move].includes(valetTokenData.permittedOperation)) {
return false
}
if (valetTokenData.uploadBytesLimit === -1) {
return false
}
const isMovingToNonSharedVault =
valetTokenData.permittedOperation === ValetTokenOperation.Move &&
valetTokenData.moveOperation?.type === 'shared-vault-to-user'
if (isMovingToNonSharedVault) {
return false
}
if (valetTokenData.uploadBytesLimit === undefined) {
return true
}
const remainingUploadSpace = valetTokenData.uploadBytesLimit - valetTokenData.uploadBytesUsed
const consideredUploadSize = valetTokenData.unencryptedFileSize as number
return remainingUploadSpace - consideredUploadSize <= 0
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.26](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.25...@standardnotes/scheduler-server@1.20.26) (2023-08-23)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.25](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.24...@standardnotes/scheduler-server@1.20.25) (2023-08-22)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.24](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.23...@standardnotes/scheduler-server@1.20.24) (2023-08-21)
### Bug Fixes
* **scheduler:** remove exit interview form link ([#702](https://github.com/standardnotes/server/issues/702)) ([3e56243](https://github.com/standardnotes/server/commit/3e56243d6f72f5ac86d4fb2191349ec3a589bc83))
## [1.20.23](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.22...@standardnotes/scheduler-server@1.20.23) (2023-08-18)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

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

View File

@@ -15,8 +15,6 @@ export const html = `<div>
are willing to pay for a product is most crucial for us as we continue to evolve and iterate on Standard
Notes.
</p>
<p>If you have a minute, please fill out this brief exit interview: </p>
<a href="https://standardnotes.typeform.com/to/dX5lzPtm">Short Exit Interview →</a>
<p>
Our team reads every single response, and your feedback will be shared with the relevant department within our
team.

View File

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

View File

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

View File

@@ -5,6 +5,9 @@ export type CrossServiceTokenData = {
uuid: string
email: string
}
shared_vault_owner_context?: {
upload_bytes_limit: number
}
roles: Array<Role>
session?: {
uuid: string

View File

@@ -8,10 +8,16 @@ export interface SharedVaultValetTokenData {
remoteIdentifier: string
unencryptedFileSize?: number
uploadBytesUsed: number
uploadBytesLimit: number
uploadBytesLimit?: number
moveOperation?: {
type: SharedVaultMoveType
fromUuid: string
toUuid: string
from: {
sharedVaultUuid?: string
ownerUuid: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
}
}
}

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.83.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.82.0...@standardnotes/syncing-server@1.83.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/syncing-server-js/issues/705)) ([205a1ed](https://github.com/standardnotes/syncing-server-js/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.82.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.81.0...@standardnotes/syncing-server@1.82.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/syncing-server-js/issues/704)) ([34085ac](https://github.com/standardnotes/syncing-server-js/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
# [1.81.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.80.0...@standardnotes/syncing-server@1.81.0) (2023-08-21)
### Bug Fixes
* **syncing-server:** DocumentDB retry writes support ([#703](https://github.com/standardnotes/syncing-server-js/issues/703)) ([15a7f0e](https://github.com/standardnotes/syncing-server-js/commit/15a7f0e71ac2f6c355fb73208559a8fd822773aa))
### Features
* **syncing-server:** add use case for migrating items from one database to another ([#701](https://github.com/standardnotes/syncing-server-js/issues/701)) ([032fcb9](https://github.com/standardnotes/syncing-server-js/commit/032fcb938d9f81381dd9879af4bda9254ee8c499))
# [1.80.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.79.1...@standardnotes/syncing-server@1.80.0) (2023-08-18)
### Features

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveSharedVaultLimit1692619430384 implements MigrationInterface {
name = 'RemoveSharedVaultLimit1692619430384'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `shared_vaults` DROP COLUMN `file_upload_bytes_limit`')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `shared_vaults` ADD `file_upload_bytes_limit` int NOT NULL')
}
}

View File

@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveSharedVaultLimit1692619677621 implements MigrationInterface {
name = 'RemoveSharedVaultLimit1692619677621'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "shared_vaults" RENAME TO "temporary_shared_vaults"')
await queryRunner.query(
'CREATE TABLE "shared_vaults" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "file_upload_bytes_used" integer NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
)
await queryRunner.query(
'INSERT INTO "shared_vaults"("uuid", "user_uuid", "file_upload_bytes_used", "created_at_timestamp", "updated_at_timestamp") SELECT "uuid", "user_uuid", "file_upload_bytes_used", "created_at_timestamp", "updated_at_timestamp" FROM "temporary_shared_vaults"',
)
await queryRunner.query('DROP TABLE "temporary_shared_vaults"')
}
public async down(): Promise<void> {
return
}
}

View File

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

View File

@@ -78,8 +78,6 @@ import { SaveItems } from '../Domain/UseCase/Syncing/SaveItems/SaveItems'
import { ItemHashHttpMapper } from '../Mapping/Http/ItemHashHttpMapper'
import { ItemHash } from '../Domain/Item/ItemHash'
import { ItemHashHttpRepresentation } from '../Mapping/Http/ItemHashHttpRepresentation'
import { TypeORMKeySystemAssociation } from '../Infra/TypeORM/TypeORMKeySystemAssociation'
import { TypeORMSharedVaultAssociation } from '../Infra/TypeORM/TypeORMSharedVaultAssociation'
import { BaseSharedVaultInvitesController } from '../Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController'
import { InviteUserToSharedVault } from '../Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault'
import { TypeORMSharedVaultRepository } from '../Infra/TypeORM/TypeORMSharedVaultRepository'
@@ -156,6 +154,8 @@ import { MongoDBItemPersistenceMapper } from '../Mapping/Persistence/MongoDB/Mon
import { Logger } from 'winston'
import { ItemRepositoryResolverInterface } from '../Domain/Item/ItemRepositoryResolverInterface'
import { TypeORMItemRepositoryResolver } from '../Infra/TypeORM/TypeORMItemRepositoryResolver'
import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser'
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -360,12 +360,6 @@ export class ContainerConfigLoader {
container
.bind<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository)
.toDynamicValue(() => appDataSource.getRepository(TypeORMItem))
container
.bind<Repository<TypeORMSharedVaultAssociation>>(TYPES.Sync_ORMSharedVaultAssociationRepository)
.toConstantValue(appDataSource.getRepository(TypeORMSharedVaultAssociation))
container
.bind<Repository<TypeORMKeySystemAssociation>>(TYPES.Sync_ORMKeySystemAssociationRepository)
.toConstantValue(appDataSource.getRepository(TypeORMKeySystemAssociation))
container
.bind<Repository<TypeORMSharedVault>>(TYPES.Sync_ORMSharedVaultRepository)
.toConstantValue(appDataSource.getRepository(TypeORMSharedVault))
@@ -775,6 +769,15 @@ export class ContainerConfigLoader {
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
),
)
container
.bind(TransitionItemsFromPrimaryToSecondaryDatabaseForUser)
.toConstantValue(
new TransitionItemsFromPrimaryToSecondaryDatabaseForUser(
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
container.get<Logger>(TYPES.Sync_Logger),
),
)
// Services
container
@@ -870,6 +873,15 @@ export class ContainerConfigLoader {
container.get<winston.Logger>(TYPES.Sync_Logger),
),
)
container
.bind<SharedVaultFileMovedEventHandler>(TYPES.Sync_SharedVaultFileMovedEventHandler)
.toConstantValue(
new SharedVaultFileMovedEventHandler(
container.get<UpdateStorageQuotaUsedInSharedVault>(TYPES.Sync_UpdateStorageQuotaUsedInSharedVault),
container.get<AddNotificationsForUsers>(TYPES.Sync_AddNotificationsForUsers),
container.get<winston.Logger>(TYPES.Sync_Logger),
),
)
// Services
container.bind<ContentDecoder>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
@@ -900,6 +912,10 @@ export class ContainerConfigLoader {
'SHARED_VAULT_FILE_REMOVED',
container.get<SharedVaultFileRemovedEventHandler>(TYPES.Sync_SharedVaultFileRemovedEventHandler),
],
[
'SHARED_VAULT_FILE_MOVED',
container.get<SharedVaultFileMovedEventHandler>(TYPES.Sync_SharedVaultFileMovedEventHandler),
],
])
if (!isConfiguredForHomeServer) {
container.bind(TYPES.Sync_AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))

View File

@@ -4,8 +4,6 @@ import { Env } from './Env'
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem'
import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
import { TypeORMSharedVaultAssociation } from '../Infra/TypeORM/TypeORMSharedVaultAssociation'
import { TypeORMKeySystemAssociation } from '../Infra/TypeORM/TypeORMKeySystemAssociation'
import { TypeORMSharedVault } from '../Infra/TypeORM/TypeORMSharedVault'
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
import { TypeORMSharedVaultInvite } from '../Infra/TypeORM/TypeORMSharedVaultInvite'
@@ -58,6 +56,7 @@ export class AppDataSource {
password: this.env.get('MONGO_PASSWORD', true),
database: this.env.get('MONGO_DATABASE'),
entities: [MongoDBItem],
retryWrites: false,
synchronize: true,
})
@@ -78,8 +77,6 @@ export class AppDataSource {
entities: [
TypeORMItem,
TypeORMNotification,
TypeORMSharedVaultAssociation,
TypeORMKeySystemAssociation,
TypeORMSharedVault,
TypeORMSharedVaultUser,
TypeORMSharedVaultInvite,

View File

@@ -17,8 +17,6 @@ const TYPES = {
Sync_MessageRepository: Symbol.for('Sync_MessageRepository'),
// ORM
Sync_ORMItemRepository: Symbol.for('Sync_ORMItemRepository'),
Sync_ORMSharedVaultAssociationRepository: Symbol.for('Sync_ORMSharedVaultAssociationRepository'),
Sync_ORMKeySystemAssociationRepository: Symbol.for('Sync_ORMKeySystemAssociationRepository'),
Sync_ORMSharedVaultRepository: Symbol.for('Sync_ORMSharedVaultRepository'),
Sync_ORMSharedVaultInviteRepository: Symbol.for('Sync_ORMSharedVaultInviteRepository'),
Sync_ORMSharedVaultUserRepository: Symbol.for('Sync_ORMSharedVaultUserRepository'),
@@ -82,6 +80,9 @@ const TYPES = {
Sync_DetermineSharedVaultOperationOnItem: Symbol.for('Sync_DetermineSharedVaultOperationOnItem'),
Sync_UpdateStorageQuotaUsedInSharedVault: Symbol.for('Sync_UpdateStorageQuotaUsedInSharedVault'),
Sync_AddNotificationsForUsers: Symbol.for('Sync_AddNotificationsForUsers'),
Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
'Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser',
),
// Handlers
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
@@ -89,6 +90,7 @@ const TYPES = {
Sync_ItemRevisionCreationRequestedEventHandler: Symbol.for('Sync_ItemRevisionCreationRequestedEventHandler'),
Sync_SharedVaultFileRemovedEventHandler: Symbol.for('Sync_SharedVaultFileRemovedEventHandler'),
Sync_SharedVaultFileUploadedEventHandler: Symbol.for('Sync_SharedVaultFileUploadedEventHandler'),
Sync_SharedVaultFileMovedEventHandler: Symbol.for('Sync_SharedVaultFileMovedEventHandler'),
// Services
Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),

View File

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

View File

@@ -110,9 +110,7 @@ describe('Item', () => {
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
keySystemAssociation: KeySystemAssociation.create({
keySystemIdentifier: 'key-system-identifier',
}).getValue(),
keySystemAssociation: KeySystemAssociation.create('key-system-identifier').getValue(),
})
expect(entityOrError.isFailed()).toBeFalsy()
@@ -138,103 +136,117 @@ describe('Item', () => {
expect(entityOrError.getValue().isAssociatedWithKeySystem('key-system-identifier')).toBeFalsy()
})
it('should set shared vault association', () => {
const sharedVaultAssociation = SharedVaultAssociation.create({
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
}).getValue()
it('should tell if an item is identical to another item', () => {
const entity = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
const entity = Item.create({
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
const otherEntity = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
entity.setSharedVaultAssociation(sharedVaultAssociation)
expect(entity.props.sharedVaultAssociation).toEqual(sharedVaultAssociation)
expect(entity.getChanges()).toHaveLength(1)
expect(entity.isIdenticalTo(otherEntity)).toBeTruthy()
})
it('should unset a shared vault association', () => {
const entity = Item.create({
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
}).getValue(),
}).getValue()
it('should tell that an item is not identical to another item', () => {
const entity = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
entity.unsetSharedVaultAssociation()
const otherEntity = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(124)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
expect(entity.props.sharedVaultAssociation).toBeUndefined()
expect(entity.getChanges()).toHaveLength(1)
expect(entity.isIdenticalTo(otherEntity)).toBeFalsy()
})
it('should set key system association', () => {
const keySystemAssociation = KeySystemAssociation.create({
keySystemIdentifier: 'key-system-identifier',
}).getValue()
it('should tell that an item is not identical to another item if their ids are different', () => {
const entity = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
const entity = Item.create({
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
const otherEntity = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(124)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
).getValue()
entity.setKeySystemAssociation(keySystemAssociation)
expect(entity.props.keySystemAssociation).toEqual(keySystemAssociation)
expect(entity.getChanges()).toHaveLength(1)
})
it('should unset a key system association', () => {
const entity = Item.create({
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
keySystemAssociation: KeySystemAssociation.create({
keySystemIdentifier: 'key-system-identifier',
}).getValue(),
}).getValue()
entity.unsetKeySystemAssociation()
expect(entity.props.keySystemAssociation).toBeUndefined()
expect(entity.getChanges()).toHaveLength(1)
expect(entity.isIdenticalTo(otherEntity)).toBeFalsy()
})
})

View File

@@ -1,8 +1,6 @@
import { Aggregate, Change, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { Aggregate, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { ItemProps } from './ItemProps'
import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation'
import { KeySystemAssociation } from '../KeySystem/KeySystemAssociation'
export class Item extends Aggregate<ItemProps> {
private constructor(props: ItemProps, id?: UniqueEntityId) {
@@ -55,57 +53,17 @@ export class Item extends Aggregate<ItemProps> {
return this.props.keySystemAssociation.props.keySystemIdentifier === keySystemIdentifier
}
setSharedVaultAssociation(sharedVaultAssociation: SharedVaultAssociation): void {
this.addChange(
Change.create({
aggregateRootUuid: this.uuid.value,
changeType: this.props.sharedVaultAssociation ? Change.TYPES.Modify : Change.TYPES.Add,
changeData: sharedVaultAssociation,
}).getValue(),
)
this.props.sharedVaultAssociation = sharedVaultAssociation
}
unsetSharedVaultAssociation(): void {
if (!this.props.sharedVaultAssociation) {
return
isIdenticalTo(item: Item): boolean {
if (this._id.toString() !== item._id.toString()) {
return false
}
this.addChange(
Change.create({
aggregateRootUuid: this.uuid.value,
changeType: Change.TYPES.Remove,
changeData: this.props.sharedVaultAssociation,
}).getValue(),
)
this.props.sharedVaultAssociation = undefined
}
const stringifiedThis = JSON.stringify(this.props)
const stringifiedItem = JSON.stringify(item.props)
setKeySystemAssociation(keySystemAssociation: KeySystemAssociation): void {
this.addChange(
Change.create({
aggregateRootUuid: this.uuid.value,
changeType: this.props.keySystemAssociation ? Change.TYPES.Modify : Change.TYPES.Add,
changeData: keySystemAssociation,
}).getValue(),
)
const base64This = Buffer.from(stringifiedThis).toString('base64')
const base64Item = Buffer.from(stringifiedItem).toString('base64')
this.props.keySystemAssociation = keySystemAssociation
}
unsetKeySystemAssociation(): void {
if (!this.props.keySystemAssociation) {
return
}
this.addChange(
Change.create({
aggregateRootUuid: this.uuid.value,
changeType: Change.TYPES.Remove,
changeData: this.props.keySystemAssociation,
}).getValue(),
)
this.props.keySystemAssociation = undefined
return base64This === base64Item
}
}

View File

@@ -1,12 +1,15 @@
import { KeySystemAssociation } from './KeySystemAssociation'
describe('KeySystemAssociation', () => {
it('should create an entity', () => {
const entityOrError = KeySystemAssociation.create({
keySystemIdentifier: '00000000-0000-0000-0000-000000000000',
})
it('should create a value object', () => {
const entityOrError = KeySystemAssociation.create('00000000-0000-0000-0000-000000000000')
expect(entityOrError.isFailed()).toBeFalsy()
expect(entityOrError.getValue().id).not.toBeNull()
})
it('should fail to create a value object with an empty key system identifier', () => {
const entityOrError = KeySystemAssociation.create('')
expect(entityOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -1,13 +1,18 @@
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
import { Result, Validator, ValueObject } from '@standardnotes/domain-core'
import { KeySystemAssociationProps } from './KeySystemAssocationProps'
export class KeySystemAssociation extends Entity<KeySystemAssociationProps> {
private constructor(props: KeySystemAssociationProps, id?: UniqueEntityId) {
super(props, id)
export class KeySystemAssociation extends ValueObject<KeySystemAssociationProps> {
private constructor(props: KeySystemAssociationProps) {
super(props)
}
static create(props: KeySystemAssociationProps, id?: UniqueEntityId): Result<KeySystemAssociation> {
return Result.ok<KeySystemAssociation>(new KeySystemAssociation(props, id))
static create(keySystemIdentifier: string): Result<KeySystemAssociation> {
const validationResult = Validator.isNotEmptyString(keySystemIdentifier)
if (validationResult.isFailed()) {
return Result.fail<KeySystemAssociation>(validationResult.getError())
}
return Result.ok<KeySystemAssociation>(new KeySystemAssociation({ keySystemIdentifier }))
}
}

View File

@@ -5,7 +5,6 @@ import { SharedVault } from './SharedVault'
describe('SharedVault', () => {
it('should create an entity', () => {
const entityOrError = SharedVault.create({
fileUploadBytesLimit: 1_000_000,
fileUploadBytesUsed: 0,
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
@@ -19,7 +18,6 @@ describe('SharedVault', () => {
it('should throw an error if id cannot be cast to uuid', () => {
const entityOrError = SharedVault.create(
{
fileUploadBytesLimit: 1_000_000,
fileUploadBytesUsed: 0,
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),

View File

@@ -3,13 +3,12 @@ import { Uuid } from '@standardnotes/domain-core'
import { SharedVaultAssociation } from './SharedVaultAssociation'
describe('SharedVaultAssociation', () => {
it('should create an entity', () => {
it('should create a value object', () => {
const entityOrError = SharedVaultAssociation.create({
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
})
expect(entityOrError.isFailed()).toBeFalsy()
expect(entityOrError.getValue().id).not.toBeNull()
})
})

View File

@@ -1,13 +1,13 @@
import { Aggregate, Result, UniqueEntityId } from '@standardnotes/domain-core'
import { Result, ValueObject } from '@standardnotes/domain-core'
import { SharedVaultAssociationProps } from './SharedVaultAssociationProps'
export class SharedVaultAssociation extends Aggregate<SharedVaultAssociationProps> {
private constructor(props: SharedVaultAssociationProps, id?: UniqueEntityId) {
super(props, id)
export class SharedVaultAssociation extends ValueObject<SharedVaultAssociationProps> {
private constructor(props: SharedVaultAssociationProps) {
super(props)
}
static create(props: SharedVaultAssociationProps, id?: UniqueEntityId): Result<SharedVaultAssociation> {
return Result.ok<SharedVaultAssociation>(new SharedVaultAssociation(props, id))
static create(props: SharedVaultAssociationProps): Result<SharedVaultAssociation> {
return Result.ok<SharedVaultAssociation>(new SharedVaultAssociation(props))
}
}

View File

@@ -3,6 +3,5 @@ import { Uuid, Timestamps } from '@standardnotes/domain-core'
export interface SharedVaultProps {
userUuid: Uuid
fileUploadBytesUsed: number
fileUploadBytesLimit: number
timestamps: Timestamps
}

View File

@@ -7,16 +7,15 @@ import {
Uuid,
Validator,
} from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { CreateSharedVaultResult } from './CreateSharedVaultResult'
import { CreateSharedVaultDTO } from './CreateSharedVaultDTO'
import { TimerInterface } from '@standardnotes/time'
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
import { AddUserToSharedVault } from '../AddUserToSharedVault/AddUserToSharedVault'
import { SharedVault } from '../../../SharedVault/SharedVault'
export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResult> {
private readonly FILE_UPLOAD_BYTES_LIMIT = 1_000_000_000
constructor(
private addUserToSharedVault: AddUserToSharedVault,
private sharedVaultRepository: SharedVaultRepositoryInterface,
@@ -49,7 +48,6 @@ export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResu
).getValue()
const sharedVaultOrError = SharedVault.create({
fileUploadBytesLimit: this.FILE_UPLOAD_BYTES_LIMIT,
fileUploadBytesUsed: 0,
userUuid,
timestamps,

View File

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

View File

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

View File

@@ -3,10 +3,12 @@ import { SharedVaultMoveType, ValetTokenOperation } from '@standardnotes/securit
export interface CreateSharedVaultFileValetTokenDTO {
userUuid: string
sharedVaultUuid: string
sharedVaultOwnerUploadBytesLimit?: number
fileUuid?: string
remoteIdentifier: string
operation: ValetTokenOperation
unencryptedFileSize?: number
moveOperationType?: SharedVaultMoveType
sharedVaultToSharedVaultMoveTargetUuid?: string
sharedVaultToSharedVaultMoveTargetOwnerUuid?: string
}

View File

@@ -26,7 +26,6 @@ describe('DeleteSharedVault', () => {
beforeEach(() => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
@@ -110,7 +109,6 @@ describe('DeleteSharedVault', () => {
it('should return error when originator of the delete request is not the owner of the shared vault', async () => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),

View File

@@ -15,7 +15,6 @@ describe('GetSharedVaultUsers', () => {
beforeEach(() => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
@@ -61,7 +60,6 @@ describe('GetSharedVaultUsers', () => {
it('returns error when originator is not the owner of the shared vault', async () => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),

View File

@@ -26,7 +26,6 @@ describe('GetSharedVaults', () => {
sharedVault = SharedVault.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
fileUploadBytesLimit: 123,
fileUploadBytesUsed: 123,
}).getValue()
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>

View File

@@ -22,7 +22,6 @@ describe('InviteUserToSharedVault', () => {
beforeEach(() => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
@@ -179,7 +178,6 @@ describe('InviteUserToSharedVault', () => {
const useCase = createUseCase()
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('10000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),

View File

@@ -19,7 +19,6 @@ describe('RemoveUserFromSharedVault', () => {
beforeEach(() => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
@@ -84,7 +83,6 @@ describe('RemoveUserFromSharedVault', () => {
it('should return error when user is not owner of shared vault', async () => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
@@ -104,7 +102,6 @@ describe('RemoveUserFromSharedVault', () => {
it('should remove shared vault user if user is owner and is being force removed', async () => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),

View File

@@ -11,7 +11,6 @@ describe('UpdateStorageQuotaUsedInSharedVault', () => {
beforeEach(() => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),

View File

@@ -123,7 +123,7 @@ export class SaveNewItem implements UseCaseInterface<Item> {
if (sharedVaultAssociationOrError.isFailed()) {
return Result.fail(sharedVaultAssociationOrError.getError())
}
newItem.setSharedVaultAssociation(sharedVaultAssociationOrError.getValue())
newItem.props.sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
}
if (dto.itemHash.hasDedicatedKeySystemAssociation()) {
@@ -133,13 +133,11 @@ export class SaveNewItem implements UseCaseInterface<Item> {
}
const keySystemIdentifier = dto.itemHash.props.key_system_identifier as string
const keySystemAssociationOrError = KeySystemAssociation.create({
keySystemIdentifier,
})
const keySystemAssociationOrError = KeySystemAssociation.create(keySystemIdentifier)
if (keySystemAssociationOrError.isFailed()) {
return Result.fail(keySystemAssociationOrError.getError())
}
newItem.setKeySystemAssociation(keySystemAssociationOrError.getValue())
newItem.props.keySystemAssociation = keySystemAssociationOrError.getValue()
}
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)

Some files were not shown because too many files have changed in this diff Show More