Compare commits

..

11 Commits

Author SHA1 Message Date
standardci
f4816e6c9a chore(release): publish new version
- @standardnotes/auth-server@1.135.1
 - @standardnotes/home-server@1.15.3
 - @standardnotes/syncing-server@1.84.2
2023-08-25 13:41:14 +00:00
Karol Sójko
152a5cbd27 fix(syncing-server): items sorting in MongoDB (#710) 2023-08-25 15:01:17 +02:00
Karol Sójko
1488763115 fix(syncing-server): logs severity on creating duplicates 2023-08-25 12:24:43 +02:00
Karol Sójko
bbb35d16fc fix(auth): account enumeration with pseudo u2f and mfa (#709) 2023-08-25 12:05:16 +02:00
standardci
ef07045ee9 chore(release): publish new version
- @standardnotes/home-server@1.15.2
 - @standardnotes/syncing-server@1.84.1
2023-08-25 09:16:28 +00:00
Karol Sójko
3ba673b424 fix(syncing-server): handling mixed values of deleted flag in MongoDB (#708) 2023-08-25 10:45:14 +02:00
standardci
9c4032ebea chore(release): publish new version
- @standardnotes/analytics@2.25.17
 - @standardnotes/api-gateway@1.72.0
 - @standardnotes/auth-server@1.135.0
 - @standardnotes/domain-events-infra@1.12.14
 - @standardnotes/domain-events@2.117.0
 - @standardnotes/event-store@1.11.23
 - @standardnotes/files-server@1.22.1
 - @standardnotes/home-server@1.15.1
 - @standardnotes/revisions-server@1.26.11
 - @standardnotes/scheduler-server@1.20.27
 - @standardnotes/security@1.12.0
 - @standardnotes/syncing-server@1.84.0
 - @standardnotes/time@1.15.0
 - @standardnotes/websockets-server@1.10.21
2023-08-24 13:57:16 +00:00
Karol Sójko
05bb12c978 feat: add trigerring items transition and checking status of it (#707) 2023-08-24 14:39:33 +02:00
Karol Sójko
df957f07e3 fix: missing topic subscription on localstack 2023-08-23 09:11:30 +02:00
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
122 changed files with 1724 additions and 261 deletions

View File

@@ -147,10 +147,16 @@ LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $SYNCING_SERVER_
echo "linking done:"
echo "$LINKING_RESULT"
echo "linking topic $SYNCING_SERVER_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
echo "linking topic $FILES_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic $FILES_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
echo "linking done:"
echo "$LINKING_RESULT"
echo "linking topic $SYNCING_SERVER_TOPIC_ARN to queue $AUTH_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $AUTH_QUEUE_ARN)
echo "linking done:"
echo "$LINKING_RESULT"
echo "linking topic $AUTH_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic $AUTH_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
echo "linking done:"

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.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.16...@standardnotes/analytics@2.25.17) (2023-08-24)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.15...@standardnotes/analytics@2.25.16) (2023-08-23)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.14...@standardnotes/analytics@2.25.15) (2023-08-22)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.25.15",
"version": "2.25.17",
"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.72.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.1...@standardnotes/api-gateway@1.72.0) (2023-08-24)
### Features
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/api-gateway/issues/707)) ([05bb12c](https://github.com/standardnotes/api-gateway/commit/05bb12c97899824f06e6d01d105dec75fc328440))
## [1.71.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.0...@standardnotes/api-gateway@1.71.1) (2023-08-23)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
### Features

View File

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

View File

@@ -39,7 +39,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
}
if (crossServiceToken === null) {
if (this.crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken)) {
const authResponse = await this.serviceProxy.validateSession({
authorization: authHeaderValue,
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
@@ -55,12 +55,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
response.locals.authToken = crossServiceToken
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
const decodedToken = <CrossServiceTokenData>(
verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
)
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({
key: cacheKey,
encodedCrossServiceToken: crossServiceToken,
encodedCrossServiceToken: response.locals.authToken,
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
userUuid: decodedToken.user.uuid,
})
@@ -126,4 +128,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration)
}
private crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken: string | null) {
if (crossServiceToken === null) {
return true
}
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
return decodedToken.ongoing_transition === true
}
}

View File

@@ -34,6 +34,16 @@ export class ItemsController extends BaseHttpController {
)
}
@httpPost('/transition')
async transition(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callSyncingServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'items/transition'),
request.body,
)
}
@httpGet('/:uuid')
async getItem(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callSyncingServer(

View File

@@ -80,6 +80,15 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/transition-status', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getTransitionStatus(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/transition-status'),
)
}
@httpGet('/:userId/params', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getKeyParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(

View File

@@ -42,7 +42,8 @@ export class EndpointResolver implements EndpointResolverInterface {
// Users Controller
['[PATCH]:users/:userId', 'auth.users.update'],
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
['[PUT]:auth/params', 'auth.users.getKeyParams'],
['[GET]:users/params', 'auth.users.getKeyParams'],
['[GET]:users/transition-status', 'auth.users.transition-status'],
['[DELETE]:users/:userUuid', 'auth.users.delete'],
['[POST]:listed', 'auth.users.createListedAccount'],
['[POST]:auth', 'auth.users.register'],
@@ -58,6 +59,7 @@ export class EndpointResolver implements EndpointResolverInterface {
// Syncing Server
['[POST]:items/sync', 'sync.items.sync'],
['[POST]:items/check-integrity', 'sync.items.check_integrity'],
['[POST]:items/transition', 'sync.items.transition'],
['[GET]:items/:uuid', 'sync.items.get_item'],
// Revisions Controller V2
['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'],

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.135.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.0...@standardnotes/auth-server@1.135.1) (2023-08-25)
### Bug Fixes
* **auth:** account enumeration with pseudo u2f and mfa ([#709](https://github.com/standardnotes/server/issues/709)) ([bbb35d1](https://github.com/standardnotes/server/commit/bbb35d16fc4f6a57fe774a648fbda13ec64a8865))
# [1.135.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.134.0...@standardnotes/auth-server@1.135.0) (2023-08-24)
### Features
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/server/issues/707)) ([05bb12c](https://github.com/standardnotes/server/commit/05bb12c97899824f06e6d01d105dec75fc328440))
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.133.0...@standardnotes/auth-server@1.134.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.132.0...@standardnotes/auth-server@1.133.0) (2023-08-22)
### Features

View File

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

View File

@@ -256,6 +256,13 @@ 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'
import { TransitionStatusRepositoryInterface } from '../Domain/Transition/TransitionStatusRepositoryInterface'
import { RedisTransitionStatusRepository } from '../Infra/Redis/RedisTransitionStatusRepository'
import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTransitionStatusRepository'
import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
import { GetTransitionStatus } from '../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
export class ContainerConfigLoader {
async load(configuration?: {
@@ -609,6 +616,9 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Timer),
),
)
container
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
.toConstantValue(new InMemoryTransitionStatusRepository())
} else {
container.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository).to(RedisPKCERepository)
container.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository).to(LockRepository)
@@ -621,6 +631,9 @@ export class ContainerConfigLoader {
container
.bind<SubscriptionTokenRepositoryInterface>(TYPES.Auth_SubscriptionTokenRepository)
.to(RedisSubscriptionTokenRepository)
container
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
.toConstantValue(new RedisTransitionStatusRepository(container.get<Redis>(TYPES.Auth_Redis)))
}
// Services
@@ -897,6 +910,22 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_SubscriptionSettingService),
),
)
container
.bind<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus)
.toConstantValue(
new UpdateTransitionStatus(
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
),
)
container
.bind<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus)
.toConstantValue(
new GetTransitionStatus(
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
),
)
// Controller
container
@@ -982,6 +1011,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(
@@ -1030,6 +1067,14 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Logger),
),
)
container
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Auth_TransitionStatusUpdatedEventHandler)
.toConstantValue(
new TransitionStatusUpdatedEventHandler(
container.get<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
@@ -1045,6 +1090,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)],
@@ -1060,6 +1106,7 @@ export class ContainerConfigLoader {
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.Auth_PredicateVerificationRequestedEventHandler)],
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)],
['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)],
['TRANSITION_STATUS_UPDATED', container.get(TYPES.Auth_TransitionStatusUpdatedEventHandler)],
])
if (isConfiguredForHomeServer) {
@@ -1164,14 +1211,15 @@ export class ContainerConfigLoader {
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
.toConstantValue(
new BaseUsersController(
container.get(TYPES.Auth_UpdateUser),
container.get(TYPES.Auth_GetUserKeyParams),
container.get(TYPES.Auth_DeleteAccount),
container.get(TYPES.Auth_GetUserSubscription),
container.get(TYPES.Auth_ClearLoginAttempts),
container.get(TYPES.Auth_IncreaseLoginAttempts),
container.get(TYPES.Auth_ChangeCredentials),
container.get(TYPES.Auth_ControllerContainer),
container.get<UpdateUser>(TYPES.Auth_UpdateUser),
container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription),
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
container.get<ChangeCredentials>(TYPES.Auth_ChangeCredentials),
container.get<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus),
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
),
)
container

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

@@ -35,6 +35,7 @@ const TYPES = {
Auth_AuthenticatorRepository: Symbol.for('Auth_AuthenticatorRepository'),
Auth_AuthenticatorChallengeRepository: Symbol.for('Auth_AuthenticatorChallengeRepository'),
Auth_CacheEntryRepository: Symbol.for('Auth_CacheEntryRepository'),
Auth_TransitionStatusRepository: Symbol.for('Auth_TransitionStatusRepository'),
// ORM
Auth_ORMOfflineSettingRepository: Symbol.for('Auth_ORMOfflineSettingRepository'),
Auth_ORMOfflineUserSubscriptionRepository: Symbol.for('Auth_ORMOfflineUserSubscriptionRepository'),
@@ -154,6 +155,8 @@ const TYPES = {
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'),
// Handlers
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
@@ -168,6 +171,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'),
@@ -181,6 +185,7 @@ const TYPES = {
Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'),
Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
Auth_TransitionStatusUpdatedEventHandler: Symbol.for('Auth_TransitionStatusUpdatedEventHandler'),
// Services
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
Auth_SessionService: Symbol.for('Auth_SessionService'),

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

@@ -60,7 +60,7 @@ describe('SubscriptionExpiredEventHandler', () => {
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.removeUserRole = jest.fn()
roleService.removeUserRoleBasedOnSubscription = jest.fn()
timestamp = dayjs.utc().valueOf()
@@ -86,7 +86,7 @@ describe('SubscriptionExpiredEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
})
it('should update subscription ends at', async () => {
@@ -108,7 +108,7 @@ describe('SubscriptionExpiredEventHandler', () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
})
@@ -117,7 +117,7 @@ describe('SubscriptionExpiredEventHandler', () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
})
})

View File

@@ -48,7 +48,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
await this.roleService.removeUserRole(await userSubscription.user, subscriptionName)
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
}
}

View File

@@ -72,7 +72,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
roleService.setOfflineUserRole = jest.fn()
subscriptionExpiresAt = timestamp + 365 * 1000
@@ -106,7 +106,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
})
it('should update user default settings', async () => {
@@ -162,7 +162,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -171,7 +171,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
})

View File

@@ -70,7 +70,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
}
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
await this.roleService.addUserRole(user, subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}
private async createSubscription(

View File

@@ -62,7 +62,7 @@ describe('SubscriptionReassignedEventHandler', () => {
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
subscriptionExpiresAt = timestamp + 365 * 1000
@@ -100,7 +100,7 @@ describe('SubscriptionReassignedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
})
it('should create subscription', async () => {
@@ -146,7 +146,7 @@ describe('SubscriptionReassignedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -155,7 +155,7 @@ describe('SubscriptionReassignedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
})

View File

@@ -67,7 +67,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
}
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
await this.roleService.addUserRole(user, subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}
private async createSubscription(

View File

@@ -61,7 +61,7 @@ describe('SubscriptionRefundedEventHandler', () => {
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.removeUserRole = jest.fn()
roleService.removeUserRoleBasedOnSubscription = jest.fn()
timestamp = dayjs.utc().valueOf()
@@ -87,7 +87,7 @@ describe('SubscriptionRefundedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
})
it('should update subscription ends at', async () => {
@@ -109,7 +109,7 @@ describe('SubscriptionRefundedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
})
@@ -118,7 +118,7 @@ describe('SubscriptionRefundedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
})
})

View File

@@ -48,7 +48,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
await this.roleService.removeUserRole(await userSubscription.user, subscriptionName)
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
}
}

View File

@@ -67,7 +67,7 @@ describe('SubscriptionRenewedEventHandler', () => {
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
roleService.setOfflineUserRole = jest.fn()
timestamp = dayjs.utc().valueOf()
@@ -107,7 +107,7 @@ describe('SubscriptionRenewedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
})
it('should update the offline user role', async () => {
@@ -123,7 +123,7 @@ describe('SubscriptionRenewedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -132,7 +132,7 @@ describe('SubscriptionRenewedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -143,7 +143,7 @@ describe('SubscriptionRenewedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
})

View File

@@ -73,7 +73,7 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
for (const userSubscription of userSubscriptions) {
const user = await userSubscription.user
await this.roleService.addUserRole(user, subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}
}

View File

@@ -88,7 +88,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
})
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
roleService.setOfflineUserRole = jest.fn()
subscriptionExpiresAt = timestamp + 365 * 1000
@@ -121,7 +121,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
})
it('should update user default settings', async () => {
@@ -243,7 +243,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -252,7 +252,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
})

View File

@@ -93,7 +93,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
event.payload.timestamp,
)
await this.roleService.addUserRole(user, event.payload.subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)

View File

@@ -0,0 +1,18 @@
import { DomainEventHandlerInterface, TransitionStatusUpdatedEvent } from '@standardnotes/domain-events'
import { UpdateTransitionStatus } from '../UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
import { Logger } from 'winston'
export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface {
constructor(private updateTransitionStatusUseCase: UpdateTransitionStatus, private logger: Logger) {}
async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
const result = await this.updateTransitionStatusUseCase.execute({
status: event.payload.status,
userUuid: event.payload.userUuid,
})
if (result.isFailed()) {
this.logger.error(`Failed to update transition status for user ${event.payload.userUuid}`)
}
}
}

View File

@@ -5,7 +5,7 @@ import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
import { SubscriptionName } from '@standardnotes/common'
import { RoleName } from '@standardnotes/domain-core'
import { RoleName, Uuid } from '@standardnotes/domain-core'
import { Role } from '../Role/Role'
import { ClientServiceInterface } from '../Client/ClientServiceInterface'
@@ -81,9 +81,44 @@ describe('RoleService', () => {
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
logger.warn = jest.fn()
logger.error = jest.fn()
})
describe('adding roles', () => {
beforeEach(() => {
user = {
uuid: '123',
email: 'test@test.com',
roles: Promise.resolve([basicRole]),
} as jest.Mocked<User>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
userRepository.save = jest.fn().mockReturnValue(user)
})
it('should add a role to a user', async () => {
await createService().addRoleToUser(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
RoleName.create(RoleName.NAMES.ProUser).getValue(),
)
user.roles = Promise.resolve([basicRole, proRole])
expect(userRepository.save).toHaveBeenCalledWith(user)
})
it('should not add a role to a user if the user could not be found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
await createService().addRoleToUser(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
RoleName.create(RoleName.NAMES.ProUser).getValue(),
)
expect(userRepository.save).not.toHaveBeenCalled()
})
})
describe('adding roles based on subscription', () => {
beforeEach(() => {
user = {
uuid: '123',
@@ -96,7 +131,7 @@ describe('RoleService', () => {
})
it('should add role to user', async () => {
await createService().addUserRole(user, SubscriptionName.ProPlan)
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
user.roles = Promise.resolve([basicRole, proRole])
@@ -112,7 +147,7 @@ describe('RoleService', () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
await createService().addUserRole(user, SubscriptionName.ProPlan)
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
expect(userRepository.save).toHaveBeenCalledWith(user)
@@ -120,7 +155,7 @@ describe('RoleService', () => {
})
it('should send websockets event', async () => {
await createService().addUserRole(user, SubscriptionName.ProPlan)
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user)
})
@@ -128,14 +163,14 @@ describe('RoleService', () => {
it('should not add role if no role name exists for subscription name', async () => {
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined)
await createService().addUserRole(user, 'test' as SubscriptionName)
await createService().addUserRoleBasedOnSubscription(user, 'test' as SubscriptionName)
expect(userRepository.save).not.toHaveBeenCalled()
})
it('should not add role if no role exists for role name', async () => {
roleRepository.findOneByName = jest.fn().mockReturnValue(null)
await createService().addUserRole(user, SubscriptionName.ProPlan)
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(userRepository.save).not.toHaveBeenCalled()
})
@@ -169,7 +204,7 @@ describe('RoleService', () => {
})
})
describe('removing roles', () => {
describe('removing roles based on subscription', () => {
beforeEach(() => {
user = {
uuid: '123',
@@ -182,13 +217,13 @@ describe('RoleService', () => {
})
it('should remove role from user', async () => {
await createService().removeUserRole(user, SubscriptionName.ProPlan)
await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(userRepository.save).toHaveBeenCalledWith(user)
})
it('should send websockets event', async () => {
await createService().removeUserRole(user, SubscriptionName.ProPlan)
await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user)
})
@@ -196,7 +231,7 @@ describe('RoleService', () => {
it('should not remove role if role name does not exist for subscription name', async () => {
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined)
await createService().removeUserRole(user, 'test' as SubscriptionName)
await createService().removeUserRoleBasedOnSubscription(user, 'test' as SubscriptionName)
expect(userRepository.save).not.toHaveBeenCalled()
})

View File

@@ -13,7 +13,7 @@ import { RoleToSubscriptionMapInterface } from './RoleToSubscriptionMapInterface
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Role } from './Role'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { Uuid } from '@standardnotes/domain-core'
import { RoleName, Uuid } from '@standardnotes/domain-core'
@injectable()
export class RoleService implements RoleServiceInterface {
@@ -54,7 +54,18 @@ export class RoleService implements RoleServiceInterface {
return false
}
async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
async addRoleToUser(userUuid: Uuid, roleName: RoleName): Promise<void> {
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.error(`Could not find user with uuid ${userUuid.value} to add role ${roleName.value}`)
return
}
await this.addToExistingRoles(user, roleName.value)
}
async addUserRoleBasedOnSubscription(user: User, subscriptionName: SubscriptionName): Promise<void> {
const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
if (roleName === undefined) {
@@ -62,25 +73,7 @@ export class RoleService implements RoleServiceInterface {
return
}
const role = await this.roleRepository.findOneByName(roleName)
if (role === null) {
this.logger.warn(`Could not find role for role name: ${roleName}`)
return
}
const rolesMap = new Map<string, Role>()
const currentRoles = await user.roles
for (const currentRole of currentRoles) {
rolesMap.set(currentRole.name, currentRole)
}
if (!rolesMap.has(role.name)) {
rolesMap.set(role.name, role)
}
user.roles = Promise.resolve([...rolesMap.values()])
await this.userRepository.save(user)
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
await this.addToExistingRoles(user, roleName)
}
async setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void> {
@@ -107,7 +100,7 @@ export class RoleService implements RoleServiceInterface {
await this.offlineUserSubscriptionRepository.save(offlineUserSubscription)
}
async removeUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
async removeUserRoleBasedOnSubscription(user: User, subscriptionName: SubscriptionName): Promise<void> {
const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
if (roleName === undefined) {
@@ -120,4 +113,27 @@ export class RoleService implements RoleServiceInterface {
await this.userRepository.save(user)
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
}
private async addToExistingRoles(user: User, roleNameString: string): Promise<void> {
const role = await this.roleRepository.findOneByName(roleNameString)
if (role === null) {
this.logger.warn(`Could not find role for role name: ${roleNameString}`)
return
}
const rolesMap = new Map<string, Role>()
const currentRoles = await user.roles
for (const currentRole of currentRoles) {
rolesMap.set(currentRole.name, currentRole)
}
if (!rolesMap.has(role.name)) {
rolesMap.set(role.name, role)
}
user.roles = Promise.resolve([...rolesMap.values()])
await this.userRepository.save(user)
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
}
}

View File

@@ -1,10 +1,12 @@
import { PermissionName } from '@standardnotes/features'
import { RoleName, Uuid } from '@standardnotes/domain-core'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { User } from '../User/User'
export interface RoleServiceInterface {
addUserRole(user: User, subscriptionName: string): Promise<void>
addRoleToUser(userUuid: Uuid, roleName: RoleName): Promise<void>
addUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void>
removeUserRole(user: User, subscriptionName: string): Promise<void>
removeUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
userHasPermission(userUuid: string, permissionName: PermissionName): Promise<boolean>
}

View File

@@ -0,0 +1,5 @@
export interface TransitionStatusRepositoryInterface {
updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void>
removeStatus(userUuid: string): Promise<void>
getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null>
}

View File

@@ -69,7 +69,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
userSubscriptionRepository.save = jest.fn().mockReturnValue(inviteeSubscription)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
@@ -103,7 +103,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
updatedAt: 1,
user: Promise.resolve(invitee),
})
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
inviteeSubscription,
)
@@ -143,7 +143,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
updatedAt: 3,
user: Promise.resolve(invitee),
})
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
inviteeSubscription,
)
@@ -162,7 +162,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
@@ -180,7 +180,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
@@ -202,7 +202,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
@@ -219,7 +219,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
@@ -244,7 +244,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
})

View File

@@ -100,7 +100,7 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
}
private async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
await this.roleService.addUserRole(user, subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}
private async createSharedSubscription(

View File

@@ -34,7 +34,7 @@ describe('ActivatePremiumFeatures', () => {
userSubscriptionRepository.save = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
@@ -73,7 +73,7 @@ describe('ActivatePremiumFeatures', () => {
expect(result.isFailed()).toBe(false)
expect(userSubscriptionRepository.save).toHaveBeenCalled()
expect(roleService.addUserRole).toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalled()
})
it('should save a subscription with custom plan name and endsAt', async () => {

View File

@@ -53,11 +53,11 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
await this.userSubscriptionRepository.save(subscription)
await this.roleService.addUserRole(user, subscriptionPlanName.value)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionPlanName.value)
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

@@ -84,7 +84,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
userSubscriptionRepository.save = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.removeUserRole = jest.fn()
roleService.removeUserRoleBasedOnSubscription = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
@@ -122,7 +122,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
endsAt: 1,
user: Promise.resolve(invitee),
})
expect(roleService.removeUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createSharedSubscriptionInvitationCanceledEvent).toHaveBeenCalledWith({
inviteeIdentifier: '123',
@@ -156,7 +156,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviteeIdentifierType: 'email',
})
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.removeUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
})
it('should not cancel a shared subscription invitation if it is not found', async () => {
@@ -204,7 +204,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviteeIdentifierType: 'email',
})
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
})
it('should not cancel a shared subscription invitation if inviter subscription is not found', async () => {

View File

@@ -90,7 +90,10 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
if (invitee !== null) {
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
await this.roleService.removeUserRoleBasedOnSubscription(
invitee,
inviterUserSubscription.planName as SubscriptionName,
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({

View File

@@ -10,6 +10,7 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
import { GetSetting } from '../GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
describe('CreateCrossServiceToken', () => {
let userProjector: ProjectorInterface<User>
@@ -18,6 +19,7 @@ describe('CreateCrossServiceToken', () => {
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
let userRepository: UserRepositoryInterface
let getSettingUseCase: GetSetting
let transitionStatusRepository: TransitionStatusRepositoryInterface
const jwtTTL = 60
let session: Session
@@ -33,6 +35,7 @@ describe('CreateCrossServiceToken', () => {
userRepository,
jwtTTL,
getSettingUseCase,
transitionStatusRepository,
)
beforeEach(() => {
@@ -64,6 +67,9 @@ describe('CreateCrossServiceToken', () => {
getSettingUseCase = {} as jest.Mocked<GetSetting>
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } }))
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('TO-DO')
})
it('should create a cross service token for user', async () => {
@@ -87,6 +93,36 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
},
60,
)
})
it('should create a cross service token for user that has an ongoing transaction', async () => {
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
await createUseCase().execute({
user,
session,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: true,
},
60,
)
@@ -109,6 +145,7 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
},
60,
)
@@ -131,6 +168,7 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
},
60,
)
@@ -180,6 +218,7 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
},
60,
)

View File

@@ -12,6 +12,7 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
import { GetSetting } from '../GetSetting/GetSetting'
import { SettingName } from '@standardnotes/settings'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
@injectable()
export class CreateCrossServiceToken implements UseCaseInterface<string> {
@@ -24,6 +25,8 @@ export class CreateCrossServiceToken implements UseCaseInterface<string> {
@inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
@inject(TYPES.Auth_GetSetting)
private getSettingUseCase: GetSetting,
@inject(TYPES.Auth_TransitionStatusRepository)
private transitionStatusRepository: TransitionStatusRepositoryInterface,
) {}
async execute(dto: CreateCrossServiceTokenDTO): Promise<Result<string>> {
@@ -42,12 +45,15 @@ export class CreateCrossServiceToken implements UseCaseInterface<string> {
return Result.fail(`Could not find user with uuid ${dto.userUuid}`)
}
const transitionStatus = await this.transitionStatusRepository.getStatus(user.uuid)
const roles = await user.roles
const authTokenData: CrossServiceTokenData = {
user: this.projectUser(user),
roles: this.projectRoles(roles),
shared_vault_owner_context: undefined,
ongoing_transition: transitionStatus === 'STARTED',
}
if (dto.sharedVaultOwnerContext !== undefined) {

View File

@@ -0,0 +1,108 @@
import { RoleName } from '@standardnotes/domain-core'
import { Role } from '../../Role/Role'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GetTransitionStatus } from './GetTransitionStatus'
describe('GetTransitionStatus', () => {
let transitionStatusRepository: TransitionStatusRepositoryInterface
let userRepository: UserRepositoryInterface
let user: User
let role: Role
const createUseCase = () => new GetTransitionStatus(transitionStatusRepository, userRepository)
beforeEach(() => {
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.getStatus = jest.fn().mockReturnValue(null)
role = {} as jest.Mocked<Role>
role.name = RoleName.NAMES.CoreUser
user = {
uuid: '00000000-0000-0000-0000-000000000000',
email: 'test@test.te',
} as jest.Mocked<User>
user.roles = Promise.resolve([role])
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
})
it('returns transition status FINISHED', async () => {
role.name = RoleName.NAMES.TransitionUser
user.roles = Promise.resolve([role])
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('FINISHED')
})
it('returns transition status STARTED', async () => {
const useCase = createUseCase()
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('STARTED')
})
it('returns transition status TO-DO', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('TO-DO')
})
it('returns transition status FAILED', async () => {
const useCase = createUseCase()
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('FAILED')
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('FAILED')
})
it('return error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
})
it('return error if user not found', async () => {
const useCase = createUseCase()
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('User not found.')
})
})

View File

@@ -0,0 +1,39 @@
import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { GetTransitionStatusDTO } from './GetTransitionStatusDTO'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
export class GetTransitionStatus implements UseCaseInterface<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'> {
constructor(
private transitionStatusRepository: TransitionStatusRepositoryInterface,
private userRepository: UserRepositoryInterface,
) {}
async execute(dto: GetTransitionStatusDTO): Promise<Result<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
return Result.fail('User not found.')
}
const roles = await user.roles
for (const role of roles) {
if (role.name === RoleName.NAMES.TransitionUser) {
return Result.ok('FINISHED')
}
}
const transitionStatus = await this.transitionStatusRepository.getStatus(userUuid.value)
if (transitionStatus === null) {
return Result.ok('TO-DO')
}
return Result.ok(transitionStatus)
}
}

View File

@@ -0,0 +1,3 @@
export interface GetTransitionStatusDTO {
userUuid: string
}

View File

@@ -93,7 +93,7 @@ describe('UpdateSetting', () => {
settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(true)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()

View File

@@ -0,0 +1,64 @@
import { RoleName, Uuid } from '@standardnotes/domain-core'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatus } from './UpdateTransitionStatus'
describe('UpdateTransitionStatus', () => {
let transitionStatusRepository: TransitionStatusRepositoryInterface
let roleService: RoleServiceInterface
const createUseCase = () => new UpdateTransitionStatus(transitionStatusRepository, roleService)
beforeEach(() => {
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.removeStatus = jest.fn()
transitionStatusRepository.updateStatus = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addRoleToUser = jest.fn()
})
it('should remove transition status and add TransitionUser role', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'FINISHED',
})
expect(result.isFailed()).toBeFalsy()
expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith('00000000-0000-0000-0000-000000000000')
expect(roleService.addRoleToUser).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
RoleName.create(RoleName.NAMES.TransitionUser).getValue(),
)
})
it('should update transition status', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'STARTED',
})
expect(result.isFailed()).toBeFalsy()
expect(transitionStatusRepository.updateStatus).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000',
'STARTED',
)
})
it('should return error when user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
status: 'STARTED',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
})
})

View File

@@ -0,0 +1,31 @@
import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatusDTO } from './UpdateTransitionStatusDTO'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
export class UpdateTransitionStatus implements UseCaseInterface<void> {
constructor(
private transitionStatusRepository: TransitionStatusRepositoryInterface,
private roleService: RoleServiceInterface,
) {}
async execute(dto: UpdateTransitionStatusDTO): Promise<Result<void>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
if (dto.status === 'FINISHED') {
await this.transitionStatusRepository.removeStatus(dto.userUuid)
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
return Result.ok()
}
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.status)
return Result.ok()
}
}

View File

@@ -0,0 +1,4 @@
export interface UpdateTransitionStatusDTO {
userUuid: string
status: 'STARTED' | 'FINISHED' | 'FAILED'
}

View File

@@ -257,7 +257,7 @@ describe('VerifyMFA', () => {
})
it('should not pass if user is not found and pseudo u2f is required', async () => {
booleanSelector.select = jest.fn().mockReturnValueOnce(false).mockReturnValueOnce(true)
booleanSelector.select = jest.fn().mockReturnValueOnce(true).mockReturnValueOnce(true)
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
expect(

View File

@@ -48,33 +48,33 @@ export class VerifyMFA implements UseCaseInterface {
const user = await this.userRepository.findOneByUsernameOrEmail(username)
if (user == null) {
const mfaSelectorHash = crypto
const secondFactorSelectorHash = crypto
.createHash('sha256')
.update(`mfa-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.digest('hex')
const u2fSelectorHash = crypto
.createHash('sha256')
.update(`u2f-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.update(`second-factor-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.digest('hex')
const isPseudoMFARequired = this.booleanSelector.select(mfaSelectorHash, [true, false])
const isPseudoSecondFactorRequired = this.booleanSelector.select(secondFactorSelectorHash, [true, false])
if (isPseudoSecondFactorRequired) {
const u2fSelectorHash = crypto
.createHash('sha256')
.update(`u2f-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.digest('hex')
const isPseudoU2FRequired = this.booleanSelector.select(u2fSelectorHash, [true, false])
const isPseudoU2FRequired = this.booleanSelector.select(u2fSelectorHash, [true, false])
if (isPseudoMFARequired) {
return {
success: false,
errorTag: ErrorTag.MfaRequired,
errorMessage: 'Please enter your two-factor authentication code.',
errorPayload: { mfa_key: `mfa_${uuidv4()}` },
}
}
if (isPseudoU2FRequired) {
return {
success: false,
errorTag: ErrorTag.U2FRequired,
errorMessage: 'Please authenticate with your U2F device.',
if (isPseudoU2FRequired) {
return {
success: false,
errorTag: ErrorTag.U2FRequired,
errorMessage: 'Please authenticate with your U2F device.',
}
} else {
return {
success: false,
errorTag: ErrorTag.MfaRequired,
errorMessage: 'Please enter your two-factor authentication code.',
errorPayload: { mfa_key: `mfa_${uuidv4()}` },
}
}
}

View File

@@ -0,0 +1,19 @@
import { TransitionStatusRepositoryInterface } from '../../Domain/Transition/TransitionStatusRepositoryInterface'
export class InMemoryTransitionStatusRepository implements TransitionStatusRepositoryInterface {
private statuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
async updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void> {
this.statuses.set(userUuid, status)
}
async removeStatus(userUuid: string): Promise<void> {
this.statuses.delete(userUuid)
}
async getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null> {
const status = this.statuses.get(userUuid) || null
return status
}
}

View File

@@ -14,6 +14,7 @@ import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempt
import { InviteToSharedSubscription } from '../../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
import { UpdateUser } from '../../Domain/UseCase/UpdateUser'
import { User } from '../../Domain/User/User'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
describe('AnnotatedUsersController', () => {
let updateUser: UpdateUser
@@ -24,6 +25,7 @@ describe('AnnotatedUsersController', () => {
let increaseLoginAttempts: IncreaseLoginAttempts
let changeCredentials: ChangeCredentials
let inviteToSharedSubscription: InviteToSharedSubscription
let getTransitionStatus: GetTransitionStatus
let request: express.Request
let response: express.Response
@@ -38,6 +40,7 @@ describe('AnnotatedUsersController', () => {
clearLoginAttempts,
increaseLoginAttempts,
changeCredentials,
getTransitionStatus,
)
beforeEach(() => {
@@ -69,6 +72,9 @@ describe('AnnotatedUsersController', () => {
inviteToSharedSubscription = {} as jest.Mocked<InviteToSharedSubscription>
inviteToSharedSubscription.execute = jest.fn()
getTransitionStatus = {} as jest.Mocked<GetTransitionStatus>
getTransitionStatus.execute = jest.fn()
request = {
headers: {},
body: {},

View File

@@ -18,6 +18,7 @@ import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
import { BaseUsersController } from './Base/BaseUsersController'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
@controller('/users')
export class AnnotatedUsersController extends BaseUsersController {
@@ -29,6 +30,7 @@ export class AnnotatedUsersController extends BaseUsersController {
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
@inject(TYPES.Auth_GetTransitionStatus) override getTransitionStatusUseCase: GetTransitionStatus,
) {
super(
updateUser,
@@ -38,6 +40,7 @@ export class AnnotatedUsersController extends BaseUsersController {
clearLoginAttempts,
increaseLoginAttempts,
changeCredentialsUseCase,
getTransitionStatusUseCase,
)
}
@@ -51,6 +54,11 @@ export class AnnotatedUsersController extends BaseUsersController {
return super.keyParams(request)
}
@httpGet('/transition-status', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async transitionStatus(request: Request, response: Response): Promise<results.JsonResult> {
return super.transitionStatus(request, response)
}
@httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
return super.deleteAccount(request, response)

View File

@@ -10,6 +10,7 @@ import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription
import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
import { ErrorTag } from '@standardnotes/responses'
import { GetTransitionStatus } from '../../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
export class BaseUsersController extends BaseHttpController {
constructor(
@@ -20,6 +21,7 @@ export class BaseUsersController extends BaseHttpController {
protected clearLoginAttempts: ClearLoginAttempts,
protected increaseLoginAttempts: IncreaseLoginAttempts,
protected changeCredentialsUseCase: ChangeCredentials,
protected getTransitionStatusUseCase: GetTransitionStatus,
private controllerContainer?: ControllerContainerInterface,
) {
super()
@@ -30,6 +32,7 @@ export class BaseUsersController extends BaseHttpController {
this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this))
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
this.controllerContainer.register('auth.users.delete', this.deleteAccount.bind(this))
this.controllerContainer.register('auth.users.transition-status', this.transitionStatus.bind(this))
}
}
@@ -103,6 +106,29 @@ export class BaseUsersController extends BaseHttpController {
return this.json(result.keyParams)
}
async transitionStatus(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.getTransitionStatusUseCase.execute({
userUuid: response.locals.user.uuid,
})
if (result.isFailed()) {
return this.json(
{
error: {
message: result.getError(),
},
},
400,
)
}
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
return this.json({
status: result.getValue(),
})
}
async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(

View File

@@ -0,0 +1,23 @@
import * as IORedis from 'ioredis'
import { TransitionStatusRepositoryInterface } from '../../Domain/Transition/TransitionStatusRepositoryInterface'
export class RedisTransitionStatusRepository implements TransitionStatusRepositoryInterface {
private readonly PREFIX = 'transition'
constructor(private redisClient: IORedis.Redis) {}
async updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void> {
await this.redisClient.set(`${this.PREFIX}:${userUuid}`, status)
}
async removeStatus(userUuid: string): Promise<void> {
await this.redisClient.del(`${this.PREFIX}:${userUuid}`)
}
async getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null> {
const status = (await this.redisClient.get(`${this.PREFIX}:${userUuid}`)) as 'STARTED' | 'FAILED' | null
return status
}
}

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.14](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.13...@standardnotes/domain-events-infra@1.12.14) (2023-08-24)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.13](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.12...@standardnotes/domain-events-infra@1.12.13) (2023-08-23)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.11...@standardnotes/domain-events-infra@1.12.12) (2023-08-22)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

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

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.
# [2.117.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.116.0...@standardnotes/domain-events@2.117.0) (2023-08-24)
### Features
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/server/issues/707)) ([05bb12c](https://github.com/standardnotes/server/commit/05bb12c97899824f06e6d01d105dec75fc328440))
# [2.116.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.1...@standardnotes/domain-events@2.116.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
## [2.115.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.0...@standardnotes/domain-events@2.115.1) (2023-08-22)
**Note:** Version bump only for package @standardnotes/domain-events

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.115.1",
"version": "2.117.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

@@ -0,0 +1,8 @@
import { DomainEventInterface } from './DomainEventInterface'
import { TransitionStatusUpdatedEventPayload } from './TransitionStatusUpdatedEventPayload'
export interface TransitionStatusUpdatedEvent extends DomainEventInterface {
type: 'TRANSITION_STATUS_UPDATED'
payload: TransitionStatusUpdatedEventPayload
}

View File

@@ -0,0 +1,4 @@
export interface TransitionStatusUpdatedEventPayload {
userUuid: string
status: 'STARTED' | 'FINISHED' | 'FAILED'
}

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'
@@ -88,6 +90,8 @@ export * from './Event/SubscriptionRevertRequestedEvent'
export * from './Event/SubscriptionRevertRequestedEventPayload'
export * from './Event/SubscriptionSyncRequestedEvent'
export * from './Event/SubscriptionSyncRequestedEventPayload'
export * from './Event/TransitionStatusUpdatedEvent'
export * from './Event/TransitionStatusUpdatedEventPayload'
export * from './Event/UserDisabledSessionUserAgentLoggingEvent'
export * from './Event/UserDisabledSessionUserAgentLoggingEventPayload'
export * from './Event/UserEmailChangedEvent'

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.23](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.22...@standardnotes/event-store@1.11.23) (2023-08-24)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.22](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.21...@standardnotes/event-store@1.11.22) (2023-08-23)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.21](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.20...@standardnotes/event-store@1.11.21) (2023-08-22)
**Note:** Version bump only for package @standardnotes/event-store

View File

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

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.22.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.0...@standardnotes/files-server@1.22.1) (2023-08-24)
**Note:** Version bump only for package @standardnotes/files-server
# [1.22.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.21.0...@standardnotes/files-server@1.22.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/files/issues/705)) ([205a1ed](https://github.com/standardnotes/files/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.21.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.4...@standardnotes/files-server@1.21.0) (2023-08-22)
### Features

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.14.2",
"version": "1.15.3",
"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.11](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.10...@standardnotes/revisions-server@1.26.11) (2023-08-24)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.10](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.9...@standardnotes/revisions-server@1.26.10) (2023-08-23)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.8...@standardnotes/revisions-server@1.26.9) (2023-08-22)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

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

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.20.27](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.26...@standardnotes/scheduler-server@1.20.27) (2023-08-24)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.26](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.25...@standardnotes/scheduler-server@1.20.26) (2023-08-23)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.25](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.24...@standardnotes/scheduler-server@1.20.25) (2023-08-22)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

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

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.12.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.11.0...@standardnotes/security@1.12.0) (2023-08-24)
### Features
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/server/issues/707)) ([05bb12c](https://github.com/standardnotes/server/commit/05bb12c97899824f06e6d01d105dec75fc328440))
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.10.0...@standardnotes/security@1.11.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.9.0...@standardnotes/security@1.10.0) (2023-08-22)
### Features

View File

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

View File

@@ -20,4 +20,5 @@ export type CrossServiceTokenData = {
refresh_expiration: string
}
extensionKey?: string
ongoing_transition?: boolean
}

View File

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

View File

@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.84.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.84.1...@standardnotes/syncing-server@1.84.2) (2023-08-25)
### Bug Fixes
* **syncing-server:** items sorting in MongoDB ([#710](https://github.com/standardnotes/syncing-server-js/issues/710)) ([152a5cb](https://github.com/standardnotes/syncing-server-js/commit/152a5cbd27375adbad8b070d1778b256a6dce1f4))
* **syncing-server:** logs severity on creating duplicates ([1488763](https://github.com/standardnotes/syncing-server-js/commit/14887631153b78117ec7433353bb32709209a617))
## [1.84.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.84.0...@standardnotes/syncing-server@1.84.1) (2023-08-25)
### Bug Fixes
* **syncing-server:** handling mixed values of deleted flag in MongoDB ([#708](https://github.com/standardnotes/syncing-server-js/issues/708)) ([3ba673b](https://github.com/standardnotes/syncing-server-js/commit/3ba673b424ae3bb6b64b2360323d7373636c6cd5))
# [1.84.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.83.0...@standardnotes/syncing-server@1.84.0) (2023-08-24)
### Features
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/syncing-server-js/issues/707)) ([05bb12c](https://github.com/standardnotes/syncing-server-js/commit/05bb12c97899824f06e6d01d105dec75fc328440))
# [1.83.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.82.0...@standardnotes/syncing-server@1.83.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/syncing-server-js/issues/705)) ([205a1ed](https://github.com/standardnotes/syncing-server-js/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.82.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.81.0...@standardnotes/syncing-server@1.82.0) (2023-08-22)
### Features

View File

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

View File

@@ -155,6 +155,9 @@ 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'
import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -769,14 +772,27 @@ export class ContainerConfigLoader {
),
)
container
.bind(TransitionItemsFromPrimaryToSecondaryDatabaseForUser)
.bind<TransitionItemsFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser,
)
.toConstantValue(
new TransitionItemsFromPrimaryToSecondaryDatabaseForUser(
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
container.get<TimerInterface>(TYPES.Sync_Timer),
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<TriggerTransitionFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
)
.toConstantValue(
new TriggerTransitionFromPrimaryToSecondaryDatabaseForUser(
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
),
)
// Services
container
@@ -872,6 +888,27 @@ 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),
),
)
container
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Sync_TransitionStatusUpdatedEventHandler)
.toConstantValue(
new TransitionStatusUpdatedEventHandler(
container.get<TransitionItemsFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser,
),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<Logger>(TYPES.Sync_Logger),
),
)
// Services
container.bind<ContentDecoder>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
@@ -902,6 +939,14 @@ export class ContainerConfigLoader {
'SHARED_VAULT_FILE_REMOVED',
container.get<SharedVaultFileRemovedEventHandler>(TYPES.Sync_SharedVaultFileRemovedEventHandler),
],
[
'SHARED_VAULT_FILE_MOVED',
container.get<SharedVaultFileMovedEventHandler>(TYPES.Sync_SharedVaultFileMovedEventHandler),
],
[
'TRANSITION_STATUS_UPDATED',
container.get<TransitionStatusUpdatedEventHandler>(TYPES.Sync_TransitionStatusUpdatedEventHandler),
],
])
if (!isConfiguredForHomeServer) {
container.bind(TYPES.Sync_AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
@@ -976,12 +1021,15 @@ export class ContainerConfigLoader {
.bind<BaseItemsController>(TYPES.Sync_BaseItemsController)
.toConstantValue(
new BaseItemsController(
container.get(TYPES.Sync_SyncItems),
container.get(TYPES.Sync_CheckIntegrity),
container.get(TYPES.Sync_GetItem),
container.get(TYPES.Sync_ItemHttpMapper),
container.get(TYPES.Sync_SyncResponseFactoryResolver),
container.get(TYPES.Sync_ControllerContainer),
container.get<SyncItems>(TYPES.Sync_SyncItems),
container.get<CheckIntegrity>(TYPES.Sync_CheckIntegrity),
container.get<GetItem>(TYPES.Sync_GetItem),
container.get<TriggerTransitionFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
),
container.get<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper),
container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer),
),
)
container

View File

@@ -83,6 +83,9 @@ const TYPES = {
Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
'Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser',
),
Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
'Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser',
),
// Handlers
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
@@ -90,6 +93,8 @@ 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'),
Sync_TransitionStatusUpdatedEventHandler: Symbol.for('Sync_TransitionStatusUpdatedEventHandler'),
// Services
Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),

View File

@@ -6,6 +6,7 @@ import {
ItemDumpedEvent,
ItemRevisionCreationRequestedEvent,
RevisionsCopyRequestedEvent,
TransitionStatusUpdatedEvent,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
@@ -13,6 +14,24 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(private timer: TimerInterface) {}
createTransitionStatusUpdatedEvent(userUuid: string, status: 'FINISHED' | 'FAILED'): TransitionStatusUpdatedEvent {
return {
type: 'TRANSITION_STATUS_UPDATED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.SyncingServer,
},
payload: {
userUuid,
status,
},
}
}
createRevisionsCopyRequestedEvent(
userUuid: string,
dto: {

View File

@@ -4,9 +4,14 @@ import {
ItemDumpedEvent,
ItemRevisionCreationRequestedEvent,
RevisionsCopyRequestedEvent,
TransitionStatusUpdatedEvent,
} from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(
userUuid: string,
status: 'STARTED' | 'FAILED' | 'FINISHED',
): TransitionStatusUpdatedEvent
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: string

View File

@@ -74,6 +74,7 @@ describe('DuplicateItemSyncedEventHandler', () => {
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
logger.debug = jest.fn()
event = {} as jest.Mocked<DuplicateItemSyncedEvent>
event.createdAt = new Date(1)

View File

@@ -31,13 +31,13 @@ export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterf
const item = await itemRepository.findByUuidAndUserUuid(event.payload.itemUuid, event.payload.userUuid)
if (item === null) {
this.logger.warn(`Could not find item with uuid ${event.payload.itemUuid}`)
this.logger.debug(`Could not find item with uuid ${event.payload.itemUuid}`)
return
}
if (!item.props.duplicateOf) {
this.logger.warn(`Item ${event.payload.itemUuid} does not point to any duplicate`)
this.logger.debug(`Item ${event.payload.itemUuid} does not point to any duplicate`)
return
}

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