diff --git a/packages/auth/bin/backup.ts b/packages/auth/bin/backup.ts index 0387681df..f0cf9790f 100644 --- a/packages/auth/bin/backup.ts +++ b/packages/auth/bin/backup.ts @@ -62,8 +62,8 @@ const requestBackups = async ( muteEmailsSettingName, setting.setting_user_uuid, ) - if (emailsMutedSetting !== null && emailsMutedSetting.value !== null) { - userHasEmailsMuted = emailsMutedSetting.value === muteEmailsSettingValue + if (emailsMutedSetting !== null && emailsMutedSetting.props.value !== null) { + userHasEmailsMuted = emailsMutedSetting.props.value === muteEmailsSettingValue } const keyParamsResponse = await getUserKeyParamsUseCase.execute({ @@ -74,7 +74,7 @@ const requestBackups = async ( await domainEventPublisher.publish( domainEventFactory.createEmailBackupRequestedEvent( setting.setting_user_uuid, - emailsMutedSetting?.uuid as string, + emailsMutedSetting?.id.toString() as string, userHasEmailsMuted, keyParamsResponse.keyParams, ), diff --git a/packages/auth/bin/user_email_backup.ts b/packages/auth/bin/user_email_backup.ts index c6d819b96..2e02a9c59 100644 --- a/packages/auth/bin/user_email_backup.ts +++ b/packages/auth/bin/user_email_backup.ts @@ -55,8 +55,8 @@ const requestBackups = async ( let userHasEmailsMuted = false const emailsMutedSetting = await settingRepository.findOneByNameAndUserUuid(muteEmailsSettingName, user.uuid) - if (emailsMutedSetting !== null && emailsMutedSetting.value !== null) { - userHasEmailsMuted = emailsMutedSetting.value === muteEmailsSettingValue + if (emailsMutedSetting !== null && emailsMutedSetting.props.value !== null) { + userHasEmailsMuted = emailsMutedSetting.props.value === muteEmailsSettingValue } const keyParamsResponse = await getUserKeyParamsUseCase.execute({ @@ -67,7 +67,7 @@ const requestBackups = async ( await domainEventPublisher.publish( domainEventFactory.createEmailBackupRequestedEvent( user.uuid, - emailsMutedSetting?.uuid as string, + emailsMutedSetting?.id.toString() as string, userHasEmailsMuted, keyParamsResponse.keyParams, ), diff --git a/packages/auth/migrations/mysql/1627638504691-move_mfa_items_to_user_settings.ts b/packages/auth/migrations/mysql/1627638504691-move_mfa_items_to_user_settings.ts index 33ac6ac83..eaa7183bf 100644 --- a/packages/auth/migrations/mysql/1627638504691-move_mfa_items_to_user_settings.ts +++ b/packages/auth/migrations/mysql/1627638504691-move_mfa_items_to_user_settings.ts @@ -5,6 +5,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm' import { Setting } from '../../src/Domain/Setting/Setting' import { User } from '../../src/Domain/User/User' import { EncryptionVersion } from '../../src/Domain/Encryption/EncryptionVersion' +import { Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core' export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { @@ -32,19 +33,21 @@ export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterfa usersMFAStatus.set(item['user_uuid'], 1) usersMFAUpdatedAt.set(item['user_uuid'], item['updated_at_timestamp']) - const setting = new Setting() - setting.uuid = item['uuid'] - setting.name = SettingName.NAMES.MfaSecret - setting.value = item['content'] + const settingOrError = Setting.create( + { + name: SettingName.NAMES.MfaSecret, + value: item['deleted'] ? null : item['content'], + serverEncryptionVersion: EncryptionVersion.Unencrypted, + timestamps: Timestamps.create(item['created_at_timestamp'], item['updated_at_timestamp']).getValue(), + userUuid: Uuid.create(user.uuid).getValue(), + sensitive: true, + }, + new UniqueEntityId(item['uuid']), + ) if (item['deleted']) { - setting.value = null usersMFAStatus.set(item['user_uuid'], 0) } - setting.serverEncryptionVersion = EncryptionVersion.Unencrypted - setting.createdAt = item['created_at_timestamp'] - setting.updatedAt = item['updated_at_timestamp'] - setting.user = Promise.resolve(user) - await queryRunner.manager.save(setting) + await queryRunner.manager.save(settingOrError.getValue()) } const redisClient = this.getRedisClient() diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index c24f9baa7..101b0b1eb 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -10,7 +10,7 @@ import { DomainEventSubscriberInterface, } from '@standardnotes/domain-events' import { TimerInterface, Timer } from '@standardnotes/time' -import { UAParser } from 'ua-parser-js' +import { UAParser, UAParserInstance } from 'ua-parser-js' import { Env } from './Env' import TYPES from './Types' @@ -56,10 +56,7 @@ import { TypeORMSettingRepository } from '../Infra/TypeORM/TypeORMSettingReposit import { CrypterInterface } from '../Domain/Encryption/CrypterInterface' import { CrypterNode } from '../Domain/Encryption/CrypterNode' import { CryptoNode } from '@standardnotes/sncrypto-node' -import { GetSettings } from '../Domain/UseCase/GetSettings/GetSettings' -import { SettingProjector } from '../Projection/SettingProjector' import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting' -import { UpdateSetting } from '../Domain/UseCase/UpdateSetting/UpdateSetting' import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler' import { SubscriptionPurchasedEventHandler } from '../Domain/Handler/SubscriptionPurchasedEventHandler' import { SubscriptionRenewedEventHandler } from '../Domain/Handler/SubscriptionRenewedEventHandler' @@ -67,8 +64,6 @@ import { SubscriptionRefundedEventHandler } from '../Domain/Handler/Subscription import { SubscriptionExpiredEventHandler } from '../Domain/Handler/SubscriptionExpiredEventHandler' import { DeleteAccount } from '../Domain/UseCase/DeleteAccount/DeleteAccount' import { DeleteSetting } from '../Domain/UseCase/DeleteSetting/DeleteSetting' -import { SettingFactory } from '../Domain/Setting/SettingFactory' -import { SettingService } from '../Domain/Setting/SettingService' import { UserSubscription } from '../Domain/Subscription/UserSubscription' import { TypeORMUserSubscriptionRepository } from '../Infra/TypeORM/TypeORMUserSubscriptionRepository' import { WebSocketsClientService } from '../Infra/WebSockets/WebSocketsClientService' @@ -80,7 +75,6 @@ import { RoleToSubscriptionMapInterface } from '../Domain/Role/RoleToSubscriptio import { RoleToSubscriptionMap } from '../Domain/Role/RoleToSubscriptionMap' import { FeatureServiceInterface } from '../Domain/Feature/FeatureServiceInterface' import { FeatureService } from '../Domain/Feature/FeatureService' -import { SettingServiceInterface } from '../Domain/Setting/SettingServiceInterface' import { ExtensionKeyGrantedEventHandler } from '../Domain/Handler/ExtensionKeyGrantedEventHandler' import { DirectCallDomainEventPublisher, @@ -138,8 +132,8 @@ import { FileRemovedEventHandler } from '../Domain/Handler/FileRemovedEventHandl import { UserDisabledSessionUserAgentLoggingEventHandler } from '../Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler' import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface' import { SettingInterpreter } from '../Domain/Setting/SettingInterpreter' -import { SettingDecrypterInterface } from '../Domain/Setting/SettingDecrypterInterface' -import { SettingDecrypter } from '../Domain/Setting/SettingDecrypter' +import { SettingCrypterInterface } from '../Domain/Setting/SettingCrypterInterface' +import { SettingCrypter } from '../Domain/Setting/SettingCrypter' import { SharedSubscriptionInvitationRepositoryInterface } from '../Domain/SharedSubscription/SharedSubscriptionInvitationRepositoryInterface' import { TypeORMSharedSubscriptionInvitationRepository } from '../Infra/TypeORM/TypeORMSharedSubscriptionInvitationRepository' import { InviteToSharedSubscription } from '../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription' @@ -148,16 +142,9 @@ import { AcceptSharedSubscriptionInvitation } from '../Domain/UseCase/AcceptShar import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSharedSubscriptionInvitation/DeclineSharedSubscriptionInvitation' import { CancelSharedSubscriptionInvitation } from '../Domain/UseCase/CancelSharedSubscriptionInvitation/CancelSharedSubscriptionInvitation' import { SharedSubscriptionInvitationCreatedEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCreatedEventHandler' -import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting' -import { SubscriptionSettingServiceInterface } from '../Domain/Setting/SubscriptionSettingServiceInterface' -import { SubscriptionSettingService } from '../Domain/Setting/SubscriptionSettingService' import { SubscriptionSettingRepositoryInterface } from '../Domain/Setting/SubscriptionSettingRepositoryInterface' import { TypeORMSubscriptionSettingRepository } from '../Infra/TypeORM/TypeORMSubscriptionSettingRepository' -import { SettingFactoryInterface } from '../Domain/Setting/SettingFactoryInterface' import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations' -import { UserSubscriptionServiceInterface } from '../Domain/Subscription/UserSubscriptionServiceInterface' -import { UserSubscriptionService } from '../Domain/Subscription/UserSubscriptionService' -import { SubscriptionSettingProjector } from '../Projection/SubscriptionSettingProjector' import { SubscriptionSettingsAssociationService } from '../Domain/Setting/SubscriptionSettingsAssociationService' import { SubscriptionSettingsAssociationServiceInterface } from '../Domain/Setting/SubscriptionSettingsAssociationServiceInterface' import { PKCERepositoryInterface } from '../Domain/User/PKCERepositoryInterface' @@ -266,6 +253,27 @@ import { UserDesignatedAsSurvivorInSharedVaultEventHandler } from '../Domain/Han import { DisableEmailSettingBasedOnEmailSubscription } from '../Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription' import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface' import { KeyParamsFactoryInterface } from '../Domain/User/KeyParamsFactoryInterface' +import { TypeORMSubscriptionSetting } from '../Infra/TypeORM/TypeORMSubscriptionSetting' +import { SetSettingValue } from '../Domain/UseCase/SetSettingValue/SetSettingValue' +import { ApplyDefaultSubscriptionSettings } from '../Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings' +import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting' +import { SetSubscriptionSettingValue } from '../Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue' +import { GetSettings } from '../Domain/UseCase/GetSettings/GetSettings' +import { GetSubscriptionSettings } from '../Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings' +import { GetAllSettingsForUser } from '../Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser' +import { GetRegularSubscriptionForUser } from '../Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' +import { GetSharedSubscriptionForUser } from '../Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser' +import { GetSharedOrRegularSubscriptionForUser } from '../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser' +import { ProjectorInterface } from '../Projection/ProjectorInterface' +import { SettingHttpRepresentation } from '../Mapping/Http/SettingHttpRepresentation' +import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting' +import { SubscriptionSettingHttpRepresentation } from '../Mapping/Http/SubscriptionSettingHttpRepresentation' +import { SettingHttpMapper } from '../Mapping/Http/SettingHttpMapper' +import { SubscriptionSettingHttpMapper } from '../Mapping/Http/SubscriptionSettingHttpMapper' +import { TypeORMSetting } from '../Infra/TypeORM/TypeORMSetting' +import { SettingPersistenceMapper } from '../Mapping/Persistence/SettingPersistenceMapper' +import { SubscriptionSettingPersistenceMapper } from '../Mapping/Persistence/SubscriptionSettingPersistenceMapper' +import { ApplyDefaultSettings } from '../Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings' export class ContainerConfigLoader { constructor(private mode: 'server' | 'worker' = 'server') {} @@ -396,6 +404,22 @@ export class ContainerConfigLoader { container .bind>(TYPES.Auth_SharedVaultUserPersistenceMapper) .toConstantValue(new SharedVaultUserPersistenceMapper()) + container + .bind>(TYPES.Auth_SettingHttpMapper) + .toConstantValue(new SettingHttpMapper()) + container + .bind>( + TYPES.Auth_SubscriptionSettingHttpMapper, + ) + .toConstantValue(new SubscriptionSettingHttpMapper()) + container + .bind>(TYPES.Auth_SettingPersistenceMapper) + .toConstantValue(new SettingPersistenceMapper()) + container + .bind>( + TYPES.Auth_SubscriptionSettingPersistenceMapper, + ) + .toConstantValue(new SubscriptionSettingPersistenceMapper()) // ORM container @@ -412,14 +436,14 @@ export class ContainerConfigLoader { .bind>(TYPES.Auth_ORMSessionRepository) .toConstantValue(appDataSource.getRepository(Session)) container - .bind>(TYPES.Auth_ORMSettingRepository) - .toConstantValue(appDataSource.getRepository(Setting)) + .bind>(TYPES.Auth_ORMSettingRepository) + .toConstantValue(appDataSource.getRepository(TypeORMSetting)) container .bind>(TYPES.Auth_ORMSharedSubscriptionInvitationRepository) .toConstantValue(appDataSource.getRepository(SharedSubscriptionInvitation)) container - .bind>(TYPES.Auth_ORMSubscriptionSettingRepository) - .toConstantValue(appDataSource.getRepository(SubscriptionSetting)) + .bind>(TYPES.Auth_ORMSubscriptionSettingRepository) + .toConstantValue(appDataSource.getRepository(TypeORMSubscriptionSetting)) container.bind>(TYPES.Auth_ORMUserRepository).toConstantValue(appDataSource.getRepository(User)) container .bind>(TYPES.Auth_ORMUserSubscriptionRepository) @@ -446,10 +470,24 @@ export class ContainerConfigLoader { .bind(TYPES.Auth_RevokedSessionRepository) .to(TypeORMRevokedSessionRepository) container.bind(TYPES.Auth_UserRepository).to(TypeORMUserRepository) - container.bind(TYPES.Auth_SettingRepository).to(TypeORMSettingRepository) + container + .bind(TYPES.Auth_SettingRepository) + .toConstantValue( + new TypeORMSettingRepository( + container.get>(TYPES.Auth_ORMSettingRepository), + container.get>(TYPES.Auth_SettingPersistenceMapper), + ), + ) container .bind(TYPES.Auth_SubscriptionSettingRepository) - .to(TypeORMSubscriptionSettingRepository) + .toConstantValue( + new TypeORMSubscriptionSettingRepository( + container.get>(TYPES.Auth_ORMSubscriptionSettingRepository), + container.get>( + TYPES.Auth_SubscriptionSettingPersistenceMapper, + ), + ), + ) container .bind(TYPES.Auth_OfflineSettingRepository) .to(TypeORMOfflineSettingRepository) @@ -512,13 +550,6 @@ export class ContainerConfigLoader { container.bind(TYPES.Auth_UserProjector).to(UserProjector) container.bind(TYPES.Auth_RoleProjector).to(RoleProjector) container.bind(TYPES.Auth_PermissionProjector).to(PermissionProjector) - container.bind(TYPES.Auth_SettingProjector).to(SettingProjector) - container - .bind(TYPES.Auth_SubscriptionSettingProjector) - .to(SubscriptionSettingProjector) - - // Factories - container.bind(TYPES.Auth_SettingFactory).to(SettingFactory) // env vars container.bind(TYPES.Auth_JWT_SECRET).toConstantValue(env.get('JWT_SECRET')) @@ -640,12 +671,55 @@ export class ContainerConfigLoader { .to(RedisSubscriptionTokenRepository) } - // Services + container + .bind(TYPES.Auth_TraceSession) + .toConstantValue( + new TraceSession( + container.get(TYPES.Auth_SessionTraceRepository), + container.get(TYPES.Auth_Timer), + container.get(TYPES.Auth_SESSION_TRACE_DAYS_TTL), + ), + ) container .bind>(TYPES.Auth_ProtocolVersionSelector) .toConstantValue(new DeterministicSelector()) - container.bind(TYPES.Auth_DeviceDetector).toConstantValue(new UAParser()) - container.bind(TYPES.Auth_SessionService).to(SessionService) + container.bind(TYPES.Auth_DeviceDetector).toConstantValue(new UAParser()) + container.bind(TYPES.Auth_Crypter).to(CrypterNode) + container + .bind(TYPES.Auth_SettingCrypter) + .toConstantValue( + new SettingCrypter( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_Crypter), + ), + ) + container + .bind(TYPES.Auth_GetSetting) + .toConstantValue( + new GetSetting( + container.get(TYPES.Auth_SettingRepository), + container.get(TYPES.Auth_SettingCrypter), + ), + ) + container + .bind(TYPES.Auth_SessionService) + .toConstantValue( + new SessionService( + container.get(TYPES.Auth_SessionRepository), + container.get(TYPES.Auth_EphemeralSessionRepository), + container.get(TYPES.Auth_RevokedSessionRepository), + container.get(TYPES.Auth_DeviceDetector), + container.get(TYPES.Auth_Timer), + container.get(TYPES.Auth_Logger), + container.get(TYPES.Auth_ACCESS_TOKEN_AGE), + container.get(TYPES.Auth_REFRESH_TOKEN_AGE), + container.get(TYPES.Auth_CryptoNode), + container.get(TYPES.Auth_TraceSession), + container.get(TYPES.Auth_UserSubscriptionRepository), + container.get(TYPES.Auth_READONLY_USERS), + container.get(TYPES.Auth_GetSetting), + ), + ) container.bind(TYPES.Auth_AuthResponseFactory20161215).to(AuthResponseFactory20161215) container.bind(TYPES.Auth_AuthResponseFactory20190520).to(AuthResponseFactory20190520) container.bind(TYPES.Auth_AuthResponseFactory20200115).to(AuthResponseFactory20200115) @@ -684,11 +758,9 @@ export class ContainerConfigLoader { .bind(TYPES.Auth_AuthenticationMethodResolver) .to(AuthenticationMethodResolver) container.bind(TYPES.Auth_DomainEventFactory).to(DomainEventFactory) - container.bind(TYPES.Auth_Crypter).to(CrypterNode) container .bind(TYPES.Auth_SettingsAssociationService) .to(SettingsAssociationService) - container.bind(TYPES.Auth_SettingDecrypter).to(SettingDecrypter) container .bind(TYPES.Auth_GetUserKeyParams) @@ -711,21 +783,6 @@ export class ContainerConfigLoader { ), ) - container - .bind(TYPES.Auth_SettingService) - .toConstantValue( - new SettingService( - container.get(TYPES.Auth_SettingFactory), - container.get(TYPES.Auth_SettingRepository), - container.get(TYPES.Auth_SettingsAssociationService), - container.get(TYPES.Auth_SettingInterpreter), - container.get(TYPES.Auth_SettingDecrypter), - container.get(TYPES.Auth_Logger), - ), - ) - container - .bind(TYPES.Auth_SubscriptionSettingService) - .to(SubscriptionSettingService) container.bind(TYPES.Auth_OfflineSettingService).to(OfflineSettingService) container.bind(TYPES.Auth_ContenDecoder).toConstantValue(new ContentDecoder()) container.bind(TYPES.Auth_WebSocketsClientService).to(WebSocketsClientService) @@ -738,7 +795,6 @@ export class ContainerConfigLoader { container .bind>(TYPES.Auth_BooleanSelector) .toConstantValue(new DeterministicSelector()) - container.bind(TYPES.Auth_UserSubscriptionService).to(UserSubscriptionService) // Middleware container.bind(TYPES.Auth_SessionMiddleware).to(SessionMiddleware) @@ -765,15 +821,6 @@ export class ContainerConfigLoader { container.bind(TYPES.Auth_OfflineUserAuthMiddleware).to(OfflineUserAuthMiddleware) // use cases - container - .bind(TYPES.Auth_TraceSession) - .toConstantValue( - new TraceSession( - container.get(TYPES.Auth_SessionTraceRepository), - container.get(TYPES.Auth_Timer), - container.get(TYPES.Auth_SESSION_TRACE_DAYS_TTL), - ), - ) container .bind(TYPES.Auth_PersistStatistics) .toConstantValue( @@ -848,24 +895,65 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_FeatureService), ), ) + container + .bind(TYPES.Auth_SetSettingValue) + .toConstantValue( + new SetSettingValue( + container.get(TYPES.Auth_GetSetting), + container.get(TYPES.Auth_SettingRepository), + container.get(TYPES.Auth_Timer), + container.get(TYPES.Auth_SettingsAssociationService), + container.get(TYPES.Auth_RoleService), + container.get(TYPES.Auth_SettingCrypter), + ), + ) container .bind(TYPES.Auth_GenerateRecoveryCodes) .toConstantValue( new GenerateRecoveryCodes( container.get(TYPES.Auth_UserRepository), - container.get(TYPES.Auth_SettingService), + container.get(TYPES.Auth_SetSettingValue), container.get(TYPES.Auth_CryptoNode), ), ) + container + .bind(TYPES.Auth_GetSubscriptionSetting) + .toConstantValue( + new GetSubscriptionSetting( + container.get(TYPES.Auth_SubscriptionSettingRepository), + container.get(TYPES.Auth_SettingCrypter), + ), + ) + container + .bind(TYPES.Auth_SetSubscriptionSettingValue) + .toConstantValue( + new SetSubscriptionSettingValue( + container.get(TYPES.Auth_SubscriptionSettingRepository), + container.get(TYPES.Auth_GetSubscriptionSetting), + container.get(TYPES.Auth_Timer), + ), + ) + container + .bind(TYPES.Auth_ApplyDefaultSubscriptionSettings) + .toConstantValue( + new ApplyDefaultSubscriptionSettings( + container.get( + TYPES.Auth_SubscriptionSettingsAssociationService, + ), + container.get(TYPES.Auth_UserSubscriptionRepository), + container.get(TYPES.Auth_GetSubscriptionSetting), + container.get(TYPES.Auth_SetSubscriptionSettingValue), + ), + ) container .bind(TYPES.Auth_ActivatePremiumFeatures) .toConstantValue( new ActivatePremiumFeatures( - container.get(TYPES.Auth_UserRepository), - container.get(TYPES.Auth_UserSubscriptionRepository), - container.get(TYPES.Auth_SubscriptionSettingService), - container.get(TYPES.Auth_RoleService), - container.get(TYPES.Auth_Timer), + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_UserSubscriptionRepository), + container.get(TYPES.Auth_ApplyDefaultSubscriptionSettings), + container.get(TYPES.Auth_RoleService), + container.get(TYPES.Auth_Timer), ), ) @@ -879,47 +967,136 @@ export class ContainerConfigLoader { container.bind(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest) container.bind(TYPES.Auth_RefreshSessionToken).to(RefreshSessionToken) container.bind(TYPES.Auth_SignIn).to(SignIn) - container.bind(TYPES.Auth_VerifyMFA).to(VerifyMFA) + container + .bind(TYPES.Auth_VerifyMFA) + .toConstantValue( + new VerifyMFA( + container.get(TYPES.Auth_UserRepository), + container.get>(TYPES.Auth_BooleanSelector), + container.get(TYPES.Auth_LockRepository), + container.get(TYPES.Auth_PSEUDO_KEY_PARAMS_KEY), + container.get(TYPES.Auth_AuthenticatorRepository), + container.get( + TYPES.Auth_VerifyAuthenticatorAuthenticationResponse, + ), + container.get(TYPES.Auth_GetSetting), + container.get(TYPES.Auth_Logger), + ), + ) container.bind(TYPES.Auth_ClearLoginAttempts).to(ClearLoginAttempts) container.bind(TYPES.Auth_IncreaseLoginAttempts).to(IncreaseLoginAttempts) container .bind(TYPES.Auth_GetUserKeyParamsRecovery) .toConstantValue( new GetUserKeyParamsRecovery( - container.get(TYPES.Auth_KeyParamsFactory), - container.get(TYPES.Auth_UserRepository), - container.get(TYPES.Auth_PKCERepository), - container.get(TYPES.Auth_SettingService), + container.get(TYPES.Auth_KeyParamsFactory), + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_PKCERepository), + container.get(TYPES.Auth_GetSetting), ), ) container.bind(TYPES.Auth_UpdateUser).to(UpdateUser) - container.bind(TYPES.Auth_Register).to(Register) + container + .bind(TYPES.Auth_ApplyDefaultSettings) + .toConstantValue( + new ApplyDefaultSettings( + container.get(TYPES.Auth_SettingsAssociationService), + container.get(TYPES.Auth_SetSettingValue), + ), + ) + container + .bind(TYPES.Auth_Register) + .toConstantValue( + new Register( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_RoleRepository), + container.get(TYPES.Auth_AuthResponseFactory20200115), + container.get(TYPES.Auth_Crypter), + container.get(TYPES.Auth_DISABLE_USER_REGISTRATION), + container.get(TYPES.Auth_Timer), + container.get(TYPES.Auth_ApplyDefaultSettings), + ), + ) container.bind(TYPES.Auth_GetActiveSessionsForUser).to(GetActiveSessionsForUser) container.bind(TYPES.Auth_DeleteOtherSessionsForUser).to(DeleteOtherSessionsForUser) container.bind(TYPES.Auth_DeleteSessionForUser).to(DeleteSessionForUser) container.bind(TYPES.Auth_ChangeCredentials).to(ChangeCredentials) - container.bind(TYPES.Auth_GetSettings).to(GetSettings) - container.bind(TYPES.Auth_GetSetting).to(GetSetting) + container + .bind(TYPES.Auth_GetSettings) + .toConstantValue( + new GetSettings( + container.get(TYPES.Auth_SettingRepository), + container.get(TYPES.Auth_SettingCrypter), + ), + ) + container + .bind(TYPES.Auth_GetSubscriptionSettings) + .toConstantValue( + new GetSubscriptionSettings( + container.get(TYPES.Auth_SubscriptionSettingRepository), + container.get(TYPES.Auth_SettingCrypter), + ), + ) + container + .bind(TYPES.Auth_GetRegularSubscriptionForUser) + .toConstantValue( + new GetRegularSubscriptionForUser( + container.get(TYPES.Auth_UserSubscriptionRepository), + ), + ) + container + .bind(TYPES.Auth_GetSharedSubscriptionForUser) + .toConstantValue( + new GetSharedSubscriptionForUser( + container.get(TYPES.Auth_UserSubscriptionRepository), + ), + ) + container + .bind(TYPES.Auth_GetSharedOrRegularSubscriptionForUser) + .toConstantValue( + new GetSharedOrRegularSubscriptionForUser( + container.get(TYPES.Auth_GetRegularSubscriptionForUser), + container.get(TYPES.Auth_GetSharedSubscriptionForUser), + ), + ) + container + .bind(TYPES.Auth_GetAllSettingsForUser) + .toConstantValue( + new GetAllSettingsForUser( + container.get(TYPES.Auth_GetSettings), + container.get(TYPES.Auth_GetSharedOrRegularSubscriptionForUser), + container.get(TYPES.Auth_GetSubscriptionSettings), + ), + ) container.bind(TYPES.Auth_GetUserFeatures).to(GetUserFeatures) - container.bind(TYPES.Auth_UpdateSetting).to(UpdateSetting) container.bind(TYPES.Auth_DeleteSetting).to(DeleteSetting) container .bind(TYPES.Auth_SignInWithRecoveryCodes) .toConstantValue( new SignInWithRecoveryCodes( - container.get(TYPES.Auth_UserRepository), - container.get(TYPES.Auth_AuthResponseFactory20200115), - container.get(TYPES.Auth_PKCERepository), - container.get(TYPES.Auth_Crypter), - container.get(TYPES.Auth_SettingService), - container.get(TYPES.Auth_GenerateRecoveryCodes), - container.get(TYPES.Auth_IncreaseLoginAttempts), - container.get(TYPES.Auth_ClearLoginAttempts), - container.get(TYPES.Auth_DeleteSetting), - container.get(TYPES.Auth_AuthenticatorRepository), + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_AuthResponseFactory20200115), + container.get(TYPES.Auth_PKCERepository), + container.get(TYPES.Auth_Crypter), + container.get(TYPES.Auth_GetSetting), + container.get(TYPES.Auth_GenerateRecoveryCodes), + container.get(TYPES.Auth_IncreaseLoginAttempts), + container.get(TYPES.Auth_ClearLoginAttempts), + container.get(TYPES.Auth_DeleteSetting), + container.get(TYPES.Auth_AuthenticatorRepository), + ), + ) + container + .bind(TYPES.Auth_DeleteAccount) + .toConstantValue( + new DeleteAccount( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_GetRegularSubscriptionForUser), + container.get(TYPES.Auth_DomainEventPublisher), + container.get(TYPES.Auth_DomainEventFactory), + container.get(TYPES.Auth_Timer), ), ) - container.bind(TYPES.Auth_DeleteAccount).to(DeleteAccount) container.bind(TYPES.Auth_GetUserSubscription).to(GetUserSubscription) container.bind(TYPES.Auth_GetUserOfflineSubscription).to(GetUserOfflineSubscription) container.bind(TYPES.Auth_CreateSubscriptionToken).to(CreateSubscriptionToken) @@ -932,12 +1109,38 @@ export class ContainerConfigLoader { container .bind(TYPES.Auth_CreateOfflineSubscriptionToken) .to(CreateOfflineSubscriptionToken) - container.bind(TYPES.Auth_CreateValetToken).to(CreateValetToken) + container + .bind(TYPES.Auth_CreateValetToken) + .toConstantValue( + new CreateValetToken( + container.get>(TYPES.Auth_ValetTokenEncoder), + container.get( + TYPES.Auth_SubscriptionSettingsAssociationService, + ), + container.get(TYPES.Auth_GetRegularSubscriptionForUser), + container.get(TYPES.Auth_GetSharedSubscriptionForUser), + container.get(TYPES.Auth_GetSubscriptionSetting), + container.get(TYPES.Auth_Timer), + container.get(TYPES.Auth_VALET_TOKEN_TTL), + ), + ) container.bind(TYPES.Auth_CreateListedAccount).to(CreateListedAccount) container.bind(TYPES.Auth_InviteToSharedSubscription).to(InviteToSharedSubscription) container .bind(TYPES.Auth_AcceptSharedSubscriptionInvitation) - .to(AcceptSharedSubscriptionInvitation) + .toConstantValue( + new AcceptSharedSubscriptionInvitation( + container.get( + TYPES.Auth_SharedSubscriptionInvitationRepository, + ), + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_UserSubscriptionRepository), + container.get(TYPES.Auth_RoleService), + container.get(TYPES.Auth_ApplyDefaultSubscriptionSettings), + container.get(TYPES.Auth_Timer), + container.get(TYPES.Auth_Logger), + ), + ) container .bind(TYPES.Auth_DeclineSharedSubscriptionInvitation) .to(DeclineSharedSubscriptionInvitation) @@ -948,15 +1151,32 @@ export class ContainerConfigLoader { .bind(TYPES.Auth_ListSharedSubscriptionInvitations) .to(ListSharedSubscriptionInvitations) container.bind(TYPES.Auth_VerifyPredicate).to(VerifyPredicate) - container.bind(TYPES.Auth_CreateCrossServiceToken).to(CreateCrossServiceToken) + container + .bind(TYPES.Auth_CreateCrossServiceToken) + .toConstantValue( + new CreateCrossServiceToken( + container.get>(TYPES.Auth_UserProjector), + container.get>(TYPES.Auth_SessionProjector), + container.get>(TYPES.Auth_RoleProjector), + container.get>(TYPES.Auth_CrossServiceTokenEncoder), + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_AUTH_JWT_TTL), + container.get(TYPES.Auth_GetRegularSubscriptionForUser), + container.get(TYPES.Auth_GetSubscriptionSetting), + container.get(TYPES.Auth_SharedVaultUserRepository), + ), + ) container.bind(TYPES.Auth_ProcessUserRequest).to(ProcessUserRequest) container .bind(TYPES.Auth_UpdateStorageQuotaUsedForUser) .toConstantValue( new UpdateStorageQuotaUsedForUser( - container.get(TYPES.Auth_UserRepository), - container.get(TYPES.Auth_UserSubscriptionService), - container.get(TYPES.Auth_SubscriptionSettingService), + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_GetRegularSubscriptionForUser), + container.get(TYPES.Auth_GetSharedSubscriptionForUser), + container.get(TYPES.Auth_GetSubscriptionSetting), + container.get(TYPES.Auth_SetSubscriptionSettingValue), + container.get(TYPES.Auth_Logger), ), ) container @@ -984,8 +1204,9 @@ export class ContainerConfigLoader { .toConstantValue( new DisableEmailSettingBasedOnEmailSubscription( container.get(TYPES.Auth_UserRepository), - container.get(TYPES.Auth_SettingRepository), - container.get(TYPES.Auth_SettingFactory), + container.get(TYPES.Auth_SetSettingValue), + container.get(TYPES.Auth_SetSubscriptionSettingValue), + container.get(TYPES.Auth_GetSharedOrRegularSubscriptionForUser), ), ) @@ -1039,7 +1260,16 @@ export class ContainerConfigLoader { ) container .bind(TYPES.Auth_SubscriptionPurchasedEventHandler) - .to(SubscriptionPurchasedEventHandler) + .toConstantValue( + new SubscriptionPurchasedEventHandler( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_UserSubscriptionRepository), + container.get(TYPES.Auth_ApplyDefaultSubscriptionSettings), + container.get(TYPES.Auth_OfflineUserSubscriptionRepository), + container.get(TYPES.Auth_RoleService), + container.get(TYPES.Auth_Logger), + ), + ) container .bind(TYPES.Auth_SubscriptionCancelledEventHandler) .to(SubscriptionCancelledEventHandler) @@ -1054,13 +1284,42 @@ export class ContainerConfigLoader { .to(SubscriptionExpiredEventHandler) container .bind(TYPES.Auth_SubscriptionSyncRequestedEventHandler) - .to(SubscriptionSyncRequestedEventHandler) + .toConstantValue( + new SubscriptionSyncRequestedEventHandler( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_UserSubscriptionRepository), + container.get(TYPES.Auth_OfflineUserSubscriptionRepository), + container.get(TYPES.Auth_RoleService), + container.get(TYPES.Auth_ApplyDefaultSubscriptionSettings), + container.get(TYPES.Auth_SetSettingValue), + container.get(TYPES.Auth_OfflineSettingService), + container.get(TYPES.Auth_ContenDecoder), + container.get(TYPES.Auth_Logger), + ), + ) container .bind(TYPES.Auth_ExtensionKeyGrantedEventHandler) - .to(ExtensionKeyGrantedEventHandler) + .toConstantValue( + new ExtensionKeyGrantedEventHandler( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_SetSettingValue), + container.get(TYPES.Auth_OfflineSettingService), + container.get(TYPES.Auth_ContenDecoder), + container.get(TYPES.Auth_Logger), + ), + ) container .bind(TYPES.Auth_SubscriptionReassignedEventHandler) - .to(SubscriptionReassignedEventHandler) + .toConstantValue( + new SubscriptionReassignedEventHandler( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_UserSubscriptionRepository), + container.get(TYPES.Auth_RoleService), + container.get(TYPES.Auth_Logger), + container.get(TYPES.Auth_ApplyDefaultSubscriptionSettings), + container.get(TYPES.Auth_SetSettingValue), + ), + ) container .bind(TYPES.Auth_FileUploadedEventHandler) .toConstantValue( @@ -1103,10 +1362,24 @@ export class ContainerConfigLoader { ) container .bind(TYPES.Auth_ListedAccountCreatedEventHandler) - .to(ListedAccountCreatedEventHandler) + .toConstantValue( + new ListedAccountCreatedEventHandler( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_GetSetting), + container.get(TYPES.Auth_SetSettingValue), + container.get(TYPES.Auth_Logger), + ), + ) container .bind(TYPES.Auth_ListedAccountDeletedEventHandler) - .to(ListedAccountDeletedEventHandler) + .toConstantValue( + new ListedAccountDeletedEventHandler( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_GetSetting), + container.get(TYPES.Auth_SetSettingValue), + container.get(TYPES.Auth_Logger), + ), + ) container .bind(TYPES.Auth_UserDisabledSessionUserAgentLoggingEventHandler) .to(UserDisabledSessionUserAgentLoggingEventHandler) @@ -1322,33 +1595,41 @@ export class ContainerConfigLoader { .bind(TYPES.Auth_BaseSubscriptionTokensController) .toConstantValue( new BaseSubscriptionTokensController( - container.get(TYPES.Auth_CreateSubscriptionToken), - container.get(TYPES.Auth_AuthenticateSubscriptionToken), - container.get(TYPES.Auth_SettingService), - container.get(TYPES.Auth_UserProjector), - container.get(TYPES.Auth_RoleProjector), - container.get(TYPES.Auth_CrossServiceTokenEncoder), - container.get(TYPES.Auth_AUTH_JWT_TTL), - container.get(TYPES.Auth_ControllerContainer), + container.get(TYPES.Auth_CreateSubscriptionToken), + container.get(TYPES.Auth_AuthenticateSubscriptionToken), + container.get(TYPES.Auth_GetSetting), + container.get>(TYPES.Auth_UserProjector), + container.get>(TYPES.Auth_RoleProjector), + container.get>(TYPES.Auth_CrossServiceTokenEncoder), + container.get(TYPES.Auth_AUTH_JWT_TTL), + container.get(TYPES.Auth_ControllerContainer), ), ) container .bind(TYPES.Auth_BaseSubscriptionSettingsController) .toConstantValue( new BaseSubscriptionSettingsController( - container.get(TYPES.Auth_GetSetting), - container.get(TYPES.Auth_ControllerContainer), + container.get(TYPES.Auth_GetSubscriptionSetting), + container.get(TYPES.Auth_GetSharedOrRegularSubscriptionForUser), + container.get>( + TYPES.Auth_SubscriptionSettingHttpMapper, + ), + container.get(TYPES.Auth_ControllerContainer), ), ) container .bind(TYPES.Auth_BaseSettingsController) .toConstantValue( new BaseSettingsController( - container.get(TYPES.Auth_GetSettings), - container.get(TYPES.Auth_GetSetting), - container.get(TYPES.Auth_UpdateSetting), - container.get(TYPES.Auth_DeleteSetting), - container.get(TYPES.Auth_ControllerContainer), + container.get(TYPES.Auth_GetAllSettingsForUser), + container.get(TYPES.Auth_GetSetting), + container.get(TYPES.Auth_SetSettingValue), + container.get(TYPES.Auth_DeleteSetting), + container.get>(TYPES.Auth_SettingHttpMapper), + container.get>( + TYPES.Auth_SubscriptionSettingHttpMapper, + ), + container.get(TYPES.Auth_ControllerContainer), ), ) container diff --git a/packages/auth/src/Bootstrap/DataSource.ts b/packages/auth/src/Bootstrap/DataSource.ts index 8e71ab264..ca3341815 100644 --- a/packages/auth/src/Bootstrap/DataSource.ts +++ b/packages/auth/src/Bootstrap/DataSource.ts @@ -5,8 +5,6 @@ import { Role } from '../Domain/Role/Role' import { RevokedSession } from '../Domain/Session/RevokedSession' import { Session } from '../Domain/Session/Session' import { OfflineSetting } from '../Domain/Setting/OfflineSetting' -import { Setting } from '../Domain/Setting/Setting' -import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting' import { SharedSubscriptionInvitation } from '../Domain/SharedSubscription/SharedSubscriptionInvitation' import { OfflineUserSubscription } from '../Domain/Subscription/OfflineUserSubscription' import { UserSubscription } from '../Domain/Subscription/UserSubscription' @@ -19,6 +17,8 @@ import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace' import { Env } from './Env' import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions' import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser' +import { TypeORMSubscriptionSetting } from '../Infra/TypeORM/TypeORMSubscriptionSetting' +import { TypeORMSetting } from '../Infra/TypeORM/TypeORMSetting' export class AppDataSource { private _dataSource: DataSource | undefined @@ -61,10 +61,10 @@ export class AppDataSource { RevokedSession, Role, Permission, - Setting, + TypeORMSetting, OfflineSetting, SharedSubscriptionInvitation, - SubscriptionSetting, + TypeORMSubscriptionSetting, TypeORMSessionTrace, TypeORMAuthenticator, TypeORMAuthenticatorChallenge, diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index 90b78401e..99ba59796 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -10,6 +10,10 @@ const TYPES = { Auth_AuthenticatorHttpMapper: Symbol.for('Auth_AuthenticatorHttpMapper'), Auth_CacheEntryPersistenceMapper: Symbol.for('Auth_CacheEntryPersistenceMapper'), Auth_SharedVaultUserPersistenceMapper: Symbol.for('Auth_SharedVaultUserPersistenceMapper'), + Auth_SettingHttpMapper: Symbol.for('Auth_SettingHttpMapper'), + Auth_SubscriptionSettingHttpMapper: Symbol.for('Auth_SubscriptionSettingHttpMapper'), + Auth_SubscriptionSettingPersistenceMapper: Symbol.for('Auth_SubscriptionSettingPersistenceMapper'), + Auth_SettingPersistenceMapper: Symbol.for('Auth_SettingPersistenceMapper'), // Controller Auth_ControllerContainer: Symbol.for('Auth_ControllerContainer'), Auth_AuthController: Symbol.for('Auth_AuthController'), @@ -65,10 +69,6 @@ const TYPES = { Auth_UserProjector: Symbol.for('Auth_UserProjector'), Auth_RoleProjector: Symbol.for('Auth_RoleProjector'), Auth_PermissionProjector: Symbol.for('Auth_PermissionProjector'), - Auth_SettingProjector: Symbol.for('Auth_SettingProjector'), - Auth_SubscriptionSettingProjector: Symbol.for('Auth_SubscriptionSettingProjector'), - // Factories - Auth_SettingFactory: Symbol.for('Auth_SettingFactory'), // env vars Auth_JWT_SECRET: Symbol.for('Auth_JWT_SECRET'), Auth_LEGACY_JWT_SECRET: Symbol.for('Auth_LEGACY_JWT_SECRET'), @@ -115,9 +115,12 @@ const TYPES = { Auth_DeleteSessionForUser: Symbol.for('Auth_DeleteSessionForUser'), Auth_ChangeCredentials: Symbol.for('Auth_ChangePassword'), Auth_GetSettings: Symbol.for('Auth_GetSettings'), + Auth_GetSubscriptionSettings: Symbol.for('Auth_GetSubscriptionSettings'), + Auth_GetRegularSubscriptionForUser: Symbol.for('Auth_GetRegularSubscriptionForUser'), + Auth_GetSharedSubscriptionForUser: Symbol.for('Auth_GetSharedSubscriptionForUser'), + Auth_GetAllSettingsForUser: Symbol.for('Auth_GetAllSettingsForUser'), Auth_GetSetting: Symbol.for('Auth_GetSetting'), Auth_GetUserFeatures: Symbol.for('Auth_GetUserFeatures'), - Auth_UpdateSetting: Symbol.for('Auth_UpdateSetting'), Auth_DeleteSetting: Symbol.for('Auth_DeleteSetting'), Auth_DeleteAccount: Symbol.for('Auth_DeleteAccount'), Auth_GetUserSubscription: Symbol.for('Auth_GetUserSubscription'), @@ -146,7 +149,12 @@ const TYPES = { Auth_VerifyAuthenticatorAuthenticationResponse: Symbol.for('Auth_VerifyAuthenticatorAuthenticationResponse'), Auth_ListAuthenticators: Symbol.for('Auth_ListAuthenticators'), Auth_DeleteAuthenticator: Symbol.for('Auth_DeleteAuthenticator'), + Auth_SetSettingValue: Symbol.for('Auth_SetSettingValue'), Auth_GenerateRecoveryCodes: Symbol.for('Auth_GenerateRecoveryCodes'), + Auth_GetSubscriptionSetting: Symbol.for('Auth_GetSubscriptionSetting'), + Auth_SetSubscriptionSettingValue: Symbol.for('Auth_SetSubscriptionSettingValue'), + Auth_ApplyDefaultSubscriptionSettings: Symbol.for('Auth_ApplyDefaultSubscriptionSettings'), + Auth_ApplyDefaultSettings: Symbol.for('Auth_ApplyDefaultSettings'), Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'), Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'), Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'), @@ -154,6 +162,7 @@ const TYPES = { Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'), Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'), Auth_DesignateSurvivor: Symbol.for('Auth_DesignateSurvivor'), + Auth_GetSharedOrRegularSubscriptionForUser: Symbol.for('Auth_GetSharedOrRegularSubscriptionForUser'), Auth_DisableEmailSettingBasedOnEmailSubscription: Symbol.for('Auth_DisableEmailSettingBasedOnEmailSubscription'), // Handlers Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'), @@ -189,8 +198,6 @@ const TYPES = { // Services Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'), Auth_SessionService: Symbol.for('Auth_SessionService'), - Auth_SettingService: Symbol.for('Auth_SettingService'), - Auth_SubscriptionSettingService: Symbol.for('Auth_SubscriptionSettingService'), Auth_OfflineSettingService: Symbol.for('Auth_OfflineSettingService'), Auth_AuthResponseFactory20161215: Symbol.for('Auth_AuthResponseFactory20161215'), Auth_AuthResponseFactory20190520: Symbol.for('Auth_AuthResponseFactory20190520'), @@ -221,11 +228,10 @@ const TYPES = { Auth_SettingsAssociationService: Symbol.for('Auth_SettingsAssociationService'), Auth_SubscriptionSettingsAssociationService: Symbol.for('Auth_SubscriptionSettingsAssociationService'), Auth_FeatureService: Symbol.for('Auth_FeatureService'), - Auth_SettingDecrypter: Symbol.for('Auth_SettingDecrypter'), + Auth_SettingCrypter: Symbol.for('Auth_SettingCrypter'), Auth_SettingInterpreter: Symbol.for('Auth_SettingInterpreter'), Auth_ProtocolVersionSelector: Symbol.for('Auth_ProtocolVersionSelector'), Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'), - Auth_UserSubscriptionService: Symbol.for('Auth_UserSubscriptionService'), Auth_BaseAuthController: Symbol.for('Auth_BaseAuthController'), Auth_BaseAuthenticatorsController: Symbol.for('Auth_BaseAuthenticatorsController'), Auth_BaseSubscriptionInvitesController: Symbol.for('Auth_BaseSubscriptionInvitesController'), diff --git a/packages/auth/src/Domain/Feature/FeatureService.spec.ts b/packages/auth/src/Domain/Feature/FeatureService.spec.ts index 930c1a633..476cd0e8b 100644 --- a/packages/auth/src/Domain/Feature/FeatureService.spec.ts +++ b/packages/auth/src/Domain/Feature/FeatureService.spec.ts @@ -111,7 +111,6 @@ describe('FeatureService', () => { cancelled: false, subscriptionId: 1, subscriptionType: UserSubscriptionType.Regular, - subscriptionSettings: Promise.resolve([]), } subscription2 = { @@ -125,7 +124,6 @@ describe('FeatureService', () => { cancelled: false, subscriptionId: 2, subscriptionType: UserSubscriptionType.Regular, - subscriptionSettings: Promise.resolve([]), } subscription3 = { @@ -139,7 +137,6 @@ describe('FeatureService', () => { cancelled: true, subscriptionId: 3, subscriptionType: UserSubscriptionType.Regular, - subscriptionSettings: Promise.resolve([]), } subscription4 = { @@ -153,7 +150,6 @@ describe('FeatureService', () => { cancelled: true, subscriptionId: 4, subscriptionType: UserSubscriptionType.Regular, - subscriptionSettings: Promise.resolve([]), } user = { @@ -329,7 +325,6 @@ describe('FeatureService', () => { cancelled: false, subscriptionId: 1, subscriptionType: UserSubscriptionType.Regular, - subscriptionSettings: Promise.resolve([]), } user = { diff --git a/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.spec.ts deleted file mode 100644 index f81719006..000000000 --- a/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import 'reflect-metadata' - -import { ExtensionKeyGrantedEvent } from '@standardnotes/domain-events' -import { Logger } from 'winston' - -import * as dayjs from 'dayjs' - -import { User } from '../User/User' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { ExtensionKeyGrantedEventHandler } from './ExtensionKeyGrantedEventHandler' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' -import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface' -import { ContentDecoderInterface, SubscriptionName } from '@standardnotes/common' - -describe('ExtensionKeyGrantedEventHandler', () => { - let userRepository: UserRepositoryInterface - let logger: Logger - let user: User - let event: ExtensionKeyGrantedEvent - let settingService: SettingServiceInterface - let offlineSettingService: OfflineSettingServiceInterface - let contentDecoder: ContentDecoderInterface - let timestamp: number - - const createHandler = () => - new ExtensionKeyGrantedEventHandler(userRepository, settingService, offlineSettingService, contentDecoder, logger) - - beforeEach(() => { - user = { - uuid: '123', - } as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - - settingService = {} as jest.Mocked - settingService.createOrReplace = jest.fn() - - offlineSettingService = {} as jest.Mocked - offlineSettingService.createOrUpdate = jest.fn() - - timestamp = dayjs.utc().valueOf() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - userEmail: 'test@test.com', - extensionKey: 'abc123', - offline: false, - offlineFeaturesToken: 'test', - subscriptionName: SubscriptionName.ProPlan, - origin: 'update-subscription', - timestamp, - payAmount: 1000, - billingEveryNMonths: 1, - activeUntil: new Date(10).toString(), - } - - contentDecoder = {} as jest.Mocked - contentDecoder.decode = jest.fn().mockReturnValue({ - featuresUrl: 'http://features-url', - extensionKey: 'key', - }) - - logger = {} as jest.Mocked - logger.info = jest.fn() - logger.warn = jest.fn() - }) - - it('should add extension key as an user offline features token for offline user setting', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(offlineSettingService.createOrUpdate).toHaveBeenCalledWith({ - email: 'test@test.com', - name: 'FEATURES_TOKEN', - value: 'key', - }) - }) - - it('should add extension key as an user offline features token if not possible to decode', async () => { - event.payload.offline = true - - contentDecoder.decode = jest.fn().mockReturnValue({}) - - await createHandler().handle(event) - - expect(offlineSettingService.createOrUpdate).not.toHaveBeenCalled() - }) - - it('should add extension key as user setting', async () => { - await createHandler().handle(event) - - expect(settingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'EXTENSION_KEY', - serverEncryptionVersion: 1, - unencryptedValue: 'abc123', - sensitive: true, - }, - user: { - uuid: '123', - }, - }) - }) - - it('should not do anything if no user is found for specified email', async () => { - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - }) - - it('should not do anything if user email is invalid', async () => { - event.payload.userEmail = '' - - await createHandler().handle(event) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - }) -}) diff --git a/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.ts b/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.ts index f0d58ebcb..c8b671749 100644 --- a/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.ts @@ -1,26 +1,22 @@ import { DomainEventHandlerInterface, ExtensionKeyGrantedEvent } from '@standardnotes/domain-events' +import { Username } from '@standardnotes/domain-core' import { SettingName } from '@standardnotes/settings' import { OfflineFeaturesTokenData } from '@standardnotes/security' import { ContentDecoderInterface } from '@standardnotes/common' -import { inject, injectable } from 'inversify' import { Logger } from 'winston' -import TYPES from '../../Bootstrap/Types' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface' import { OfflineSettingName } from '../Setting/OfflineSettingName' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { Username } from '@standardnotes/domain-core' +import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue' -@injectable() export class ExtensionKeyGrantedEventHandler implements DomainEventHandlerInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_OfflineSettingService) private offlineSettingService: OfflineSettingServiceInterface, - @inject(TYPES.Auth_ContenDecoder) private contentDecoder: ContentDecoderInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, + private userRepository: UserRepositoryInterface, + private setSettingValue: SetSettingValue, + private offlineSettingService: OfflineSettingServiceInterface, + private contentDecoder: ContentDecoderInterface, + private logger: Logger, ) {} async handle(event: ExtensionKeyGrantedEvent): Promise { @@ -58,14 +54,14 @@ export class ExtensionKeyGrantedEventHandler implements DomainEventHandlerInterf return } - await this.settingService.createOrReplace({ - user, - props: { - name: SettingName.NAMES.ExtensionKey, - unencryptedValue: event.payload.extensionKey, - serverEncryptionVersion: EncryptionVersion.Default, - sensitive: true, - }, + const result = await this.setSettingValue.execute({ + userUuid: user.uuid, + settingName: SettingName.NAMES.ExtensionKey, + value: event.payload.extensionKey, }) + + if (result.isFailed()) { + this.logger.error(`Could not set extension key for user ${user.uuid}`) + } } } diff --git a/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.spec.ts deleted file mode 100644 index 6b0640c60..000000000 --- a/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import 'reflect-metadata' -import { ListedAccountCreatedEvent } from '@standardnotes/domain-events' -import { Logger } from 'winston' - -import { ListedAccountCreatedEventHandler } from './ListedAccountCreatedEventHandler' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { User } from '../User/User' -import { Setting } from '../Setting/Setting' - -describe('ListedAccountCreatedEventHandler', () => { - let settingService: SettingServiceInterface - let userRepository: UserRepositoryInterface - let event: ListedAccountCreatedEvent - let user: User - let logger: Logger - - const createHandler = () => new ListedAccountCreatedEventHandler(userRepository, settingService, logger) - - beforeEach(() => { - user = {} as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - - settingService = {} as jest.Mocked - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) - settingService.createOrReplace = jest.fn() - - event = {} as jest.Mocked - event.payload = { - userEmail: 'test@test.com', - userId: 1, - userName: 'testuser', - secret: 'new-secret', - hostUrl: 'https://dev.listed.to', - } - - logger = {} as jest.Mocked - logger.warn = jest.fn() - }) - - it('should not save the listed secret if username is invalid', async () => { - event.payload.userEmail = '' - - await createHandler().handle(event) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - }) - - it('should not save the listed secret if user is not found', async () => { - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - }) - - it('should save the listed secret as a user setting', async () => { - await createHandler().handle(event) - - expect(settingService.createOrReplace).toHaveBeenCalledWith({ - user, - props: { - name: 'LISTED_AUTHOR_SECRETS', - sensitive: false, - unencryptedValue: '[{"authorId":1,"secret":"new-secret","hostUrl":"https://dev.listed.to"}]', - }, - }) - }) - - it('should add the listed secret as a user setting to an existing list of secrets', async () => { - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({ - value: '[{"authorId":2,"secret":"old-secret","hostUrl":"https://dev.listed.to"}]', - } as jest.Mocked) - - await createHandler().handle(event) - - expect(settingService.createOrReplace).toHaveBeenCalledWith({ - user, - props: { - name: 'LISTED_AUTHOR_SECRETS', - sensitive: false, - unencryptedValue: - '[{"authorId":2,"secret":"old-secret","hostUrl":"https://dev.listed.to"},{"authorId":1,"secret":"new-secret","hostUrl":"https://dev.listed.to"}]', - }, - }) - }) -}) diff --git a/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.ts b/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.ts index d4411db62..ac4147a25 100644 --- a/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.ts @@ -1,19 +1,18 @@ import { Username } from '@standardnotes/domain-core' import { DomainEventHandlerInterface, ListedAccountCreatedEvent } from '@standardnotes/domain-events' import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings' -import { inject, injectable } from 'inversify' import { Logger } from 'winston' -import TYPES from '../../Bootstrap/Types' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' +import { GetSetting } from '../UseCase/GetSetting/GetSetting' +import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue' -@injectable() export class ListedAccountCreatedEventHandler implements DomainEventHandlerInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, + private userRepository: UserRepositoryInterface, + private getSetting: GetSetting, + private setSettingValue: SetSettingValue, + private logger: Logger, ) {} async handle(event: ListedAccountCreatedEvent): Promise { @@ -34,23 +33,27 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter let authSecrets: ListedAuthorSecretsData = [newSecret] - const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(), + const listedAuthorSecretsSettingOrError = await this.getSetting.execute({ + settingName: SettingName.NAMES.ListedAuthorSecrets, userUuid: user.uuid, + decrypted: true, + allowSensitiveRetrieval: false, }) - if (listedAuthorSecretsSetting !== null) { - const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.value as string) + if (!listedAuthorSecretsSettingOrError.isFailed()) { + const listedAuthorSecretsSetting = listedAuthorSecretsSettingOrError.getValue() + const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.decryptedValue as string) existingSecrets.push(newSecret) authSecrets = existingSecrets } - await this.settingService.createOrReplace({ - user, - props: { - name: SettingName.NAMES.ListedAuthorSecrets, - unencryptedValue: JSON.stringify(authSecrets), - sensitive: false, - }, + const result = await this.setSettingValue.execute({ + userUuid: user.uuid, + settingName: SettingName.NAMES.ListedAuthorSecrets, + value: JSON.stringify(authSecrets), }) + + if (result.isFailed()) { + this.logger.error(`Could not update listed author secrets for user with uuid ${user.uuid}`) + } } } diff --git a/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.spec.ts deleted file mode 100644 index 3d60ed723..000000000 --- a/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import 'reflect-metadata' -import { ListedAccountDeletedEvent } from '@standardnotes/domain-events' -import { Logger } from 'winston' - -import { ListedAccountDeletedEventHandler } from './ListedAccountDeletedEventHandler' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { User } from '../User/User' -import { Setting } from '../Setting/Setting' - -describe('ListedAccountDeletedEventHandler', () => { - let settingService: SettingServiceInterface - let userRepository: UserRepositoryInterface - let event: ListedAccountDeletedEvent - let user: User - let logger: Logger - - const createHandler = () => new ListedAccountDeletedEventHandler(userRepository, settingService, logger) - - beforeEach(() => { - user = {} as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - - settingService = {} as jest.Mocked - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({ - value: '[{"authorId":1,"secret":"my-secret","hostUrl":"https://dev.listed.to"}]', - } as jest.Mocked) - settingService.createOrReplace = jest.fn() - - event = {} as jest.Mocked - event.payload = { - userEmail: 'test@test.com', - userId: 1, - userName: 'testuser', - secret: 'my-secret', - hostUrl: 'https://dev.listed.to', - } - - logger = {} as jest.Mocked - logger.warn = jest.fn() - }) - - it('should not remove the listed secret if username is invalid', async () => { - event.payload.userEmail = '' - - await createHandler().handle(event) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - }) - - it('should not remove the listed secret if user is not found', async () => { - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - }) - - it('should not remove the listed secret if setting is not found', async () => { - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - }) - - it('should remove the listed secret from the user setting', async () => { - await createHandler().handle(event) - - expect(settingService.createOrReplace).toHaveBeenCalledWith({ - user, - props: { - name: 'LISTED_AUTHOR_SECRETS', - sensitive: false, - unencryptedValue: '[]', - }, - }) - }) - - it('should remove the listed secret from an existing list of secrets', async () => { - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({ - value: - '[{"authorId":2,"secret":"old-secret","hostUrl":"https://dev.listed.to"},{"authorId":1,"secret":"my-secret","hostUrl":"https://dev.listed.to"},{"authorId":1,"secret":"my-secret","hostUrl":"https://local.listed.to"}]', - } as jest.Mocked) - - await createHandler().handle(event) - - expect(settingService.createOrReplace).toHaveBeenCalledWith({ - user, - props: { - name: 'LISTED_AUTHOR_SECRETS', - sensitive: false, - unencryptedValue: - '[{"authorId":2,"secret":"old-secret","hostUrl":"https://dev.listed.to"},{"authorId":1,"secret":"my-secret","hostUrl":"https://local.listed.to"}]', - }, - }) - }) -}) diff --git a/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.ts b/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.ts index 9011e1a3c..482b4b07e 100644 --- a/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.ts @@ -1,19 +1,18 @@ import { Username } from '@standardnotes/domain-core' import { DomainEventHandlerInterface, ListedAccountDeletedEvent } from '@standardnotes/domain-events' import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings' -import { inject, injectable } from 'inversify' import { Logger } from 'winston' -import TYPES from '../../Bootstrap/Types' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' +import { GetSetting } from '../UseCase/GetSetting/GetSetting' +import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue' -@injectable() export class ListedAccountDeletedEventHandler implements DomainEventHandlerInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, + private userRepository: UserRepositoryInterface, + private getSetting: GetSetting, + private setSettingValue: SetSettingValue, + private logger: Logger, ) {} async handle(event: ListedAccountDeletedEvent): Promise { @@ -31,30 +30,35 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter return } - const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(), + const listedAuthorSecretsSettingOrError = await this.getSetting.execute({ + settingName: SettingName.NAMES.ListedAuthorSecrets, + decrypted: true, userUuid: user.uuid, + allowSensitiveRetrieval: false, }) - if (listedAuthorSecretsSetting === null) { - this.logger.warn(`Could not find listed secrets setting for user ${user.uuid}`) + if (listedAuthorSecretsSettingOrError.isFailed()) { + this.logger.error(`Could not find listed secrets setting for user ${user.uuid}`) return } - const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.value as string) + const listedAuthorSecretsSetting = listedAuthorSecretsSettingOrError.getValue() + + const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.decryptedValue as string) const filteredSecrets = existingSecrets.filter( (secret) => secret.authorId !== event.payload.userId || (secret.authorId === event.payload.userId && secret.hostUrl !== event.payload.hostUrl), ) - await this.settingService.createOrReplace({ - user, - props: { - name: SettingName.NAMES.ListedAuthorSecrets, - unencryptedValue: JSON.stringify(filteredSecrets), - sensitive: false, - }, + const result = await this.setSettingValue.execute({ + settingName: SettingName.NAMES.ListedAuthorSecrets, + value: JSON.stringify(filteredSecrets), + userUuid: user.uuid, }) + + if (result.isFailed()) { + this.logger.error(`Could not update listed author secrets for user with uuid ${user.uuid}`) + } } } diff --git a/packages/auth/src/Domain/Handler/PaymentsAccountDeletedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/PaymentsAccountDeletedEventHandler.spec.ts deleted file mode 100644 index b92ca03db..000000000 --- a/packages/auth/src/Domain/Handler/PaymentsAccountDeletedEventHandler.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Logger } from 'winston' -import { Result } from '@standardnotes/domain-core' -import { PaymentsAccountDeletedEvent } from '@standardnotes/domain-events' - -import { DeleteAccount } from '../UseCase/DeleteAccount/DeleteAccount' -import { PaymentsAccountDeletedEventHandler } from './PaymentsAccountDeletedEventHandler' - -describe('PaymentsAccountDeletedEventHandler', () => { - let deleteAccountUseCase: DeleteAccount - let logger: Logger - let event: PaymentsAccountDeletedEvent - - const createHandler = () => new PaymentsAccountDeletedEventHandler(deleteAccountUseCase, logger) - - beforeEach(() => { - deleteAccountUseCase = {} as jest.Mocked - deleteAccountUseCase.execute = jest.fn().mockResolvedValue(Result.ok('success')) - - logger = {} as jest.Mocked - logger.error = jest.fn() - - event = { - payload: { - username: 'username', - }, - } as jest.Mocked - }) - - it('should delete account', async () => { - const handler = createHandler() - - await handler.handle(event) - - expect(deleteAccountUseCase.execute).toHaveBeenCalledWith({ - username: 'username', - }) - }) - - it('should log error if delete account fails', async () => { - const handler = createHandler() - - deleteAccountUseCase.execute = jest.fn().mockResolvedValue(Result.fail('error')) - - await handler.handle(event) - - expect(logger.error).toHaveBeenCalledWith('Failed to delete account for user username: error') - }) -}) diff --git a/packages/auth/src/Domain/Handler/PredicateVerificationRequestedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/PredicateVerificationRequestedEventHandler.spec.ts deleted file mode 100644 index 70c9b458d..000000000 --- a/packages/auth/src/Domain/Handler/PredicateVerificationRequestedEventHandler.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import 'reflect-metadata' - -import { - DomainEventPublisherInterface, - DomainEventService, - PredicateVerificationRequestedEvent, - PredicateVerificationRequestedEventPayload, - PredicateVerifiedEvent, -} from '@standardnotes/domain-events' -import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates' -import { Logger } from 'winston' - -import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' -import { VerifyPredicate } from '../UseCase/VerifyPredicate/VerifyPredicate' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' - -import { PredicateVerificationRequestedEventHandler } from './PredicateVerificationRequestedEventHandler' -import { User } from '../User/User' - -describe('PredicateVerificationRequestedEventHandler', () => { - let verifyPredicate: VerifyPredicate - let userRepository: UserRepositoryInterface - let domainEventFactory: DomainEventFactoryInterface - let domainEventPublisher: DomainEventPublisherInterface - let logger: Logger - let event: PredicateVerificationRequestedEvent - - const createHandler = () => - new PredicateVerificationRequestedEventHandler( - verifyPredicate, - userRepository, - domainEventFactory, - domainEventPublisher, - logger, - ) - - beforeEach(() => { - verifyPredicate = {} as jest.Mocked - verifyPredicate.execute = jest - .fn() - .mockReturnValue({ predicateVerificationResult: PredicateVerificationResult.Affirmed }) - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue({ uuid: '1-2-3' } as jest.Mocked) - - domainEventFactory = {} as jest.Mocked - domainEventFactory.createPredicateVerifiedEvent = jest - .fn() - .mockReturnValue({} as jest.Mocked) - - domainEventPublisher = {} as jest.Mocked - domainEventPublisher.publish = jest.fn() - - logger = {} as jest.Mocked - logger.warn = jest.fn() - logger.info = jest.fn() - logger.debug = jest.fn() - - event = {} as jest.Mocked - event.meta = { - correlation: { - userIdentifier: '2-3-4', - userIdentifierType: 'uuid', - }, - origin: DomainEventService.Auth, - } - event.payload = { - predicate: {} as jest.Mocked, - } as jest.Mocked - }) - - it('should verify a predicate by user uuid', async () => { - await createHandler().handle(event) - - expect(verifyPredicate.execute).toHaveBeenCalledWith({ - predicate: event.payload.predicate, - userUuid: '2-3-4', - }) - expect(domainEventPublisher.publish).toHaveBeenCalled() - }) - - it('should verify a predicate by user email', async () => { - event.meta = { - correlation: { - userIdentifier: 'test@test.te', - userIdentifierType: 'email', - }, - origin: DomainEventService.Auth, - } - - await createHandler().handle(event) - - expect(verifyPredicate.execute).toHaveBeenCalledWith({ - predicate: event.payload.predicate, - userUuid: '1-2-3', - }) - expect(domainEventPublisher.publish).toHaveBeenCalled() - }) - - it('should do nothing if username is invalid', async () => { - event.meta = { - correlation: { - userIdentifier: ' ', - userIdentifierType: 'email', - }, - origin: DomainEventService.Auth, - } - - await createHandler().handle(event) - - expect(verifyPredicate.execute).not.toHaveBeenCalled() - expect(domainEventPublisher.publish).not.toHaveBeenCalled() - }) - - it('should mark a predicate verification with undetermined result if user is missing', async () => { - event.meta = { - correlation: { - userIdentifier: 'test@test.te', - userIdentifierType: 'email', - }, - origin: DomainEventService.Auth, - } - - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(verifyPredicate.execute).not.toHaveBeenCalled() - expect(domainEventPublisher.publish).toHaveBeenCalled() - }) -}) diff --git a/packages/auth/src/Domain/Handler/SharedSubscriptionInvitationCreatedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SharedSubscriptionInvitationCreatedEventHandler.spec.ts deleted file mode 100644 index 9068f0659..000000000 --- a/packages/auth/src/Domain/Handler/SharedSubscriptionInvitationCreatedEventHandler.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import 'reflect-metadata' - -import { SharedSubscriptionInvitationCreatedEvent } from '@standardnotes/domain-events' - -import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType' -import { AcceptSharedSubscriptionInvitation } from '../UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation' - -import { SharedSubscriptionInvitationCreatedEventHandler } from './SharedSubscriptionInvitationCreatedEventHandler' - -describe('SharedSubscriptionInvitationCreatedEventHandler', () => { - let acceptSharedSubscriptionInvitation: AcceptSharedSubscriptionInvitation - - const createHandler = () => new SharedSubscriptionInvitationCreatedEventHandler(acceptSharedSubscriptionInvitation) - - beforeEach(() => { - acceptSharedSubscriptionInvitation = {} as jest.Mocked - acceptSharedSubscriptionInvitation.execute = jest.fn() - }) - - it('should accept automatically invitation for hash invitees', async () => { - const event = { - payload: { - inviteeIdentifierType: InviteeIdentifierType.Hash, - sharedSubscriptionInvitationUuid: '1-2-3', - }, - } as jest.Mocked - - await createHandler().handle(event) - - expect(acceptSharedSubscriptionInvitation.execute).toHaveBeenCalled() - }) - - it('should not accept automatically invitation for email invitees', async () => { - const event = { - payload: { - inviteeIdentifierType: InviteeIdentifierType.Email, - sharedSubscriptionInvitationUuid: '1-2-3', - }, - } as jest.Mocked - - await createHandler().handle(event) - - expect(acceptSharedSubscriptionInvitation.execute).not.toHaveBeenCalled() - }) -}) diff --git a/packages/auth/src/Domain/Handler/SubscriptionCancelledEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionCancelledEventHandler.spec.ts deleted file mode 100644 index d926ebd95..000000000 --- a/packages/auth/src/Domain/Handler/SubscriptionCancelledEventHandler.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { SubscriptionCancelledEvent } from '@standardnotes/domain-events' - -import * as dayjs from 'dayjs' - -import { SubscriptionCancelledEventHandler } from './SubscriptionCancelledEventHandler' -import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' - -describe('SubscriptionCancelledEventHandler', () => { - let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface - let event: SubscriptionCancelledEvent - let timestamp: number - - const createHandler = () => - new SubscriptionCancelledEventHandler(userSubscriptionRepository, offlineUserSubscriptionRepository) - - beforeEach(() => { - userSubscriptionRepository = {} as jest.Mocked - userSubscriptionRepository.updateCancelled = jest.fn() - - offlineUserSubscriptionRepository = {} as jest.Mocked - offlineUserSubscriptionRepository.updateCancelled = jest.fn() - - timestamp = dayjs.utc().valueOf() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.ProPlan, - timestamp, - offline: false, - replaced: false, - subscriptionCreatedAt: 1, - subscriptionEndsAt: 2, - subscriptionUpdatedAt: 2, - lastPayedAt: 1, - userExistingSubscriptionsCount: 1, - billingFrequency: 1, - payAmount: 12.99, - } - }) - - it('should update subscription cancelled', async () => { - event.payload.timestamp = 1642395451516000 - await createHandler().handle(event) - - expect(userSubscriptionRepository.updateCancelled).toHaveBeenCalledWith(1, true, 1642395451516000) - }) - - it('should update offline subscription cancelled', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(offlineUserSubscriptionRepository.updateCancelled).toHaveBeenCalledWith(1, true, timestamp) - }) -}) diff --git a/packages/auth/src/Domain/Handler/SubscriptionExpiredEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionExpiredEventHandler.spec.ts deleted file mode 100644 index 6d1a50c55..000000000 --- a/packages/auth/src/Domain/Handler/SubscriptionExpiredEventHandler.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { RoleName } from '@standardnotes/domain-core' -import { SubscriptionExpiredEvent } from '@standardnotes/domain-events' -import { Logger } from 'winston' - -import * as dayjs from 'dayjs' - -import { User } from '../User/User' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { SubscriptionExpiredEventHandler } from './SubscriptionExpiredEventHandler' -import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { RoleServiceInterface } from '../Role/RoleServiceInterface' -import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' -import { UserSubscription } from '../Subscription/UserSubscription' - -describe('SubscriptionExpiredEventHandler', () => { - let userRepository: UserRepositoryInterface - let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface - let roleService: RoleServiceInterface - let logger: Logger - let user: User - let event: SubscriptionExpiredEvent - let timestamp: number - - const createHandler = () => - new SubscriptionExpiredEventHandler( - userRepository, - userSubscriptionRepository, - offlineUserSubscriptionRepository, - roleService, - logger, - ) - - beforeEach(() => { - user = { - uuid: '123', - email: 'test@test.com', - roles: Promise.resolve([ - { - name: RoleName.NAMES.ProUser, - }, - ]), - } as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - userRepository.save = jest.fn().mockReturnValue(user) - - userSubscriptionRepository = {} as jest.Mocked - userSubscriptionRepository.updateEndsAt = jest.fn() - userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13) - userSubscriptionRepository.findBySubscriptionId = jest - .fn() - .mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked]) - - offlineUserSubscriptionRepository = {} as jest.Mocked - offlineUserSubscriptionRepository.updateEndsAt = jest.fn() - - roleService = {} as jest.Mocked - roleService.removeUserRoleBasedOnSubscription = jest.fn() - - timestamp = dayjs.utc().valueOf() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.PlusPlan, - timestamp, - offline: false, - totalActiveSubscriptionsCount: 123, - userExistingSubscriptionsCount: 2, - billingFrequency: 1, - payAmount: 12.99, - } - - logger = {} as jest.Mocked - logger.info = jest.fn() - logger.warn = jest.fn() - }) - - it('should update the user role', async () => { - await createHandler().handle(event) - - expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan) - }) - - it('should update subscription ends at', async () => { - await createHandler().handle(event) - - expect(userSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, timestamp, timestamp) - }) - - it('should update offline subscription ends at', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(offlineUserSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, timestamp, timestamp) - }) - - it('should not do anything if no user is found for specified email', async () => { - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled() - }) - - it('should not do anything if username is invalid', async () => { - event.payload.userEmail = ' ' - - await createHandler().handle(event) - - expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled() - }) -}) diff --git a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts deleted file mode 100644 index cbadabd14..000000000 --- a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts +++ /dev/null @@ -1,177 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { RoleName } from '@standardnotes/domain-core' -import { SubscriptionPurchasedEvent } from '@standardnotes/domain-events' -import { Logger } from 'winston' - -import * as dayjs from 'dayjs' - -import { RoleServiceInterface } from '../Role/RoleServiceInterface' -import { User } from '../User/User' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { SubscriptionPurchasedEventHandler } from './SubscriptionPurchasedEventHandler' -import { UserSubscription } from '../Subscription/UserSubscription' -import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' -import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription' -import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' -import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' - -describe('SubscriptionPurchasedEventHandler', () => { - let userRepository: UserRepositoryInterface - let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let offlineUserSubscription: OfflineUserSubscription - let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface - let roleService: RoleServiceInterface - let logger: Logger - let user: User - let subscription: UserSubscription - let event: SubscriptionPurchasedEvent - let subscriptionExpiresAt: number - let subscriptionSettingService: SubscriptionSettingServiceInterface - let timestamp: number - - const createHandler = () => - new SubscriptionPurchasedEventHandler( - userRepository, - userSubscriptionRepository, - offlineUserSubscriptionRepository, - roleService, - subscriptionSettingService, - logger, - ) - - beforeEach(() => { - user = { - uuid: '123', - email: 'test@test.com', - roles: Promise.resolve([ - { - name: RoleName.NAMES.CoreUser, - }, - ]), - } as jest.Mocked - subscription = { - subscriptionType: UserSubscriptionType.Regular, - } as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - userRepository.save = jest.fn().mockReturnValue(user) - - userSubscriptionRepository = {} as jest.Mocked - userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(0) - userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13) - userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription) - - offlineUserSubscription = {} as jest.Mocked - - offlineUserSubscriptionRepository = {} as jest.Mocked - offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(offlineUserSubscription) - offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription) - - roleService = {} as jest.Mocked - roleService.addUserRoleBasedOnSubscription = jest.fn() - roleService.setOfflineUserRole = jest.fn() - - subscriptionExpiresAt = timestamp + 365 * 1000 - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.ProPlan, - subscriptionExpiresAt, - timestamp: dayjs.utc().valueOf(), - offline: false, - discountCode: null, - limitedDiscountPurchased: false, - newSubscriber: true, - totalActiveSubscriptionsCount: 123, - userRegisteredAt: dayjs.utc().valueOf() - 23, - billingFrequency: 12, - payAmount: 29.99, - } - - subscriptionSettingService = {} as jest.Mocked - subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn() - - logger = {} as jest.Mocked - logger.info = jest.fn() - logger.warn = jest.fn() - }) - - it('should update the user role', async () => { - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan) - }) - - it('should update user default settings', async () => { - await createHandler().handle(event) - - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( - subscription, - ) - }) - - it('should update the offline user role', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(roleService.setOfflineUserRole).toHaveBeenCalledWith(offlineUserSubscription) - }) - - it('should create subscription', async () => { - await createHandler().handle(event) - - subscription.planName = SubscriptionName.ProPlan - subscription.endsAt = subscriptionExpiresAt - subscription.subscriptionId = 1 - subscription.user = Promise.resolve(user) - - expect(userSubscriptionRepository.save).toHaveBeenCalledWith({ - ...subscription, - createdAt: expect.any(Number), - updatedAt: expect.any(Number), - cancelled: false, - }) - }) - - it('should create an offline subscription', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(offlineUserSubscriptionRepository.save).toHaveBeenCalledWith({ - endsAt: subscriptionExpiresAt, - subscriptionId: 1, - planName: 'PRO_PLAN', - email: 'test@test.com', - createdAt: expect.any(Number), - updatedAt: expect.any(Number), - cancelled: false, - }) - }) - - it('should not do anything if no user is found for specified email', async () => { - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.save).not.toHaveBeenCalled() - }) - - it('should not do anything if username is invalid', async () => { - event.payload.userEmail = ' ' - - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.save).not.toHaveBeenCalled() - }) -}) diff --git a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts b/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts index 7b7fb8090..68453462b 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts @@ -1,8 +1,7 @@ import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events' -import { inject, injectable } from 'inversify' +import { Username } from '@standardnotes/domain-core' import { Logger } from 'winston' -import TYPES from '../../Bootstrap/Types' import { RoleServiceInterface } from '../Role/RoleServiceInterface' import { User } from '../User/User' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' @@ -11,21 +10,16 @@ import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscri import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription' import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' -import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' -import { Username } from '@standardnotes/domain-core' +import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings' -@injectable() export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_UserSubscriptionRepository) + private userRepository: UserRepositoryInterface, private userSubscriptionRepository: UserSubscriptionRepositoryInterface, - @inject(TYPES.Auth_OfflineUserSubscriptionRepository) + private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings, private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface, - @inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface, - @inject(TYPES.Auth_SubscriptionSettingService) - private subscriptionSettingService: SubscriptionSettingServiceInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, + private roleService: RoleServiceInterface, + private logger: Logger, ) {} async handle(event: SubscriptionPurchasedEvent): Promise { @@ -66,7 +60,15 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte await this.addUserRole(user, event.payload.subscriptionName) - await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription) + const result = await this.applyDefaultSubscriptionSettings.execute({ + userSubscriptionUuid: userSubscription.uuid, + userUuid: user.uuid, + subscriptionPlanName: event.payload.subscriptionName, + }) + + if (result.isFailed()) { + this.logger.error(`Could not apply default subscription settings for user ${user.uuid}: ${result.getError()}`) + } } private async addUserRole(user: User, subscriptionName: string): Promise { diff --git a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.spec.ts deleted file mode 100644 index fc9f6195d..000000000 --- a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.spec.ts +++ /dev/null @@ -1,161 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { RoleName } from '@standardnotes/domain-core' -import { SubscriptionReassignedEvent } from '@standardnotes/domain-events' -import { Logger } from 'winston' - -import * as dayjs from 'dayjs' - -import { RoleServiceInterface } from '../Role/RoleServiceInterface' -import { User } from '../User/User' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { SubscriptionReassignedEventHandler } from './SubscriptionReassignedEventHandler' -import { UserSubscription } from '../Subscription/UserSubscription' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' -import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' -import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' - -describe('SubscriptionReassignedEventHandler', () => { - let userRepository: UserRepositoryInterface - let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let roleService: RoleServiceInterface - let logger: Logger - let user: User - let subscription: UserSubscription - let event: SubscriptionReassignedEvent - let subscriptionExpiresAt: number - let timestamp: number - let settingService: SettingServiceInterface - let subscriptionSettingService: SubscriptionSettingServiceInterface - - const createHandler = () => - new SubscriptionReassignedEventHandler( - userRepository, - userSubscriptionRepository, - roleService, - settingService, - subscriptionSettingService, - logger, - ) - - beforeEach(() => { - user = { - uuid: '123', - email: 'test@test.com', - roles: Promise.resolve([ - { - name: RoleName.NAMES.CoreUser, - }, - ]), - } as jest.Mocked - subscription = { - subscriptionType: UserSubscriptionType.Regular, - } as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - userRepository.save = jest.fn().mockReturnValue(user) - - userSubscriptionRepository = {} as jest.Mocked - userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription) - - roleService = {} as jest.Mocked - roleService.addUserRoleBasedOnSubscription = jest.fn() - - subscriptionExpiresAt = timestamp + 365 * 1000 - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - subscriptionId: 1, - offline: false, - extensionKey: 'abc123', - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.ProPlan, - subscriptionExpiresAt, - timestamp: dayjs.utc().valueOf(), - } - - settingService = {} as jest.Mocked - settingService.createOrReplace = jest.fn() - - subscriptionSettingService = {} as jest.Mocked - subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn() - - logger = {} as jest.Mocked - logger.info = jest.fn() - logger.warn = jest.fn() - }) - - it('should update user default settings', async () => { - await createHandler().handle(event) - - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( - subscription, - ) - }) - - it('should update the user role', async () => { - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan) - }) - - it('should create subscription', async () => { - await createHandler().handle(event) - - subscription.planName = SubscriptionName.ProPlan - subscription.endsAt = subscriptionExpiresAt - subscription.subscriptionId = 1 - subscription.user = Promise.resolve(user) - - expect(userSubscriptionRepository.save).toHaveBeenCalledWith({ - ...subscription, - createdAt: expect.any(Number), - updatedAt: expect.any(Number), - cancelled: false, - }) - }) - - it('should create an extension key setting for the user', async () => { - await createHandler().handle(event) - - expect(settingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'EXTENSION_KEY', - serverEncryptionVersion: 1, - unencryptedValue: 'abc123', - sensitive: true, - }, - user: { - uuid: '123', - email: 'test@test.com', - roles: Promise.resolve([ - { - name: RoleName.NAMES.CoreUser, - }, - ]), - }, - }) - }) - - it('should not do anything if no user is found for specified email', async () => { - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.save).not.toHaveBeenCalled() - }) - - it('should not do anything if username is invalid', async () => { - event.payload.userEmail = ' ' - - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.save).not.toHaveBeenCalled() - }) -}) diff --git a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts b/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts index 07ac0020c..a644478c4 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts @@ -1,31 +1,25 @@ import { DomainEventHandlerInterface, SubscriptionReassignedEvent } from '@standardnotes/domain-events' -import { inject, injectable } from 'inversify' import { Logger } from 'winston' -import TYPES from '../../Bootstrap/Types' import { RoleServiceInterface } from '../Role/RoleServiceInterface' import { User } from '../User/User' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' import { UserSubscription } from '../Subscription/UserSubscription' import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { SettingName } from '@standardnotes/settings' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' -import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' import { Username } from '@standardnotes/domain-core' +import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings' +import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue' -@injectable() export class SubscriptionReassignedEventHandler implements DomainEventHandlerInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_UserSubscriptionRepository) + private userRepository: UserRepositoryInterface, private userSubscriptionRepository: UserSubscriptionRepositoryInterface, - @inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface, - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_SubscriptionSettingService) - private subscriptionSettingService: SubscriptionSettingServiceInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, + private roleService: RoleServiceInterface, + private logger: Logger, + private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings, + private setSettingValue: SetSettingValue, ) {} async handle(event: SubscriptionReassignedEvent): Promise { @@ -53,17 +47,25 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt await this.addUserRole(user, event.payload.subscriptionName) - await this.settingService.createOrReplace({ - user, - props: { - name: SettingName.NAMES.ExtensionKey, - unencryptedValue: event.payload.extensionKey, - serverEncryptionVersion: EncryptionVersion.Default, - sensitive: true, - }, + const result = await this.setSettingValue.execute({ + userUuid: user.uuid, + settingName: SettingName.NAMES.ExtensionKey, + value: event.payload.extensionKey, }) + if (result.isFailed()) { + this.logger.error(`Could not set extension key for user ${user.uuid}`) + } - await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription) + const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({ + subscriptionPlanName: event.payload.subscriptionName, + userUuid: user.uuid, + userSubscriptionUuid: userSubscription.uuid, + }) + if (applyingSettingsResult.isFailed()) { + this.logger.error( + `Could not apply default subscription settings for user ${user.uuid}: ${applyingSettingsResult.getError()}`, + ) + } } private async addUserRole(user: User, subscriptionName: string): Promise { diff --git a/packages/auth/src/Domain/Handler/SubscriptionRefundedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionRefundedEventHandler.spec.ts deleted file mode 100644 index 8cb77177d..000000000 --- a/packages/auth/src/Domain/Handler/SubscriptionRefundedEventHandler.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { RoleName } from '@standardnotes/domain-core' -import { SubscriptionRefundedEvent } from '@standardnotes/domain-events' -import { Logger } from 'winston' - -import * as dayjs from 'dayjs' - -import { User } from '../User/User' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { SubscriptionRefundedEventHandler } from './SubscriptionRefundedEventHandler' -import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { RoleServiceInterface } from '../Role/RoleServiceInterface' -import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' -import { UserSubscription } from '../Subscription/UserSubscription' - -describe('SubscriptionRefundedEventHandler', () => { - let userRepository: UserRepositoryInterface - let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface - let roleService: RoleServiceInterface - let logger: Logger - let user: User - let event: SubscriptionRefundedEvent - let timestamp: number - - const createHandler = () => - new SubscriptionRefundedEventHandler( - userRepository, - userSubscriptionRepository, - offlineUserSubscriptionRepository, - roleService, - logger, - ) - - beforeEach(() => { - user = { - uuid: '123', - email: 'test@test.com', - roles: Promise.resolve([ - { - name: RoleName.NAMES.ProUser, - }, - ]), - } as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - userRepository.save = jest.fn().mockReturnValue(user) - - userSubscriptionRepository = {} as jest.Mocked - userSubscriptionRepository.updateEndsAt = jest.fn() - userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(1) - userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13) - userSubscriptionRepository.findBySubscriptionId = jest - .fn() - .mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked]) - - offlineUserSubscriptionRepository = {} as jest.Mocked - offlineUserSubscriptionRepository.updateEndsAt = jest.fn() - - roleService = {} as jest.Mocked - roleService.removeUserRoleBasedOnSubscription = jest.fn() - - timestamp = dayjs.utc().valueOf() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.PlusPlan, - timestamp, - offline: false, - userExistingSubscriptionsCount: 3, - totalActiveSubscriptionsCount: 1, - billingFrequency: 1, - payAmount: 12.99, - } - - logger = {} as jest.Mocked - logger.info = jest.fn() - logger.warn = jest.fn() - }) - - it('should update the user role', async () => { - await createHandler().handle(event) - - expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan) - }) - - it('should update subscription ends at', async () => { - await createHandler().handle(event) - - expect(userSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, timestamp, timestamp) - }) - - it('should update offline subscription ends at', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(offlineUserSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, timestamp, timestamp) - }) - - it('should not do anything if no user is found for specified email', async () => { - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled() - }) - - it('should not do anything if username is invalid', async () => { - event.payload.userEmail = ' ' - - await createHandler().handle(event) - - expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled() - }) -}) diff --git a/packages/auth/src/Domain/Handler/SubscriptionRenewedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionRenewedEventHandler.spec.ts deleted file mode 100644 index 4f638f8a7..000000000 --- a/packages/auth/src/Domain/Handler/SubscriptionRenewedEventHandler.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { RoleName } from '@standardnotes/domain-core' -import { SubscriptionRenewedEvent } from '@standardnotes/domain-events' -import * as dayjs from 'dayjs' -import { Logger } from 'winston' - -import { SubscriptionRenewedEventHandler } from './SubscriptionRenewedEventHandler' -import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' -import { User } from '../User/User' -import { UserSubscription } from '../Subscription/UserSubscription' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription' -import { RoleServiceInterface } from '../Role/RoleServiceInterface' - -describe('SubscriptionRenewedEventHandler', () => { - let userRepository: UserRepositoryInterface - let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let offlineUserSubscription: OfflineUserSubscription - let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface - let roleService: RoleServiceInterface - let logger: Logger - let user: User - let subscription: UserSubscription - let event: SubscriptionRenewedEvent - let subscriptionExpiresAt: number - let timestamp: number - - const createHandler = () => - new SubscriptionRenewedEventHandler( - userRepository, - userSubscriptionRepository, - offlineUserSubscriptionRepository, - roleService, - logger, - ) - - beforeEach(() => { - user = { - uuid: '123', - email: 'test@test.com', - roles: Promise.resolve([ - { - name: RoleName.NAMES.CoreUser, - }, - ]), - } as jest.Mocked - subscription = {} as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - userRepository.save = jest.fn().mockReturnValue(user) - - userSubscriptionRepository = {} as jest.Mocked - userSubscriptionRepository.updateEndsAt = jest.fn() - userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription) - userSubscriptionRepository.findBySubscriptionId = jest - .fn() - .mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked]) - - offlineUserSubscription = {} as jest.Mocked - - offlineUserSubscriptionRepository = {} as jest.Mocked - offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(offlineUserSubscription) - offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription) - - roleService = {} as jest.Mocked - roleService.addUserRoleBasedOnSubscription = jest.fn() - roleService.setOfflineUserRole = jest.fn() - - timestamp = dayjs.utc().valueOf() - subscriptionExpiresAt = dayjs.utc().valueOf() + 365 * 1000 - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.ProPlan, - subscriptionExpiresAt, - timestamp, - offline: false, - billingFrequency: 1, - payAmount: 12.99, - } - - logger = {} as jest.Mocked - logger.warn = jest.fn() - }) - - it('should update subscription ends at', async () => { - await createHandler().handle(event) - - expect(userSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, subscriptionExpiresAt, timestamp) - }) - - it('should update offline subscription ends at', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(offlineUserSubscriptionRepository.save).toHaveBeenCalledWith(offlineUserSubscription) - }) - - it('should update the user role', async () => { - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan) - }) - - it('should update the offline user role', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(roleService.setOfflineUserRole).toHaveBeenCalledWith(offlineUserSubscription) - }) - - it('should not do anything if no user is found for specified email', async () => { - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.save).not.toHaveBeenCalled() - }) - - it('should not do anything if username is invalid', async () => { - event.payload.userEmail = ' ' - - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.save).not.toHaveBeenCalled() - }) - - it('should not do anything if no offline subscription is found for specified id', async () => { - event.payload.offline = true - - offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.save).not.toHaveBeenCalled() - }) -}) diff --git a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.spec.ts deleted file mode 100644 index 8a2bb6d9b..000000000 --- a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.spec.ts +++ /dev/null @@ -1,258 +0,0 @@ -import 'reflect-metadata' - -import { ContentDecoderInterface, SubscriptionName } from '@standardnotes/common' -import { RoleName } from '@standardnotes/domain-core' -import { SubscriptionSyncRequestedEvent } from '@standardnotes/domain-events' -import { Logger } from 'winston' - -import * as dayjs from 'dayjs' - -import { RoleServiceInterface } from '../Role/RoleServiceInterface' -import { User } from '../User/User' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { SubscriptionSyncRequestedEventHandler } from './SubscriptionSyncRequestedEventHandler' -import { UserSubscription } from '../Subscription/UserSubscription' -import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' -import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' -import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface' -import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' -import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' - -describe('SubscriptionSyncRequestedEventHandler', () => { - let userRepository: UserRepositoryInterface - let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let offlineUserSubscription: OfflineUserSubscription - let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface - let roleService: RoleServiceInterface - let logger: Logger - let user: User - let subscription: UserSubscription - let event: SubscriptionSyncRequestedEvent - let subscriptionExpiresAt: number - let settingService: SettingServiceInterface - let subscriptionSettingService: SubscriptionSettingServiceInterface - let timestamp: number - let offlineSettingService: OfflineSettingServiceInterface - let contentDecoder: ContentDecoderInterface - - const createHandler = () => - new SubscriptionSyncRequestedEventHandler( - userRepository, - userSubscriptionRepository, - offlineUserSubscriptionRepository, - roleService, - settingService, - subscriptionSettingService, - offlineSettingService, - contentDecoder, - logger, - ) - - beforeEach(() => { - user = { - uuid: '123', - email: 'test@test.com', - roles: Promise.resolve([ - { - name: RoleName.NAMES.CoreUser, - }, - ]), - } as jest.Mocked - subscription = { - subscriptionType: UserSubscriptionType.Regular, - } as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - userRepository.save = jest.fn().mockReturnValue(user) - - userSubscriptionRepository = {} as jest.Mocked - userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription) - userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([]) - - offlineUserSubscription = {} as jest.Mocked - - offlineUserSubscriptionRepository = {} as jest.Mocked - offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(null) - offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription) - - offlineSettingService = {} as jest.Mocked - offlineSettingService.createOrUpdate = jest.fn() - - contentDecoder = {} as jest.Mocked - contentDecoder.decode = jest.fn().mockReturnValue({ - featuresUrl: 'http://features-url', - extensionKey: 'key', - }) - - roleService = {} as jest.Mocked - roleService.addUserRoleBasedOnSubscription = jest.fn() - roleService.setOfflineUserRole = jest.fn() - - subscriptionExpiresAt = timestamp + 365 * 1000 - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.ProPlan, - subscriptionExpiresAt, - timestamp: dayjs.utc().valueOf(), - offline: false, - extensionKey: 'abc123', - offlineFeaturesToken: 'test', - canceled: false, - } - - settingService = {} as jest.Mocked - settingService.createOrReplace = jest.fn() - - subscriptionSettingService = {} as jest.Mocked - subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn() - - logger = {} as jest.Mocked - logger.info = jest.fn() - logger.warn = jest.fn() - }) - - it('should update the user role', async () => { - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan) - }) - - it('should update user default settings', async () => { - await createHandler().handle(event) - - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( - subscription, - ) - - expect(settingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'EXTENSION_KEY', - serverEncryptionVersion: 1, - unencryptedValue: 'abc123', - sensitive: true, - }, - user: { - email: 'test@test.com', - roles: Promise.resolve([ - { - name: RoleName.NAMES.CoreUser, - }, - ]), - uuid: '123', - }, - }) - }) - - it('should update the offline user role', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(roleService.setOfflineUserRole).toHaveBeenCalledWith(offlineUserSubscription) - }) - - it('should not update the offline user features token if it is not possible to decode the extension key', async () => { - event.payload.offline = true - - contentDecoder.decode = jest.fn().mockReturnValue({}) - - await createHandler().handle(event) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - }) - - it('should create subscription', async () => { - await createHandler().handle(event) - - subscription.planName = SubscriptionName.ProPlan - subscription.endsAt = subscriptionExpiresAt - subscription.subscriptionId = 1 - subscription.user = Promise.resolve(user) - - expect(userSubscriptionRepository.save).toHaveBeenCalledWith({ - ...subscription, - createdAt: expect.any(Number), - updatedAt: expect.any(Number), - cancelled: false, - }) - }) - - it('should update an existing subscription', async () => { - userSubscriptionRepository.findBySubscriptionIdAndType = jest - .fn() - .mockReturnValue([{} as jest.Mocked]) - await createHandler().handle(event) - - subscription.planName = SubscriptionName.ProPlan - subscription.endsAt = subscriptionExpiresAt - subscription.subscriptionId = 1 - subscription.user = Promise.resolve(user) - - expect(userSubscriptionRepository.save).toHaveBeenCalledWith({ - ...subscription, - createdAt: expect.any(Number), - updatedAt: expect.any(Number), - cancelled: false, - }) - }) - - it('should create an offline subscription', async () => { - event.payload.offline = true - - await createHandler().handle(event) - - expect(offlineUserSubscriptionRepository.save).toHaveBeenCalledWith({ - endsAt: subscriptionExpiresAt, - subscriptionId: 1, - planName: 'PRO_PLAN', - email: 'test@test.com', - createdAt: expect.any(Number), - updatedAt: expect.any(Number), - cancelled: false, - }) - }) - - it('should update an offline subscription', async () => { - offlineUserSubscriptionRepository.findOneBySubscriptionId = jest - .fn() - .mockReturnValue({} as jest.Mocked) - event.payload.offline = true - - await createHandler().handle(event) - - expect(offlineUserSubscriptionRepository.save).toHaveBeenCalledWith({ - endsAt: subscriptionExpiresAt, - subscriptionId: 1, - planName: 'PRO_PLAN', - email: 'test@test.com', - createdAt: expect.any(Number), - updatedAt: expect.any(Number), - cancelled: false, - }) - }) - - it('should not do anything if no user is found for specified email', async () => { - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.save).not.toHaveBeenCalled() - }) - - it('should not do anything if username is invalid', async () => { - event.payload.userEmail = ' ' - - await createHandler().handle(event) - - expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(userSubscriptionRepository.save).not.toHaveBeenCalled() - }) -}) diff --git a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts b/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts index 8a4b1ec60..fab59b1af 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts @@ -1,9 +1,10 @@ import { OfflineFeaturesTokenData } from '@standardnotes/security' +import { SettingName } from '@standardnotes/settings' +import { Username } from '@standardnotes/domain-core' +import { ContentDecoderInterface } from '@standardnotes/common' import { DomainEventHandlerInterface, SubscriptionSyncRequestedEvent } from '@standardnotes/domain-events' -import { inject, injectable } from 'inversify' import { Logger } from 'winston' -import TYPES from '../../Bootstrap/Types' import { RoleServiceInterface } from '../Role/RoleServiceInterface' import { User } from '../User/User' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' @@ -11,31 +12,23 @@ import { UserSubscription } from '../Subscription/UserSubscription' import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription' import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface' -import { ContentDecoderInterface } from '@standardnotes/common' import { OfflineSettingName } from '../Setting/OfflineSettingName' -import { SettingName } from '@standardnotes/settings' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' -import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' -import { Username } from '@standardnotes/domain-core' +import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings' +import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue' -@injectable() export class SubscriptionSyncRequestedEventHandler implements DomainEventHandlerInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_UserSubscriptionRepository) + private userRepository: UserRepositoryInterface, private userSubscriptionRepository: UserSubscriptionRepositoryInterface, - @inject(TYPES.Auth_OfflineUserSubscriptionRepository) private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface, - @inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface, - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_SubscriptionSettingService) - private subscriptionSettingService: SubscriptionSettingServiceInterface, - @inject(TYPES.Auth_OfflineSettingService) private offlineSettingService: OfflineSettingServiceInterface, - @inject(TYPES.Auth_ContenDecoder) private contentDecoder: ContentDecoderInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, + private roleService: RoleServiceInterface, + private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings, + private setSettingValue: SetSettingValue, + private offlineSettingService: OfflineSettingServiceInterface, + private contentDecoder: ContentDecoderInterface, + private logger: Logger, ) {} async handle(event: SubscriptionSyncRequestedEvent): Promise { @@ -95,17 +88,26 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName) - await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription) - - await this.settingService.createOrReplace({ - user, - props: { - name: SettingName.NAMES.ExtensionKey, - unencryptedValue: event.payload.extensionKey, - serverEncryptionVersion: EncryptionVersion.Default, - sensitive: true, - }, + const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({ + userSubscriptionUuid: userSubscription.uuid, + userUuid: user.uuid, + subscriptionPlanName: event.payload.subscriptionName, }) + if (applyingSettingsResult.isFailed()) { + this.logger.error( + `Could not apply default subscription settings for user ${user.uuid}: ${applyingSettingsResult.getError()}`, + ) + } + + const result = await this.setSettingValue.execute({ + userUuid: user.uuid, + settingName: SettingName.NAMES.ExtensionKey, + value: event.payload.subscriptionName, + }) + + if (result.isFailed()) { + this.logger.error(`Could not set extension key for user ${user.uuid}`) + } } private async createOrUpdateSubscription( diff --git a/packages/auth/src/Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler.spec.ts b/packages/auth/src/Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler.spec.ts deleted file mode 100644 index c7ce0b103..000000000 --- a/packages/auth/src/Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import 'reflect-metadata' - -import { UserDisabledSessionUserAgentLoggingEvent } from '@standardnotes/domain-events' -import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface' - -import { UserDisabledSessionUserAgentLoggingEventHandler } from './UserDisabledSessionUserAgentLoggingEventHandler' -import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface' - -describe('UserDisabledSessionUserAgentLoggingEventHandler', () => { - let sessionRepository: SessionRepositoryInterface - let revokedSessionRepository: RevokedSessionRepositoryInterface - let event: UserDisabledSessionUserAgentLoggingEvent - - const createHandler = () => - new UserDisabledSessionUserAgentLoggingEventHandler(sessionRepository, revokedSessionRepository) - - beforeEach(() => { - sessionRepository = {} as jest.Mocked - sessionRepository.clearUserAgentByUserUuid = jest.fn() - - revokedSessionRepository = {} as jest.Mocked - revokedSessionRepository.clearUserAgentByUserUuid = jest.fn() - - event = {} as jest.Mocked - event.payload = { - userUuid: '1-2-3', - email: 'test@test.te', - } - }) - - it('should clear all user agent info from all user sessions', async () => { - await createHandler().handle(event) - - expect(sessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3') - expect(revokedSessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3') - }) -}) diff --git a/packages/auth/src/Domain/Session/SessionService.spec.ts b/packages/auth/src/Domain/Session/SessionService.spec.ts index d97aaf629..b5fbf6daf 100644 --- a/packages/auth/src/Domain/Session/SessionService.spec.ts +++ b/packages/auth/src/Domain/Session/SessionService.spec.ts @@ -11,7 +11,6 @@ import { EphemeralSessionRepositoryInterface } from './EphemeralSessionRepositor import { EphemeralSession } from './EphemeralSession' import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface' import { RevokedSession } from './RevokedSession' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { LogSessionUserAgentOption } from '@standardnotes/settings' import { Setting } from '../Setting/Setting' import { CryptoNode } from '@standardnotes/sncrypto-node' @@ -19,6 +18,7 @@ import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscri import { TraceSession } from '../UseCase/TraceSession/TraceSession' import { UserSubscription } from '../Subscription/UserSubscription' import { Result } from '@standardnotes/domain-core' +import { GetSetting } from '../UseCase/GetSetting/GetSetting' describe('SessionService', () => { let sessionRepository: SessionRepositoryInterface @@ -27,7 +27,7 @@ describe('SessionService', () => { let existingSession: Session let existingEphemeralSession: EphemeralSession let revokedSession: RevokedSession - let settingService: SettingServiceInterface + let getSetting: GetSetting let deviceDetector: UAParser let timer: TimerInterface let logger: winston.Logger @@ -46,11 +46,11 @@ describe('SessionService', () => { logger, 123, 234, - settingService, cryptoNode, traceSession, userSubscriptionRepository, readonlyUsers, + getSetting, ) beforeEach(() => { @@ -72,8 +72,8 @@ describe('SessionService', () => { sessionRepository.insert = jest.fn() sessionRepository.update = jest.fn() - settingService = {} as jest.Mocked - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) + getSetting = {} as jest.Mocked + getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) ephemeralSessionRepository = {} as jest.Mocked ephemeralSessionRepository.insert = jest.fn() @@ -240,9 +240,12 @@ describe('SessionService', () => { const user = {} as jest.Mocked user.uuid = '123' - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({ - value: LogSessionUserAgentOption.Disabled, - } as jest.Mocked) + getSetting.execute = jest.fn().mockReturnValue( + Result.ok({ + setting: {} as jest.Mocked, + decryptedValue: LogSessionUserAgentOption.Disabled, + }), + ) const result = await createService().createNewSessionForUser({ user, diff --git a/packages/auth/src/Domain/Session/SessionService.ts b/packages/auth/src/Domain/Session/SessionService.ts index afad94c73..73758e846 100644 --- a/packages/auth/src/Domain/Session/SessionService.ts +++ b/packages/auth/src/Domain/Session/SessionService.ts @@ -1,15 +1,13 @@ import * as crypto from 'crypto' import * as dayjs from 'dayjs' -import { UAParser } from 'ua-parser-js' -import { inject, injectable } from 'inversify' import { v4 as uuidv4 } from 'uuid' +import { UAParserInstance } from 'ua-parser-js' import { TimerInterface } from '@standardnotes/time' import { Logger } from 'winston' import { LogSessionUserAgentOption, SettingName } from '@standardnotes/settings' import { SessionBody } from '@standardnotes/responses' import { CryptoNode } from '@standardnotes/sncrypto-node' -import TYPES from '../../Bootstrap/Types' import { Session } from './Session' import { SessionRepositoryInterface } from './SessionRepositoryInterface' import { SessionServiceInterface } from './SessionServiceInterface' @@ -18,30 +16,27 @@ import { EphemeralSessionRepositoryInterface } from './EphemeralSessionRepositor import { EphemeralSession } from './EphemeralSession' import { RevokedSession } from './RevokedSession' import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { TraceSession } from '../UseCase/TraceSession/TraceSession' import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' +import { GetSetting } from '../UseCase/GetSetting/GetSetting' -@injectable() export class SessionService implements SessionServiceInterface { static readonly SESSION_TOKEN_VERSION = 1 constructor( - @inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface, - @inject(TYPES.Auth_EphemeralSessionRepository) + private sessionRepository: SessionRepositoryInterface, private ephemeralSessionRepository: EphemeralSessionRepositoryInterface, - @inject(TYPES.Auth_RevokedSessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface, - @inject(TYPES.Auth_DeviceDetector) private deviceDetector: UAParser, - @inject(TYPES.Auth_Timer) private timer: TimerInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, - @inject(TYPES.Auth_ACCESS_TOKEN_AGE) private accessTokenAge: number, - @inject(TYPES.Auth_REFRESH_TOKEN_AGE) private refreshTokenAge: number, - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_CryptoNode) private cryptoNode: CryptoNode, - @inject(TYPES.Auth_TraceSession) private traceSession: TraceSession, - @inject(TYPES.Auth_UserSubscriptionRepository) + private revokedSessionRepository: RevokedSessionRepositoryInterface, + private deviceDetector: UAParserInstance, + private timer: TimerInterface, + private logger: Logger, + private accessTokenAge: number, + private refreshTokenAge: number, + private cryptoNode: CryptoNode, + private traceSession: TraceSession, private userSubscriptionRepository: UserSubscriptionRepositoryInterface, - @inject(TYPES.Auth_READONLY_USERS) private readonlyUsers: string[], + private readonlyUsers: string[], + private getSetting: GetSetting, ) {} async createNewSessionForUser(dto: { @@ -320,15 +315,17 @@ export class SessionService implements SessionServiceInterface { } private async isLoggingUserAgentEnabledOnSessions(user: User): Promise { - const loggingSetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(), + const loggingSettingOrError = await this.getSetting.execute({ + settingName: SettingName.NAMES.LogSessionUserAgent, + decrypted: true, userUuid: user.uuid, + allowSensitiveRetrieval: true, }) - - if (loggingSetting === null) { + if (loggingSettingOrError.isFailed()) { return true } + const loggingSetting = loggingSettingOrError.getValue() - return loggingSetting.value === LogSessionUserAgentOption.Enabled + return loggingSetting.decryptedValue === LogSessionUserAgentOption.Enabled } } diff --git a/packages/auth/src/Domain/Setting/CreateOrReplaceSettingDto.ts b/packages/auth/src/Domain/Setting/CreateOrReplaceSettingDto.ts deleted file mode 100644 index cc7b768d5..000000000 --- a/packages/auth/src/Domain/Setting/CreateOrReplaceSettingDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { User } from '../User/User' -import { SettingProps } from './SettingProps' - -export type CreateOrReplaceSettingDto = { - user: User - props: SettingProps -} diff --git a/packages/auth/src/Domain/Setting/CreateOrReplaceSettingResponse.ts b/packages/auth/src/Domain/Setting/CreateOrReplaceSettingResponse.ts deleted file mode 100644 index 935d4f292..000000000 --- a/packages/auth/src/Domain/Setting/CreateOrReplaceSettingResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Setting } from './Setting' - -export type CreateOrReplaceSettingResponse = { - status: 'created' | 'replaced' - setting: Setting -} diff --git a/packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingDTO.ts b/packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingDTO.ts deleted file mode 100644 index b157d42a3..000000000 --- a/packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingDTO.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UserSubscription } from '../Subscription/UserSubscription' -import { User } from '../User/User' -import { SubscriptionSettingProps } from './SubscriptionSettingProps' - -export type CreateOrReplaceSubscriptionSettingDTO = { - userSubscription: UserSubscription - user: User - props: SubscriptionSettingProps -} diff --git a/packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingResponse.ts b/packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingResponse.ts deleted file mode 100644 index 29fad9e3a..000000000 --- a/packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { SubscriptionSetting } from './SubscriptionSetting' - -export type CreateOrReplaceSubscriptionSettingResponse = { - status: 'created' | 'replaced' - subscriptionSetting: SubscriptionSetting -} diff --git a/packages/auth/src/Domain/Setting/FindSettingDTO.ts b/packages/auth/src/Domain/Setting/FindSettingDTO.ts deleted file mode 100644 index 2e811f365..000000000 --- a/packages/auth/src/Domain/Setting/FindSettingDTO.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SettingName } from '@standardnotes/settings' - -export type FindSettingDTO = { - userUuid: string - settingName: SettingName - settingUuid?: string -} diff --git a/packages/auth/src/Domain/Setting/FindSubscriptionSettingDTO.ts b/packages/auth/src/Domain/Setting/FindSubscriptionSettingDTO.ts deleted file mode 100644 index 2911aef85..000000000 --- a/packages/auth/src/Domain/Setting/FindSubscriptionSettingDTO.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { SettingName } from '@standardnotes/settings' - -export type FindSubscriptionSettingDTO = { - userUuid: string - userSubscriptionUuid: string - subscriptionSettingName: SettingName - settingUuid?: string -} diff --git a/packages/auth/src/Domain/Setting/Setting.ts b/packages/auth/src/Domain/Setting/Setting.ts index a5c7b3685..cbd1ce5f5 100644 --- a/packages/auth/src/Domain/Setting/Setting.ts +++ b/packages/auth/src/Domain/Setting/Setting.ts @@ -1,60 +1,13 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' -import { User } from '../User/User' +import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core' -@Entity({ name: 'settings' }) -@Index('index_settings_on_name_and_user_uuid', ['name', 'user']) -export class Setting { - @PrimaryGeneratedColumn('uuid') - declare uuid: string +import { SettingProps } from './SettingProps' - @Column({ - length: 255, - }) - declare name: string +export class Setting extends Entity { + private constructor(props: SettingProps, id?: UniqueEntityId) { + super(props, id) + } - @Column({ - type: 'text', - nullable: true, - }) - declare value: string | null - - @Column({ - name: 'server_encryption_version', - type: 'tinyint', - default: EncryptionVersion.Unencrypted, - }) - declare serverEncryptionVersion: number - - @Column({ - name: 'created_at', - type: 'bigint', - }) - declare createdAt: number - - @Column({ - name: 'updated_at', - type: 'bigint', - }) - @Index('index_settings_on_updated_at') - declare updatedAt: number - - @ManyToOne( - /* istanbul ignore next */ - () => User, - /* istanbul ignore next */ - (user) => user.settings, - /* istanbul ignore next */ - { onDelete: 'CASCADE', nullable: false, lazy: true, eager: false }, - ) - @JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' }) - declare user: Promise - - @Column({ - type: 'tinyint', - width: 1, - nullable: false, - default: 0, - }) - declare sensitive: boolean + static create(props: SettingProps, id?: UniqueEntityId): Result { + return Result.ok(new Setting(props, id)) + } } diff --git a/packages/auth/src/Domain/Setting/SettingCrypter.spec.ts b/packages/auth/src/Domain/Setting/SettingCrypter.spec.ts new file mode 100644 index 000000000..3e1b04281 --- /dev/null +++ b/packages/auth/src/Domain/Setting/SettingCrypter.spec.ts @@ -0,0 +1,240 @@ +import 'reflect-metadata' +import { CrypterInterface } from '../Encryption/CrypterInterface' +import { EncryptionVersion } from '../Encryption/EncryptionVersion' +import { User } from '../User/User' +import { UserRepositoryInterface } from '../User/UserRepositoryInterface' +import { Setting } from './Setting' + +import { SettingCrypter } from './SettingCrypter' +import { SubscriptionSetting } from './SubscriptionSetting' +import { SettingName } from '@standardnotes/settings' +import { Timestamps, Uuid } from '@standardnotes/domain-core' + +describe('SettingCrypter', () => { + let userRepository: UserRepositoryInterface + let crypter: CrypterInterface + let user: User + + const createDecrypter = () => new SettingCrypter(userRepository, crypter) + + beforeEach(() => { + crypter = {} as jest.Mocked + crypter.decryptForUser = jest.fn().mockReturnValue('decrypted') + + user = { + uuid: '4-5-6', + } as jest.Mocked + + userRepository = {} as jest.Mocked + userRepository.findOneByUuid = jest.fn().mockReturnValue(user) + }) + + describe('setting', () => { + it('should encrypt a string value', async () => { + const string = 'decrypted' + + crypter.encryptForUser = jest.fn().mockReturnValue('encrypted') + + const encrypted = await createDecrypter().encryptValue( + string, + Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + ) + + expect(encrypted).toEqual('encrypted') + }) + + it('should return null when trying to encrypt a null value', async () => { + const encrypted = await createDecrypter().encryptValue( + null, + Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + ) + + expect(encrypted).toBeNull() + }) + + it('should throw error when encrypting and user is not found', async () => { + userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + let caughtError = null + try { + await createDecrypter().encryptValue('test', Uuid.create('00000000-0000-0000-0000-000000000000').getValue()) + } catch (error) { + caughtError = error + } + + expect(caughtError).not.toBeNull() + }) + + it('should decrypt an encrypted value of a setting', async () => { + const setting = Setting.create({ + name: SettingName.NAMES.ListedAuthorSecrets, + value: 'encrypted', + serverEncryptionVersion: EncryptionVersion.Default, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual( + 'decrypted', + ) + }) + + it('should return null if the setting value is null', async () => { + const setting = Setting.create({ + name: SettingName.NAMES.ListedAuthorSecrets, + value: null, + serverEncryptionVersion: EncryptionVersion.Default, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toBeNull() + }) + + it('should return unencrypted value if the setting value is unencrypted', async () => { + const setting = Setting.create({ + name: SettingName.NAMES.ListedAuthorSecrets, + value: 'test', + serverEncryptionVersion: EncryptionVersion.Unencrypted, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual( + 'test', + ) + }) + + it('should throw if the user could not be found', async () => { + const setting = Setting.create({ + name: SettingName.NAMES.ListedAuthorSecrets, + value: 'encrypted', + serverEncryptionVersion: EncryptionVersion.Default, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + let caughtError = null + try { + await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000') + } catch (error) { + caughtError = error + } + + expect(caughtError).not.toBeNull() + }) + + it('should throw if the user uuid is invalid', async () => { + const setting = Setting.create({ + name: SettingName.NAMES.ListedAuthorSecrets, + value: 'encrypted', + serverEncryptionVersion: EncryptionVersion.Default, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + let caughtError = null + try { + await createDecrypter().decryptSettingValue(setting, 'invalid') + } catch (error) { + caughtError = error + } + + expect(caughtError).not.toBeNull() + }) + }) + + describe('subscription setting', () => { + it('should decrypt an encrypted value of a setting', async () => { + const setting = SubscriptionSetting.create({ + name: SettingName.NAMES.ExtensionKey, + value: 'encrypted', + sensitive: true, + serverEncryptionVersion: EncryptionVersion.Default, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + expect( + await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000'), + ).toEqual('decrypted') + }) + + it('should return null if the setting value is null', async () => { + const setting = SubscriptionSetting.create({ + name: SettingName.NAMES.ExtensionKey, + value: null, + sensitive: true, + serverEncryptionVersion: EncryptionVersion.Default, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + expect( + await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000'), + ).toBeNull() + }) + + it('should return unencrypted value if the setting value is unencrypted', async () => { + const setting = SubscriptionSetting.create({ + name: SettingName.NAMES.ExtensionKey, + value: 'test', + sensitive: true, + serverEncryptionVersion: EncryptionVersion.Unencrypted, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + expect( + await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000'), + ).toEqual('test') + }) + + it('should throw if the user could not be found', async () => { + const setting = SubscriptionSetting.create({ + name: SettingName.NAMES.ExtensionKey, + value: 'encrypted', + sensitive: true, + serverEncryptionVersion: EncryptionVersion.Default, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + let caughtError = null + try { + await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000') + } catch (error) { + caughtError = error + } + + expect(caughtError).not.toBeNull() + }) + + it('should throw if the user uuid is invalid', async () => { + const setting = SubscriptionSetting.create({ + name: SettingName.NAMES.ExtensionKey, + value: 'encrypted', + sensitive: true, + serverEncryptionVersion: EncryptionVersion.Default, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + let caughtError = null + try { + await createDecrypter().decryptSubscriptionSettingValue(setting, 'invalid') + } catch (error) { + caughtError = error + } + + expect(caughtError).not.toBeNull() + }) + }) +}) diff --git a/packages/auth/src/Domain/Setting/SettingCrypter.ts b/packages/auth/src/Domain/Setting/SettingCrypter.ts new file mode 100644 index 000000000..66ed243b2 --- /dev/null +++ b/packages/auth/src/Domain/Setting/SettingCrypter.ts @@ -0,0 +1,60 @@ +import { CrypterInterface } from '../Encryption/CrypterInterface' +import { EncryptionVersion } from '../Encryption/EncryptionVersion' +import { UserRepositoryInterface } from '../User/UserRepositoryInterface' +import { Setting } from './Setting' +import { SettingCrypterInterface } from './SettingCrypterInterface' +import { Uuid } from '@standardnotes/domain-core' +import { SubscriptionSetting } from './SubscriptionSetting' + +export class SettingCrypter implements SettingCrypterInterface { + constructor( + private userRepository: UserRepositoryInterface, + private crypter: CrypterInterface, + ) {} + + async encryptValue(value: string | null, userUuid: Uuid): Promise { + if (value === null) { + return null + } + + const user = await this.userRepository.findOneByUuid(userUuid) + + if (user === null) { + throw new Error(`Could not find user with uuid: ${userUuid.value}`) + } + + return this.crypter.encryptForUser(value, user) + } + + async decryptSettingValue(setting: Setting, userUuidString: string): Promise { + return this.decrypt(setting.props.value, setting.props.serverEncryptionVersion, userUuidString) + } + + async decryptSubscriptionSettingValue(setting: SubscriptionSetting, userUuidString: string): Promise { + return this.decrypt(setting.props.value, setting.props.serverEncryptionVersion, userUuidString) + } + + private async decrypt( + value: string | null, + serverEncryptionVersion: number, + userUuidString: string, + ): Promise { + if (value !== null && serverEncryptionVersion === EncryptionVersion.Default) { + const userUuidOrError = Uuid.create(userUuidString) + if (userUuidOrError.isFailed()) { + throw new Error(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + const user = await this.userRepository.findOneByUuid(userUuid) + + if (user === null) { + throw new Error(`Could not find user with uuid: ${userUuid.value}`) + } + + return this.crypter.decryptForUser(value, user) + } + + return value + } +} diff --git a/packages/auth/src/Domain/Setting/SettingCrypterInterface.ts b/packages/auth/src/Domain/Setting/SettingCrypterInterface.ts new file mode 100644 index 000000000..ba09d955f --- /dev/null +++ b/packages/auth/src/Domain/Setting/SettingCrypterInterface.ts @@ -0,0 +1,10 @@ +import { Uuid } from '@standardnotes/domain-core' + +import { Setting } from './Setting' +import { SubscriptionSetting } from './SubscriptionSetting' + +export interface SettingCrypterInterface { + encryptValue(value: string | null, userUuid: Uuid): Promise + decryptSettingValue(value: Setting, userUuid: string): Promise + decryptSubscriptionSettingValue(setting: SubscriptionSetting, userUuid: string): Promise +} diff --git a/packages/auth/src/Domain/Setting/SettingDecrypter.spec.ts b/packages/auth/src/Domain/Setting/SettingDecrypter.spec.ts deleted file mode 100644 index ec73d07b9..000000000 --- a/packages/auth/src/Domain/Setting/SettingDecrypter.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import 'reflect-metadata' -import { CrypterInterface } from '../Encryption/CrypterInterface' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' -import { User } from '../User/User' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { Setting } from './Setting' - -import { SettingDecrypter } from './SettingDecrypter' - -describe('SettingDecrypter', () => { - let userRepository: UserRepositoryInterface - let crypter: CrypterInterface - let user: User - - const createDecrypter = () => new SettingDecrypter(userRepository, crypter) - - beforeEach(() => { - crypter = {} as jest.Mocked - crypter.decryptForUser = jest.fn().mockReturnValue('decrypted') - - user = { - uuid: '4-5-6', - } as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUuid = jest.fn().mockReturnValue(user) - }) - - it('should decrypt an encrypted value of a setting', async () => { - const setting = { - value: 'encrypted', - serverEncryptionVersion: EncryptionVersion.Default, - } as jest.Mocked - - expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual( - 'decrypted', - ) - }) - - it('should return null if the setting value is null', async () => { - const setting = { - value: null, - serverEncryptionVersion: EncryptionVersion.Default, - } as jest.Mocked - - expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toBeNull() - }) - - it('should return unencrypted value if the setting value is unencrypted', async () => { - const setting = { - value: 'test', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - } as jest.Mocked - - expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual('test') - }) - - it('should throw if the user could not be found', async () => { - const setting = { - value: 'encrypted', - serverEncryptionVersion: EncryptionVersion.Default, - } as jest.Mocked - userRepository.findOneByUuid = jest.fn().mockReturnValue(null) - - let caughtError = null - try { - await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000') - } catch (error) { - caughtError = error - } - - expect(caughtError).not.toBeNull() - }) - - it('should throw if the user uuid is invalid', async () => { - const setting = { - value: 'encrypted', - serverEncryptionVersion: EncryptionVersion.Default, - } as jest.Mocked - - let caughtError = null - try { - await createDecrypter().decryptSettingValue(setting, 'invalid') - } catch (error) { - caughtError = error - } - - expect(caughtError).not.toBeNull() - }) -}) diff --git a/packages/auth/src/Domain/Setting/SettingDecrypter.ts b/packages/auth/src/Domain/Setting/SettingDecrypter.ts deleted file mode 100644 index cf3b1c679..000000000 --- a/packages/auth/src/Domain/Setting/SettingDecrypter.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { inject, injectable } from 'inversify' -import TYPES from '../../Bootstrap/Types' -import { CrypterInterface } from '../Encryption/CrypterInterface' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { Setting } from './Setting' -import { SettingDecrypterInterface } from './SettingDecrypterInterface' -import { SubscriptionSetting } from './SubscriptionSetting' -import { Uuid } from '@standardnotes/domain-core' - -@injectable() -export class SettingDecrypter implements SettingDecrypterInterface { - constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_Crypter) private crypter: CrypterInterface, - ) {} - - async decryptSettingValue(setting: Setting | SubscriptionSetting, userUuidString: string): Promise { - if (setting.value !== null && setting.serverEncryptionVersion === EncryptionVersion.Default) { - const userUuidOrError = Uuid.create(userUuidString) - if (userUuidOrError.isFailed()) { - throw new Error(userUuidOrError.getError()) - } - const userUuid = userUuidOrError.getValue() - - const user = await this.userRepository.findOneByUuid(userUuid) - - if (user === null) { - throw new Error(`Could not find user with uuid: ${userUuid.value}`) - } - - return this.crypter.decryptForUser(setting.value, user) - } - - return setting.value - } -} diff --git a/packages/auth/src/Domain/Setting/SettingDecrypterInterface.ts b/packages/auth/src/Domain/Setting/SettingDecrypterInterface.ts deleted file mode 100644 index c36feab8a..000000000 --- a/packages/auth/src/Domain/Setting/SettingDecrypterInterface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Setting } from './Setting' -import { SubscriptionSetting } from './SubscriptionSetting' - -export interface SettingDecrypterInterface { - decryptSettingValue(setting: Setting | SubscriptionSetting, userUuid: string): Promise -} diff --git a/packages/auth/src/Domain/Setting/SettingDescription.ts b/packages/auth/src/Domain/Setting/SettingDescription.ts index 91e556de5..55c87aac8 100644 --- a/packages/auth/src/Domain/Setting/SettingDescription.ts +++ b/packages/auth/src/Domain/Setting/SettingDescription.ts @@ -1,8 +1,4 @@ -import { EncryptionVersion } from '../Encryption/EncryptionVersion' - export type SettingDescription = { value: string - sensitive: boolean - serverEncryptionVersion: EncryptionVersion replaceable: boolean } diff --git a/packages/auth/src/Domain/Setting/SettingFactory.spec.ts b/packages/auth/src/Domain/Setting/SettingFactory.spec.ts deleted file mode 100644 index 170ddc750..000000000 --- a/packages/auth/src/Domain/Setting/SettingFactory.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -import 'reflect-metadata' - -import { TimerInterface } from '@standardnotes/time' -import { CrypterInterface } from '../Encryption/CrypterInterface' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' -import { User } from '../User/User' -import { Setting } from './Setting' -import { SettingFactory } from './SettingFactory' -import { SettingProps } from './SettingProps' -import { SubscriptionSettingProps } from './SubscriptionSettingProps' -import { UserSubscription } from '../Subscription/UserSubscription' -import { SubscriptionSetting } from './SubscriptionSetting' - -describe('SettingFactory', () => { - let crypter: CrypterInterface - let timer: TimerInterface - let user: User - let userSubscription: UserSubscription - - const createFactory = () => new SettingFactory(crypter, timer) - - beforeEach(() => { - crypter = {} as jest.Mocked - crypter.encryptForUser = jest.fn().mockReturnValue('encrypted') - - timer = {} as jest.Mocked - timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1) - - user = {} as jest.Mocked - - userSubscription = { - user: Promise.resolve(user), - } as jest.Mocked - }) - - it('should create a Setting', async () => { - const props: SettingProps = { - name: 'name', - unencryptedValue: 'value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - const actual = await createFactory().create(props, user) - - expect(actual).toEqual({ - createdAt: 1, - updatedAt: 1, - name: 'name', - sensitive: false, - serverEncryptionVersion: 0, - user: Promise.resolve(user), - uuid: expect.any(String), - value: 'value', - }) - }) - - it('should create a SubscriptionSetting', async () => { - const props: SubscriptionSettingProps = { - name: 'name', - unencryptedValue: 'value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - const actual = await createFactory().createSubscriptionSetting(props, userSubscription) - - expect(actual).toEqual({ - createdAt: 1, - updatedAt: 1, - name: 'name', - sensitive: false, - serverEncryptionVersion: 0, - userSubscription: Promise.resolve(userSubscription), - uuid: expect.any(String), - value: 'value', - }) - }) - - it('should create an encrypted SubscriptionSetting', async () => { - const value = 'value' - const props: SettingProps = { - name: 'name', - unencryptedValue: value, - sensitive: false, - } - - const actual = await createFactory().createSubscriptionSetting(props, userSubscription) - - expect(actual).toEqual({ - createdAt: 1, - updatedAt: 1, - name: 'name', - sensitive: false, - serverEncryptionVersion: 1, - userSubscription: Promise.resolve(userSubscription), - uuid: expect.any(String), - value: 'encrypted', - }) - }) - - it('should create a SubscriptionSetting replacement', async () => { - const original = { - userSubscription: Promise.resolve(userSubscription), - } as jest.Mocked - original.uuid = '2-3-4' - - const props: SettingProps = { - name: 'name', - unencryptedValue: 'value2', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: true, - } - - const actual = await createFactory().createSubscriptionSettingReplacement(original, props) - - expect(actual).toEqual({ - createdAt: 1, - updatedAt: 1, - name: 'name', - sensitive: true, - serverEncryptionVersion: 0, - userSubscription: Promise.resolve(userSubscription), - uuid: '2-3-4', - value: 'value2', - }) - }) - - it('should create a Setting replacement', async () => { - const original = {} as jest.Mocked - original.uuid = '2-3-4' - - const props: SettingProps = { - name: 'name', - unencryptedValue: 'value2', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: true, - } - - const actual = await createFactory().createReplacement(original, props) - - expect(actual).toEqual({ - createdAt: 1, - updatedAt: 1, - name: 'name', - sensitive: true, - serverEncryptionVersion: 0, - user: Promise.resolve(user), - uuid: '2-3-4', - value: 'value2', - }) - }) - - it('should create an encrypted Setting', async () => { - const value = 'value' - const props: SettingProps = { - name: 'name', - unencryptedValue: value, - sensitive: false, - } - - const actual = await createFactory().create(props, user) - - expect(actual).toEqual({ - createdAt: 1, - updatedAt: 1, - name: 'name', - sensitive: false, - serverEncryptionVersion: 1, - user: Promise.resolve(user), - uuid: expect.any(String), - value: 'encrypted', - }) - }) - - it('should throw for unrecognized encryption version', async () => { - const value = 'value' - const props: SettingProps = { - name: 'name', - unencryptedValue: value, - serverEncryptionVersion: 99999999999, - sensitive: false, - } - - await expect(async () => await createFactory().create(props, user)).rejects.toThrow() - }) -}) diff --git a/packages/auth/src/Domain/Setting/SettingFactory.ts b/packages/auth/src/Domain/Setting/SettingFactory.ts deleted file mode 100644 index 659733b6d..000000000 --- a/packages/auth/src/Domain/Setting/SettingFactory.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { inject, injectable } from 'inversify' -import TYPES from '../../Bootstrap/Types' -import { User } from '../User/User' -import { Setting } from './Setting' -import { SettingProps } from './SettingProps' -import { v4 as uuidv4 } from 'uuid' -import { CrypterInterface } from '../Encryption/CrypterInterface' -import { TimerInterface } from '@standardnotes/time' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' -import { SettingFactoryInterface } from './SettingFactoryInterface' -import { UserSubscription } from '../Subscription/UserSubscription' -import { SubscriptionSetting } from './SubscriptionSetting' -import { SubscriptionSettingProps } from './SubscriptionSettingProps' - -@injectable() -export class SettingFactory implements SettingFactoryInterface { - constructor( - @inject(TYPES.Auth_Crypter) private crypter: CrypterInterface, - @inject(TYPES.Auth_Timer) private timer: TimerInterface, - ) {} - - async createSubscriptionSetting( - props: SubscriptionSettingProps, - userSubscription: UserSubscription, - ): Promise { - const uuid = props.uuid ?? uuidv4() - const now = this.timer.getTimestampInMicroseconds() - const createdAt = props.createdAt ?? now - const updatedAt = props.updatedAt ?? now - - const { name, unencryptedValue, serverEncryptionVersion = EncryptionVersion.Default, sensitive } = props - - const subscriptionSetting = { - uuid, - userSubscription: Promise.resolve(userSubscription), - name, - value: await this.createValue({ - unencryptedValue, - serverEncryptionVersion, - user: await userSubscription.user, - }), - serverEncryptionVersion, - createdAt, - updatedAt, - sensitive, - } - - return Object.assign(new SubscriptionSetting(), subscriptionSetting) - } - - async createSubscriptionSettingReplacement( - original: SubscriptionSetting, - props: SubscriptionSettingProps, - ): Promise { - const { uuid, userSubscription } = original - - return Object.assign(await this.createSubscriptionSetting(props, await userSubscription), { - uuid, - }) - } - - async create(props: SettingProps, user: User): Promise { - const uuid = props.uuid ?? uuidv4() - const now = this.timer.getTimestampInMicroseconds() - const createdAt = props.createdAt ?? now - const updatedAt = props.updatedAt ?? now - - const { name, unencryptedValue, serverEncryptionVersion = EncryptionVersion.Default, sensitive } = props - - const setting = { - uuid, - user: Promise.resolve(user), - name, - value: await this.createValue({ - unencryptedValue, - serverEncryptionVersion, - user, - }), - serverEncryptionVersion, - createdAt, - updatedAt, - sensitive, - } - - return Object.assign(new Setting(), setting) - } - - async createReplacement(original: Setting, props: SettingProps): Promise { - const { uuid, user } = original - - return Object.assign(await this.create(props, await user), { - uuid, - }) - } - - async createValue({ - unencryptedValue, - serverEncryptionVersion, - user, - }: { - unencryptedValue: string | null - serverEncryptionVersion: number - user: User - }): Promise { - switch (serverEncryptionVersion) { - case EncryptionVersion.Unencrypted: - return unencryptedValue - case EncryptionVersion.Default: - return this.crypter.encryptForUser(unencryptedValue as string, user) - default: - throw Error(`Unrecognized encryption version: ${serverEncryptionVersion}!`) - } - } -} diff --git a/packages/auth/src/Domain/Setting/SettingFactoryInterface.ts b/packages/auth/src/Domain/Setting/SettingFactoryInterface.ts deleted file mode 100644 index 0c728f273..000000000 --- a/packages/auth/src/Domain/Setting/SettingFactoryInterface.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { UserSubscription } from '../Subscription/UserSubscription' -import { User } from '../User/User' -import { Setting } from './Setting' -import { SettingProps } from './SettingProps' -import { SubscriptionSetting } from './SubscriptionSetting' -import { SubscriptionSettingProps } from './SubscriptionSettingProps' - -export interface SettingFactoryInterface { - create(props: SettingProps, user: User): Promise - createSubscriptionSetting( - props: SubscriptionSettingProps, - userSubscription: UserSubscription, - ): Promise - createReplacement(original: Setting, props: SettingProps): Promise - createSubscriptionSettingReplacement( - original: SubscriptionSetting, - props: SubscriptionSettingProps, - ): Promise -} diff --git a/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts b/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts index c93ef8384..b2ab7e24a 100644 --- a/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts +++ b/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts @@ -15,19 +15,20 @@ import { Logger } from 'winston' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { User } from '../User/User' import { Setting } from './Setting' -import { SettingDecrypterInterface } from './SettingDecrypterInterface' +import { SettingCrypterInterface } from './SettingCrypterInterface' import { SettingInterpreter } from './SettingInterpreter' import { SettingRepositoryInterface } from './SettingRepositoryInterface' import { GetUserKeyParams } from '../UseCase/GetUserKeyParams/GetUserKeyParams' import { KeyParamsData } from '@standardnotes/responses' +import { Uuid, Timestamps, UniqueEntityId } from '@standardnotes/domain-core' describe('SettingInterpreter', () => { let user: User let domainEventPublisher: DomainEventPublisherInterface let domainEventFactory: DomainEventFactoryInterface let settingRepository: SettingRepositoryInterface - let settingDecrypter: SettingDecrypterInterface + let settingCrypter: SettingCrypterInterface let logger: Logger let getUserKeyParams: GetUserKeyParams @@ -44,8 +45,8 @@ describe('SettingInterpreter', () => { settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null) settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) - settingDecrypter = {} as jest.Mocked - settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted') + settingCrypter = {} as jest.Mocked + settingCrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted') domainEventPublisher = {} as jest.Mocked domainEventPublisher.publish = jest.fn() @@ -96,11 +97,19 @@ describe('SettingInterpreter', () => { }) it('should trigger backup if email backup setting is created - emails muted', async () => { - settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({ - name: SettingName.NAMES.MuteFailedBackupsEmails, - uuid: '6-7-8', - value: 'muted', - } as jest.Mocked) + const setting = Setting.create( + { + name: SettingName.NAMES.MuteFailedBackupsEmails, + value: 'muted', + serverEncryptionVersion: 0, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }, + new UniqueEntityId('7fb54003-1dd2-40bd-8900-2bacd6cf629c'), + ).getValue() + + settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting) await createInterpreter().interpretSettingUpdated( SettingName.NAMES.EmailBackupFrequency, @@ -109,7 +118,12 @@ describe('SettingInterpreter', () => { ) expect(domainEventPublisher.publish).toHaveBeenCalled() - expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '6-7-8', true, {}) + expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith( + '4-5-6', + '7fb54003-1dd2-40bd-8900-2bacd6cf629c', + true, + {}, + ) }) it('should not trigger backup if email backup setting is disabled', async () => { diff --git a/packages/auth/src/Domain/Setting/SettingInterpreter.ts b/packages/auth/src/Domain/Setting/SettingInterpreter.ts index 015a36531..191431afb 100644 --- a/packages/auth/src/Domain/Setting/SettingInterpreter.ts +++ b/packages/auth/src/Domain/Setting/SettingInterpreter.ts @@ -54,8 +54,8 @@ export class SettingInterpreter implements SettingInterpreterInterface { userUuid, ) if (muteFailedEmailsBackupSetting !== null) { - userHasEmailsMuted = muteFailedEmailsBackupSetting.value === MuteFailedBackupsEmailsOption.Muted - muteEmailsSettingUuid = muteFailedEmailsBackupSetting.uuid + userHasEmailsMuted = muteFailedEmailsBackupSetting.props.value === MuteFailedBackupsEmailsOption.Muted + muteEmailsSettingUuid = muteFailedEmailsBackupSetting.id.toString() } const keyParamsResponse = await this.getUserKeyParams.execute({ diff --git a/packages/auth/src/Domain/Setting/SettingProps.ts b/packages/auth/src/Domain/Setting/SettingProps.ts index b0aa26a62..e5dc53205 100644 --- a/packages/auth/src/Domain/Setting/SettingProps.ts +++ b/packages/auth/src/Domain/Setting/SettingProps.ts @@ -1,12 +1,10 @@ -import { Setting } from './Setting' +import { Timestamps, Uuid } from '@standardnotes/domain-core' -export type SettingProps = Omit< - Setting, - 'uuid' | 'user' | 'createdAt' | 'updatedAt' | 'serverEncryptionVersion' | 'value' -> & { - uuid?: string - createdAt?: number - updatedAt?: number - unencryptedValue: string | null - serverEncryptionVersion?: number +export interface SettingProps { + name: string + value: string | null + serverEncryptionVersion: number + timestamps: Timestamps + sensitive: boolean + userUuid: Uuid } diff --git a/packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts b/packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts index 9513323dd..671d8f7e7 100644 --- a/packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts +++ b/packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts @@ -13,5 +13,6 @@ export interface SettingRepositoryInterface { streamAllByNameAndValue(name: SettingName, value: string): Promise streamAllByName(name: SettingName): Promise deleteByUserUuid(dto: DeleteSettingDto): Promise - save(setting: Setting): Promise + insert(setting: Setting): Promise + update(setting: Setting): Promise } diff --git a/packages/auth/src/Domain/Setting/SettingService.spec.ts b/packages/auth/src/Domain/Setting/SettingService.spec.ts deleted file mode 100644 index 71d4caaca..000000000 --- a/packages/auth/src/Domain/Setting/SettingService.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -import 'reflect-metadata' - -import { LogSessionUserAgentOption, MuteSignInEmailsOption, SettingName } from '@standardnotes/settings' -import { Logger } from 'winston' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' -import { User } from '../User/User' -import { Setting } from './Setting' -import { SettingRepositoryInterface } from './SettingRepositoryInterface' - -import { SettingService } from './SettingService' -import { SettingsAssociationServiceInterface } from './SettingsAssociationServiceInterface' -import { SettingInterpreterInterface } from './SettingInterpreterInterface' -import { SettingDecrypterInterface } from './SettingDecrypterInterface' -import { SettingFactoryInterface } from './SettingFactoryInterface' - -describe('SettingService', () => { - let setting: Setting - let user: User - let factory: SettingFactoryInterface - let settingRepository: SettingRepositoryInterface - let settingsAssociationService: SettingsAssociationServiceInterface - let settingInterpreter: SettingInterpreterInterface - let settingDecrypter: SettingDecrypterInterface - let logger: Logger - - const createService = () => - new SettingService( - factory, - settingRepository, - settingsAssociationService, - settingInterpreter, - settingDecrypter, - logger, - ) - - beforeEach(() => { - user = { - uuid: '4-5-6', - } as jest.Mocked - user.isPotentiallyAPrivateUsernameAccount = jest.fn().mockReturnValue(false) - - setting = { - name: SettingName.NAMES.DropboxBackupToken, - } as jest.Mocked - - factory = {} as jest.Mocked - factory.create = jest.fn().mockReturnValue(setting) - factory.createReplacement = jest.fn().mockReturnValue(setting) - - settingRepository = {} as jest.Mocked - settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null) - settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) - settingRepository.save = jest.fn().mockImplementation((setting) => setting) - - settingsAssociationService = {} as jest.Mocked - settingsAssociationService.getDefaultSettingsAndValuesForNewUser = jest.fn().mockReturnValue( - new Map([ - [ - SettingName.NAMES.MuteSignInEmails, - { - value: MuteSignInEmailsOption.NotMuted, - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - }, - ], - ]), - ) - - settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount = jest.fn().mockReturnValue( - new Map([ - [ - SettingName.NAMES.LogSessionUserAgent, - { - sensitive: false, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - value: LogSessionUserAgentOption.Disabled, - }, - ], - ]), - ) - - settingInterpreter = {} as jest.Mocked - settingInterpreter.interpretSettingUpdated = jest.fn() - - settingDecrypter = {} as jest.Mocked - settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted') - - logger = {} as jest.Mocked - logger.debug = jest.fn() - logger.warn = jest.fn() - logger.error = jest.fn() - }) - - it('should create default settings for a newly registered user', async () => { - await createService().applyDefaultSettingsUponRegistration(user) - - expect(settingRepository.save).toHaveBeenCalledWith(setting) - }) - - it('should create default settings for a newly registered vault account', async () => { - user.isPotentiallyAPrivateUsernameAccount = jest.fn().mockReturnValue(true) - - await createService().applyDefaultSettingsUponRegistration(user) - - expect(settingRepository.save).toHaveBeenCalledWith(setting) - }) - - it("should create setting if it doesn't exist", async () => { - const result = await createService().createOrReplace({ - user, - props: { - name: SettingName.NAMES.MuteFailedBackupsEmails, - unencryptedValue: 'value', - serverEncryptionVersion: 1, - sensitive: false, - }, - }) - - expect(result.status).toEqual('created') - }) - - it('should throw error if setting name is not valid', async () => { - await expect( - createService().createOrReplace({ - user, - props: { - name: 'invalid', - unencryptedValue: 'value', - serverEncryptionVersion: 1, - sensitive: false, - }, - }), - ).rejects.toThrowError('Invalid setting name: invalid') - }) - - it('should create setting with a given uuid if it does not exist', async () => { - settingRepository.findOneByUuid = jest.fn().mockReturnValue(null) - - const result = await createService().createOrReplace({ - user, - props: { - uuid: '1-2-3', - name: SettingName.NAMES.MuteFailedBackupsEmails, - unencryptedValue: 'value', - serverEncryptionVersion: 1, - sensitive: false, - }, - }) - - expect(result.status).toEqual('created') - }) - - it('should replace setting if it does exist', async () => { - settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting) - - const result = await createService().createOrReplace({ - user: user, - props: { - ...setting, - unencryptedValue: 'value', - serverEncryptionVersion: 1, - }, - }) - - expect(result.status).toEqual('replaced') - }) - - it('should replace setting with a given uuid if it does exist', async () => { - settingRepository.findOneByUuid = jest.fn().mockReturnValue(setting) - - const result = await createService().createOrReplace({ - user: user, - props: { - ...setting, - uuid: '1-2-3', - unencryptedValue: 'value', - serverEncryptionVersion: 1, - }, - }) - - expect(result.status).toEqual('replaced') - }) - - it('should find and decrypt the value of a setting for user', async () => { - setting = { - value: 'encrypted', - serverEncryptionVersion: EncryptionVersion.Default, - } as jest.Mocked - - settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting) - - expect( - await createService().findSettingWithDecryptedValue({ - userUuid: '1-2-3', - settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(), - }), - ).toEqual({ - serverEncryptionVersion: 1, - value: 'decrypted', - }) - }) -}) diff --git a/packages/auth/src/Domain/Setting/SettingService.ts b/packages/auth/src/Domain/Setting/SettingService.ts deleted file mode 100644 index cfd916561..000000000 --- a/packages/auth/src/Domain/Setting/SettingService.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { SettingName } from '@standardnotes/settings' -import { Logger } from 'winston' - -import { User } from '../User/User' -import { CreateOrReplaceSettingDto } from './CreateOrReplaceSettingDto' -import { CreateOrReplaceSettingResponse } from './CreateOrReplaceSettingResponse' -import { FindSettingDTO } from './FindSettingDTO' -import { Setting } from './Setting' -import { SettingRepositoryInterface } from './SettingRepositoryInterface' -import { SettingServiceInterface } from './SettingServiceInterface' -import { SettingsAssociationServiceInterface } from './SettingsAssociationServiceInterface' -import { SettingInterpreterInterface } from './SettingInterpreterInterface' -import { SettingDecrypterInterface } from './SettingDecrypterInterface' -import { SettingFactoryInterface } from './SettingFactoryInterface' - -export class SettingService implements SettingServiceInterface { - constructor( - private factory: SettingFactoryInterface, - private settingRepository: SettingRepositoryInterface, - private settingsAssociationService: SettingsAssociationServiceInterface, - private settingInterpreter: SettingInterpreterInterface, - private settingDecrypter: SettingDecrypterInterface, - private logger: Logger, - ) {} - - async applyDefaultSettingsUponRegistration(user: User): Promise { - let defaultSettingsWithValues = this.settingsAssociationService.getDefaultSettingsAndValuesForNewUser() - if (user.isPotentiallyAPrivateUsernameAccount()) { - defaultSettingsWithValues = - this.settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount() - } - - for (const settingName of defaultSettingsWithValues.keys()) { - this.logger.debug(`Creating setting ${settingName} for user ${user.uuid}`) - - const setting = defaultSettingsWithValues.get(settingName) as { - value: string - sensitive: boolean - serverEncryptionVersion: number - } - - await this.createOrReplace({ - user, - props: { - name: settingName, - unencryptedValue: setting.value, - serverEncryptionVersion: setting.serverEncryptionVersion, - sensitive: setting.sensitive, - }, - }) - } - } - - async findSettingWithDecryptedValue(dto: FindSettingDTO): Promise { - let setting: Setting | null - if (dto.settingUuid !== undefined) { - setting = await this.settingRepository.findOneByUuid(dto.settingUuid) - } else { - setting = await this.settingRepository.findLastByNameAndUserUuid(dto.settingName.value, dto.userUuid) - } - - if (setting === null) { - return null - } - - setting.value = await this.settingDecrypter.decryptSettingValue(setting, dto.userUuid) - - return setting - } - - async createOrReplace(dto: CreateOrReplaceSettingDto): Promise { - const { user, props } = dto - - const settingNameOrError = SettingName.create(props.name) - if (settingNameOrError.isFailed()) { - throw new Error(settingNameOrError.getError()) - } - const settingName = settingNameOrError.getValue() - - const existing = await this.findSettingWithDecryptedValue({ - userUuid: user.uuid, - settingName, - settingUuid: props.uuid, - }) - - if (existing === null) { - const setting = await this.settingRepository.save(await this.factory.create(props, user)) - - this.logger.debug('[%s] Created setting %s: %O', user.uuid, props.name, setting) - - await this.settingInterpreter.interpretSettingUpdated(setting.name, user, props.unencryptedValue) - - return { - status: 'created', - setting, - } - } - - const setting = await this.settingRepository.save(await this.factory.createReplacement(existing, props)) - - this.logger.debug('[%s] Replaced existing setting %s with: %O', user.uuid, props.name, setting) - - await this.settingInterpreter.interpretSettingUpdated(setting.name, user, props.unencryptedValue) - - return { - status: 'replaced', - setting, - } - } -} diff --git a/packages/auth/src/Domain/Setting/SettingServiceInterface.ts b/packages/auth/src/Domain/Setting/SettingServiceInterface.ts deleted file mode 100644 index a173ec1b2..000000000 --- a/packages/auth/src/Domain/Setting/SettingServiceInterface.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { User } from '../User/User' -import { CreateOrReplaceSettingDto } from './CreateOrReplaceSettingDto' -import { CreateOrReplaceSettingResponse } from './CreateOrReplaceSettingResponse' -import { FindSettingDTO } from './FindSettingDTO' -import { Setting } from './Setting' - -export interface SettingServiceInterface { - applyDefaultSettingsUponRegistration(user: User): Promise - createOrReplace(dto: CreateOrReplaceSettingDto): Promise - findSettingWithDecryptedValue(dto: FindSettingDTO): Promise -} diff --git a/packages/auth/src/Domain/Setting/SettingsAssociationService.ts b/packages/auth/src/Domain/Setting/SettingsAssociationService.ts index 6ce6ede33..47c6f6932 100644 --- a/packages/auth/src/Domain/Setting/SettingsAssociationService.ts +++ b/packages/auth/src/Domain/Setting/SettingsAssociationService.ts @@ -32,6 +32,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt SettingName.NAMES.MuteMarketingEmails, SettingName.NAMES.ListedAuthorSecrets, SettingName.NAMES.LogSessionUserAgent, + SettingName.NAMES.RecoveryCodes, ] private readonly CLIENT_IMMUTABLE_SETTINGS = [ @@ -49,8 +50,6 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt [ SettingName.NAMES.MuteMarketingEmails, { - sensitive: false, - serverEncryptionVersion: EncryptionVersion.Unencrypted, value: MuteMarketingEmailsOption.NotMuted, replaceable: false, }, @@ -58,8 +57,6 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt [ SettingName.NAMES.LogSessionUserAgent, { - sensitive: false, - serverEncryptionVersion: EncryptionVersion.Unencrypted, value: LogSessionUserAgentOption.Enabled, replaceable: false, }, @@ -70,8 +67,6 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt [ SettingName.NAMES.LogSessionUserAgent, { - sensitive: false, - serverEncryptionVersion: EncryptionVersion.Unencrypted, value: LogSessionUserAgentOption.Disabled, replaceable: false, }, diff --git a/packages/auth/src/Domain/Setting/SimpleSetting.ts b/packages/auth/src/Domain/Setting/SimpleSetting.ts deleted file mode 100644 index 064a0c527..000000000 --- a/packages/auth/src/Domain/Setting/SimpleSetting.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Setting } from './Setting' - -export type SimpleSetting = Omit diff --git a/packages/auth/src/Domain/Setting/SimpleSubscriptionSetting.ts b/packages/auth/src/Domain/Setting/SimpleSubscriptionSetting.ts deleted file mode 100644 index 7d54828f5..000000000 --- a/packages/auth/src/Domain/Setting/SimpleSubscriptionSetting.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SubscriptionSetting } from './SubscriptionSetting' - -export type SimpleSubscriptionSetting = Omit diff --git a/packages/auth/src/Domain/Setting/SubscriptionSetting.ts b/packages/auth/src/Domain/Setting/SubscriptionSetting.ts index 297b8812a..8b5a1be2e 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSetting.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSetting.ts @@ -1,60 +1,13 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' -import { UserSubscription } from '../Subscription/UserSubscription' +import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core' -@Entity({ name: 'subscription_settings' }) -@Index('index_settings_on_name_and_user_subscription_uuid', ['name', 'userSubscription']) -export class SubscriptionSetting { - @PrimaryGeneratedColumn('uuid') - declare uuid: string +import { SubscriptionSettingProps } from './SubscriptionSettingProps' - @Column({ - length: 255, - }) - declare name: string +export class SubscriptionSetting extends Entity { + private constructor(props: SubscriptionSettingProps, id?: UniqueEntityId) { + super(props, id) + } - @Column({ - type: 'text', - nullable: true, - }) - declare value: string | null - - @Column({ - name: 'server_encryption_version', - type: 'tinyint', - default: EncryptionVersion.Unencrypted, - }) - declare serverEncryptionVersion: number - - @Column({ - name: 'created_at', - type: 'bigint', - }) - declare createdAt: number - - @Column({ - name: 'updated_at', - type: 'bigint', - }) - @Index('index_subcsription_settings_on_updated_at') - declare updatedAt: number - - @ManyToOne( - /* istanbul ignore next */ - () => UserSubscription, - /* istanbul ignore next */ - (userSubscription) => userSubscription.subscriptionSettings, - /* istanbul ignore next */ - { onDelete: 'CASCADE', nullable: false, lazy: true, eager: false }, - ) - @JoinColumn({ name: 'user_subscription_uuid', referencedColumnName: 'uuid' }) - declare userSubscription: Promise - - @Column({ - type: 'tinyint', - width: 1, - nullable: false, - default: 0, - }) - declare sensitive: boolean + static create(props: SubscriptionSettingProps, id?: UniqueEntityId): Result { + return Result.ok(new SubscriptionSetting(props, id)) + } } diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingProps.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingProps.ts index 58e7d1f53..0d7a8b84f 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingProps.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingProps.ts @@ -1,12 +1,10 @@ -import { SubscriptionSetting } from './SubscriptionSetting' +import { Timestamps, Uuid } from '@standardnotes/domain-core' -export type SubscriptionSettingProps = Omit< - SubscriptionSetting, - 'uuid' | 'userSubscription' | 'createdAt' | 'updatedAt' | 'serverEncryptionVersion' | 'value' -> & { - uuid?: string - createdAt?: number - updatedAt?: number - unencryptedValue: string | null - serverEncryptionVersion?: number +export interface SubscriptionSettingProps { + name: string + value: string | null + serverEncryptionVersion: number + timestamps: Timestamps + sensitive: boolean + userSubscriptionUuid: Uuid } diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingRepositoryInterface.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingRepositoryInterface.ts index 885a3353c..99fbca0ef 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingRepositoryInterface.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingRepositoryInterface.ts @@ -1,8 +1,11 @@ +import { Uuid } from '@standardnotes/domain-core' + import { SubscriptionSetting } from './SubscriptionSetting' export interface SubscriptionSettingRepositoryInterface { - findOneByUuid(uuid: string): Promise - findLastByNameAndUserSubscriptionUuid(name: string, userSubscriptionUuid: string): Promise - findAllBySubscriptionUuid(userSubscriptionUuid: string): Promise - save(subscriptionSetting: SubscriptionSetting): Promise + findOneByUuid(uuid: Uuid): Promise + findLastByNameAndUserSubscriptionUuid(name: string, userSubscriptionUuid: Uuid): Promise + findAllBySubscriptionUuid(userSubscriptionUuid: Uuid): Promise + insert(subscriptionSetting: SubscriptionSetting): Promise + update(subscriptionSetting: SubscriptionSetting): Promise } diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts deleted file mode 100644 index bd6e059e5..000000000 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts +++ /dev/null @@ -1,404 +0,0 @@ -import 'reflect-metadata' - -import { Logger } from 'winston' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' - -import { SubscriptionSettingService } from './SubscriptionSettingService' -import { SettingDecrypterInterface } from './SettingDecrypterInterface' -import { SubscriptionSettingRepositoryInterface } from './SubscriptionSettingRepositoryInterface' -import { SubscriptionSetting } from './SubscriptionSetting' -import { UserSubscription } from '../Subscription/UserSubscription' -import { SubscriptionName } from '@standardnotes/common' -import { User } from '../User/User' -import { SettingFactoryInterface } from './SettingFactoryInterface' -import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface' -import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { SettingName } from '@standardnotes/settings' -import { SettingInterpreterInterface } from './SettingInterpreterInterface' - -describe('SubscriptionSettingService', () => { - let setting: SubscriptionSetting - let user: User - let userSubscription: UserSubscription - let factory: SettingFactoryInterface - let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface - let subscriptionSettingsAssociationService: SubscriptionSettingsAssociationServiceInterface - let settingInterpreter: SettingInterpreterInterface - let settingDecrypter: SettingDecrypterInterface - let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let logger: Logger - - const createService = () => - new SubscriptionSettingService( - factory, - subscriptionSettingRepository, - subscriptionSettingsAssociationService, - settingInterpreter, - settingDecrypter, - userSubscriptionRepository, - logger, - ) - - beforeEach(() => { - user = {} as jest.Mocked - - userSubscription = { - uuid: '1-2-3', - user: Promise.resolve(user), - planName: SubscriptionName.PlusPlan, - } as jest.Mocked - - setting = { - name: SettingName.NAMES.FileUploadBytesUsed, - } as jest.Mocked - - factory = {} as jest.Mocked - factory.createSubscriptionSetting = jest.fn().mockReturnValue(setting) - factory.createSubscriptionSettingReplacement = jest.fn().mockReturnValue(setting) - - subscriptionSettingRepository = {} as jest.Mocked - subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null) - subscriptionSettingRepository.save = jest.fn().mockImplementation((setting) => setting) - - userSubscriptionRepository = {} as jest.Mocked - userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([ - { - uuid: 's-1-2-3', - } as jest.Mocked, - { - uuid: 's-2-3-4', - } as jest.Mocked, - ]) - - subscriptionSettingsAssociationService = {} as jest.Mocked - subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( - new Map([ - [ - SettingName.NAMES.FileUploadBytesUsed, - { - value: '0', - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - replaceable: true, - }, - ], - ]), - ) - - settingInterpreter = {} as jest.Mocked - settingInterpreter.interpretSettingUpdated = jest.fn() - - settingDecrypter = {} as jest.Mocked - settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted') - - logger = {} as jest.Mocked - logger.debug = jest.fn() - logger.warn = jest.fn() - logger.error = jest.fn() - }) - - it('should create default settings for a subscription', async () => { - await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription) - - expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting) - }) - - it('should create default settings for a subscription with overrides', async () => { - subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( - new Map([ - [ - SettingName.NAMES.FileUploadBytesUsed, - { - value: '0', - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - replaceable: false, - }, - ], - [ - SettingName.NAMES.FileUploadBytesLimit, - { - value: '345', - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - replaceable: true, - }, - ], - ]), - ) - - await createService().applyDefaultSubscriptionSettingsForSubscription( - userSubscription, - new Map([[SettingName.NAMES.FileUploadBytesLimit, '123']]), - ) - - expect(factory.createSubscriptionSetting).toHaveBeenNthCalledWith( - 1, - { - name: SettingName.NAMES.FileUploadBytesUsed, - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - unencryptedValue: '0', - }, - { - planName: SubscriptionName.PlusPlan, - user: Promise.resolve(user), - uuid: '1-2-3', - }, - ) - expect(factory.createSubscriptionSetting).toHaveBeenNthCalledWith( - 2, - { - name: SettingName.NAMES.FileUploadBytesLimit, - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - unencryptedValue: '123', - }, - { - planName: SubscriptionName.PlusPlan, - user: Promise.resolve(user), - uuid: '1-2-3', - }, - ) - }) - - it('should throw error if subscription setting is invalid', async () => { - subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( - new Map([ - [ - 'invalid', - { - value: '0', - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - replaceable: true, - }, - ], - ]), - ) - - await expect(createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)).rejects.toThrow() - }) - - it('should throw error if setting name is not a subscription setting when applying defaults', async () => { - subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( - new Map([ - [ - SettingName.NAMES.DropboxBackupFrequency, - { - value: '0', - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - replaceable: false, - }, - ], - ]), - ) - - await expect(createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)).rejects.toThrow() - }) - - it('should reassign existing default settings for a subscription if it is not replaceable', async () => { - subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( - new Map([ - [ - SettingName.NAMES.FileUploadBytesUsed, - { - value: '0', - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - replaceable: false, - }, - ], - ]), - ) - subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(setting) - - await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription) - - expect(subscriptionSettingRepository.save).toHaveBeenCalled() - }) - - it('should create default settings for a subscription if it is not replaceable and not existing', async () => { - subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( - new Map([ - [ - SettingName.NAMES.FileUploadBytesUsed, - { - value: '0', - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - replaceable: false, - }, - ], - ]), - ) - subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null) - - await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription) - - expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting) - }) - - it('should create default settings for a subscription if it is not replaceable and no previous subscription existed', async () => { - subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( - new Map([ - [ - SettingName.NAMES.FileUploadBytesUsed, - { - value: '0', - sensitive: 0, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - replaceable: false, - }, - ], - ]), - ) - subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null) - userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([ - { - uuid: '1-2-3', - } as jest.Mocked, - ]) - - await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription) - - expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting) - }) - - it('should not create default settings for a subscription if subscription has no defaults', async () => { - subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest - .fn() - .mockReturnValue(undefined) - - await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription) - - expect(subscriptionSettingRepository.save).not.toHaveBeenCalled() - }) - - it("should create setting if it doesn't exist", async () => { - const result = await createService().createOrReplace({ - userSubscription, - user, - props: { - name: SettingName.NAMES.FileUploadBytesLimit, - unencryptedValue: 'value', - serverEncryptionVersion: 1, - sensitive: false, - }, - }) - - expect(result.status).toEqual('created') - }) - - it('should throw error if the setting name is not valid', async () => { - await expect( - createService().createOrReplace({ - userSubscription, - user, - props: { - name: 'invalid', - unencryptedValue: 'value', - serverEncryptionVersion: 1, - sensitive: false, - }, - }), - ).rejects.toThrow() - }) - - it('should throw error if the setting name is not a subscription setting', async () => { - await expect( - createService().createOrReplace({ - userSubscription, - user, - props: { - name: SettingName.NAMES.DropboxBackupFrequency, - unencryptedValue: 'value', - serverEncryptionVersion: 1, - sensitive: false, - }, - }), - ).rejects.toThrow() - }) - - it('should create setting with a given uuid if it does not exist', async () => { - subscriptionSettingRepository.findOneByUuid = jest.fn().mockReturnValue(null) - - const result = await createService().createOrReplace({ - userSubscription, - user, - props: { - uuid: '1-2-3', - name: SettingName.NAMES.FileUploadBytesLimit, - unencryptedValue: 'value', - serverEncryptionVersion: 1, - sensitive: false, - }, - }) - - expect(result.status).toEqual('created') - }) - - it('should replace setting if it does exist', async () => { - subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(setting) - - const result = await createService().createOrReplace({ - userSubscription, - user, - props: { - ...setting, - unencryptedValue: 'value', - serverEncryptionVersion: 1, - }, - }) - - expect(result.status).toEqual('replaced') - }) - - it('should replace setting with a given uuid if it does exist', async () => { - subscriptionSettingRepository.findOneByUuid = jest.fn().mockReturnValue(setting) - - const result = await createService().createOrReplace({ - userSubscription, - user, - props: { - ...setting, - uuid: '1-2-3', - unencryptedValue: 'value', - serverEncryptionVersion: 1, - }, - }) - - expect(result.status).toEqual('replaced') - }) - - it('should find and decrypt the value of a setting for user', async () => { - setting = { - value: 'encrypted', - serverEncryptionVersion: EncryptionVersion.Default, - } as jest.Mocked - - subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(setting) - - expect( - await createService().findSubscriptionSettingWithDecryptedValue({ - userSubscriptionUuid: '2-3-4', - userUuid: '1-2-3', - subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), - }), - ).toEqual({ - serverEncryptionVersion: 1, - value: 'decrypted', - }) - }) - - it('should throw error when trying to find and decrypt a setting with invalid subscription setting name', async () => { - await expect( - createService().findSubscriptionSettingWithDecryptedValue({ - userSubscriptionUuid: '2-3-4', - userUuid: '1-2-3', - subscriptionSettingName: SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue(), - }), - ).rejects.toThrow() - }) -}) diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts deleted file mode 100644 index 8550c974f..000000000 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { inject, injectable } from 'inversify' -import { Logger } from 'winston' - -import TYPES from '../../Bootstrap/Types' -import { UserSubscription } from '../Subscription/UserSubscription' - -import { SettingDecrypterInterface } from './SettingDecrypterInterface' -import { SettingDescription } from './SettingDescription' -import { SubscriptionSettingServiceInterface } from './SubscriptionSettingServiceInterface' -import { CreateOrReplaceSubscriptionSettingDTO } from './CreateOrReplaceSubscriptionSettingDTO' -import { CreateOrReplaceSubscriptionSettingResponse } from './CreateOrReplaceSubscriptionSettingResponse' -import { SubscriptionSetting } from './SubscriptionSetting' -import { FindSubscriptionSettingDTO } from './FindSubscriptionSettingDTO' -import { SubscriptionSettingRepositoryInterface } from './SubscriptionSettingRepositoryInterface' -import { SettingFactoryInterface } from './SettingFactoryInterface' -import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface' -import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' -import { SettingName } from '@standardnotes/settings' -import { SettingInterpreterInterface } from './SettingInterpreterInterface' - -@injectable() -export class SubscriptionSettingService implements SubscriptionSettingServiceInterface { - constructor( - @inject(TYPES.Auth_SettingFactory) private factory: SettingFactoryInterface, - @inject(TYPES.Auth_SubscriptionSettingRepository) - private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface, - @inject(TYPES.Auth_SubscriptionSettingsAssociationService) - private subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface, - @inject(TYPES.Auth_SettingInterpreter) private settingInterpreter: SettingInterpreterInterface, - @inject(TYPES.Auth_SettingDecrypter) private settingDecrypter: SettingDecrypterInterface, - @inject(TYPES.Auth_UserSubscriptionRepository) - private userSubscriptionRepository: UserSubscriptionRepositoryInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, - ) {} - - async applyDefaultSubscriptionSettingsForSubscription( - userSubscription: UserSubscription, - overrides?: Map, - ): Promise { - const defaultSettingsWithValues = - await this.subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName( - userSubscription.planName, - ) - if (defaultSettingsWithValues === undefined) { - this.logger.warn(`Could not find settings for subscription: ${userSubscription.planName}`) - - return - } - - const user = await userSubscription.user - - for (const settingNameString of defaultSettingsWithValues.keys()) { - const settingNameOrError = SettingName.create(settingNameString) - if (settingNameOrError.isFailed()) { - throw new Error(settingNameOrError.getError()) - } - const settingName = settingNameOrError.getValue() - if (!settingName.isASubscriptionSetting()) { - throw new Error(`Setting ${settingName.value} is not a subscription setting`) - } - - const setting = defaultSettingsWithValues.get(settingName.value) as SettingDescription - if (!setting.replaceable) { - const existingSetting = await this.findPreviousSubscriptionSetting( - settingName, - userSubscription.uuid, - user.uuid, - ) - if (existingSetting !== null) { - existingSetting.userSubscription = Promise.resolve(userSubscription) - await this.subscriptionSettingRepository.save(existingSetting) - - continue - } - } - - let unencryptedValue = setting.value - if (overrides && overrides.has(settingName.value)) { - unencryptedValue = overrides.get(settingName.value) as string - } - - await this.createOrReplace({ - userSubscription, - user, - props: { - name: settingName.value, - unencryptedValue, - serverEncryptionVersion: setting.serverEncryptionVersion, - sensitive: setting.sensitive, - }, - }) - } - } - - async findSubscriptionSettingWithDecryptedValue( - dto: FindSubscriptionSettingDTO, - ): Promise { - if (!dto.subscriptionSettingName.isASubscriptionSetting()) { - throw new Error(`Setting ${dto.subscriptionSettingName.value} is not a subscription setting`) - } - - let setting: SubscriptionSetting | null - if (dto.settingUuid !== undefined) { - setting = await this.subscriptionSettingRepository.findOneByUuid(dto.settingUuid) - } else { - setting = await this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid( - dto.subscriptionSettingName.value, - dto.userSubscriptionUuid, - ) - } - - if (setting === null) { - return null - } - - setting.value = await this.settingDecrypter.decryptSettingValue(setting, dto.userUuid) - - return setting - } - - async createOrReplace( - dto: CreateOrReplaceSubscriptionSettingDTO, - ): Promise { - const { userSubscription, user, props } = dto - - const settingNameOrError = SettingName.create(props.name) - if (settingNameOrError.isFailed()) { - throw new Error(settingNameOrError.getError()) - } - const settingName = settingNameOrError.getValue() - - if (!settingName.isASubscriptionSetting()) { - throw new Error(`Setting ${settingName.value} is not a subscription setting`) - } - - const existing = await this.findSubscriptionSettingWithDecryptedValue({ - userUuid: user.uuid, - userSubscriptionUuid: userSubscription.uuid, - subscriptionSettingName: settingName, - settingUuid: props.uuid, - }) - - if (existing === null) { - const subscriptionSetting = await this.subscriptionSettingRepository.save( - await this.factory.createSubscriptionSetting(props, userSubscription), - ) - - this.logger.debug('Created subscription setting %s: %O', props.name, subscriptionSetting) - - await this.settingInterpreter.interpretSettingUpdated(settingName.value, user, props.unencryptedValue) - - return { - status: 'created', - subscriptionSetting, - } - } - - const subscriptionSetting = await this.subscriptionSettingRepository.save( - await this.factory.createSubscriptionSettingReplacement(existing, props), - ) - - this.logger.debug('Replaced existing subscription setting %s with: %O', props.name, subscriptionSetting) - - await this.settingInterpreter.interpretSettingUpdated(settingName.value, user, props.unencryptedValue) - - return { - status: 'replaced', - subscriptionSetting, - } - } - - private async findPreviousSubscriptionSetting( - settingName: SettingName, - currentUserSubscriptionUuid: string, - userUuid: string, - ): Promise { - const userSubscriptions = await this.userSubscriptionRepository.findByUserUuid(userUuid) - const previousSubscriptions = userSubscriptions.filter( - (subscription) => subscription.uuid !== currentUserSubscriptionUuid, - ) - const lastSubscription = previousSubscriptions.shift() - - if (!lastSubscription) { - return null - } - - return this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid( - settingName.value, - lastSubscription.uuid, - ) - } -} diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingServiceInterface.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingServiceInterface.ts deleted file mode 100644 index a6280e97a..000000000 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingServiceInterface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { UserSubscription } from '../Subscription/UserSubscription' - -import { CreateOrReplaceSubscriptionSettingDTO } from './CreateOrReplaceSubscriptionSettingDTO' -import { CreateOrReplaceSubscriptionSettingResponse } from './CreateOrReplaceSubscriptionSettingResponse' -import { FindSubscriptionSettingDTO } from './FindSubscriptionSettingDTO' -import { SubscriptionSetting } from './SubscriptionSetting' - -export interface SubscriptionSettingServiceInterface { - applyDefaultSubscriptionSettingsForSubscription( - userSubscription: UserSubscription, - overrides?: Map, - ): Promise - createOrReplace(dto: CreateOrReplaceSubscriptionSettingDTO): Promise - findSubscriptionSettingWithDecryptedValue(dto: FindSubscriptionSettingDTO): Promise -} diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.spec.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.spec.ts index 57e4b2cae..ef077426a 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.spec.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.spec.ts @@ -5,12 +5,12 @@ import { RoleName } from '@standardnotes/domain-core' import { SettingName } from '@standardnotes/settings' import { PermissionName } from '@standardnotes/features' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface' import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface' import { Role } from '../Role/Role' import { Permission } from '../Permission/Permission' import { SubscriptionSettingsAssociationService } from './SubscriptionSettingsAssociationService' +import { SettingDescription } from './SettingDescription' describe('SubscriptionSettingsAssociationService', () => { let roleToSubscriptionMap: RoleToSubscriptionMapInterface @@ -49,15 +49,9 @@ describe('SubscriptionSettingsAssociationService', () => { expect(settings).not.toBeUndefined() - const flatSettings = [ - ...( - settings as Map - ).keys(), - ] + const flatSettings = [...(settings as Map).keys()] expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'MUTE_SIGN_IN_EMAILS', 'FILE_UPLOAD_BYTES_LIMIT']) expect(settings?.get(SettingName.NAMES.FileUploadBytesLimit)).toEqual({ - sensitive: false, - serverEncryptionVersion: 0, value: '107374182400', replaceable: true, }) @@ -74,15 +68,9 @@ describe('SubscriptionSettingsAssociationService', () => { expect(settings).not.toBeUndefined() - const flatSettings = [ - ...( - settings as Map - ).keys(), - ] + const flatSettings = [...(settings as Map).keys()] expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'MUTE_SIGN_IN_EMAILS', 'FILE_UPLOAD_BYTES_LIMIT']) expect(settings?.get(SettingName.NAMES.FileUploadBytesLimit)).toEqual({ - sensitive: false, - serverEncryptionVersion: 0, value: '104857600', replaceable: true, }) diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.ts index 49701f63c..2b9ff0c5b 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.ts @@ -4,7 +4,6 @@ import { SettingName } from '@standardnotes/settings' import { inject, injectable } from 'inversify' import TYPES from '../../Bootstrap/Types' -import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { Permission } from '../Permission/Permission' import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface' import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface' @@ -23,15 +22,10 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti [ SubscriptionName.PlusPlan, new Map([ - [ - SettingName.NAMES.FileUploadBytesUsed, - { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false }, - ], + [SettingName.NAMES.FileUploadBytesUsed, { value: '0', replaceable: false }], [ SettingName.NAMES.MuteSignInEmails, { - sensitive: false, - serverEncryptionVersion: EncryptionVersion.Unencrypted, value: 'not_muted', replaceable: false, }, @@ -41,15 +35,10 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti [ SubscriptionName.ProPlan, new Map([ - [ - SettingName.NAMES.FileUploadBytesUsed, - { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false }, - ], + [SettingName.NAMES.FileUploadBytesUsed, { value: '0', replaceable: false }], [ SettingName.NAMES.MuteSignInEmails, { - sensitive: false, - serverEncryptionVersion: EncryptionVersion.Unencrypted, value: 'not_muted', replaceable: false, }, @@ -68,8 +57,6 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti } defaultSettings.set(SettingName.NAMES.FileUploadBytesLimit, { - sensitive: false, - serverEncryptionVersion: EncryptionVersion.Unencrypted, value: (await this.getFileUploadLimit(subscriptionName)).toString(), replaceable: true, }) diff --git a/packages/auth/src/Domain/Subscription/UserSubscription.ts b/packages/auth/src/Domain/Subscription/UserSubscription.ts index 641674694..1cc71165b 100644 --- a/packages/auth/src/Domain/Subscription/UserSubscription.ts +++ b/packages/auth/src/Domain/Subscription/UserSubscription.ts @@ -1,5 +1,4 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm' -import { SubscriptionSetting } from '../Setting/SubscriptionSetting' +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm' import { User } from '../User/User' import { UserSubscriptionType } from './UserSubscriptionType' @@ -74,14 +73,4 @@ export class UserSubscription { ) @JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' }) declare user: Promise - - @OneToMany( - /* istanbul ignore next */ - () => SubscriptionSetting, - /* istanbul ignore next */ - (subscriptionSetting) => subscriptionSetting.userSubscription, - /* istanbul ignore next */ - { lazy: true, eager: false }, - ) - declare subscriptionSettings: Promise } diff --git a/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts b/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts index 1d3fdae5a..f3751009d 100644 --- a/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts +++ b/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts @@ -5,6 +5,7 @@ export interface UserSubscriptionRepositoryInterface { findOneByUuid(uuid: string): Promise countByUserUuid(userUuid: string): Promise findOneByUserUuid(userUuid: string): Promise + findOneByUserUuidAndType(userUuid: string, type: UserSubscriptionType): Promise findByUserUuid(userUuid: string): Promise findOneByUserUuidAndSubscriptionId(userUuid: string, subscriptionId: number): Promise findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise diff --git a/packages/auth/src/Domain/Subscription/UserSubscriptionService.spec.ts b/packages/auth/src/Domain/Subscription/UserSubscriptionService.spec.ts deleted file mode 100644 index ef52e7eed..000000000 --- a/packages/auth/src/Domain/Subscription/UserSubscriptionService.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import 'reflect-metadata' -import { User } from '../User/User' -import { UserSubscription } from './UserSubscription' -import { UserSubscriptionRepositoryInterface } from './UserSubscriptionRepositoryInterface' - -import { UserSubscriptionService } from './UserSubscriptionService' -import { UserSubscriptionType } from './UserSubscriptionType' - -describe('UserSubscriptionService', () => { - let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let regularSubscription: UserSubscription - let sharedSubscription: UserSubscription - let user: User - - const createService = () => new UserSubscriptionService(userSubscriptionRepository) - - beforeEach(() => { - user = { - uuid: '1-2-3', - } as jest.Mocked - - regularSubscription = { - uuid: '1-2-3', - subscriptionType: UserSubscriptionType.Regular, - user: Promise.resolve(user), - } as jest.Mocked - sharedSubscription = { - uuid: '2-3-4', - subscriptionType: UserSubscriptionType.Shared, - user: Promise.resolve(user), - } as jest.Mocked - - userSubscriptionRepository = {} as jest.Mocked - userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null) - userSubscriptionRepository.findOneByUuid = jest.fn().mockReturnValue(null) - userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([]) - }) - - describe('by uuid', () => { - it('should return undefined if there is no user subscription', async () => { - expect(await createService().findRegularSubscriptionForUuid('1-2-3')).toEqual({ - regularSubscription: null, - sharedSubscription: null, - }) - }) - - it('should return a regular subscription if the uuid corresponds to a regular subscription', async () => { - userSubscriptionRepository.findOneByUuid = jest.fn().mockReturnValue(regularSubscription) - - expect(await createService().findRegularSubscriptionForUuid('1-2-3')).toEqual({ - regularSubscription, - sharedSubscription: null, - }) - }) - - it('should return a regular subscription if the uuid corresponds to a shared subscription', async () => { - userSubscriptionRepository.findOneByUuid = jest.fn().mockReturnValue(sharedSubscription) - userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([regularSubscription]) - - expect(await createService().findRegularSubscriptionForUuid('1-2-3')).toEqual({ - regularSubscription, - sharedSubscription, - }) - }) - - it('should return undefined if a regular subscription is not found corresponding to the shared subscription', async () => { - userSubscriptionRepository.findOneByUuid = jest.fn().mockReturnValue(sharedSubscription) - userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([]) - - expect(await createService().findRegularSubscriptionForUuid('1-2-3')).toEqual({ - regularSubscription: null, - sharedSubscription, - }) - }) - }) - - describe('by user uuid', () => { - it('should return undefined if there is no user subscription', async () => { - expect(await createService().findRegularSubscriptionForUserUuid('1-2-3')).toEqual({ - regularSubscription: null, - sharedSubscription: null, - }) - }) - - it('should return a regular subscription if the uuid corresponds to a regular subscription', async () => { - userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(regularSubscription) - - expect(await createService().findRegularSubscriptionForUserUuid('1-2-3')).toEqual({ - regularSubscription, - sharedSubscription: null, - }) - }) - - it('should return a regular subscription if the uuid corresponds to a shared subscription', async () => { - userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(sharedSubscription) - userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([regularSubscription]) - - expect(await createService().findRegularSubscriptionForUserUuid('1-2-3')).toEqual({ - regularSubscription, - sharedSubscription, - }) - }) - - it('should return undefined if a regular subscription is not found corresponding to the shared subscription', async () => { - userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(sharedSubscription) - userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([]) - - expect(await createService().findRegularSubscriptionForUserUuid('1-2-3')).toEqual({ - regularSubscription: null, - sharedSubscription, - }) - }) - }) -}) diff --git a/packages/auth/src/Domain/Subscription/UserSubscriptionService.ts b/packages/auth/src/Domain/Subscription/UserSubscriptionService.ts deleted file mode 100644 index 70eb2dded..000000000 --- a/packages/auth/src/Domain/Subscription/UserSubscriptionService.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { inject, injectable } from 'inversify' - -import TYPES from '../../Bootstrap/Types' -import { FindRegularSubscriptionResponse } from './FindRegularSubscriptionResponse' - -import { UserSubscription } from './UserSubscription' -import { UserSubscriptionRepositoryInterface } from './UserSubscriptionRepositoryInterface' -import { UserSubscriptionServiceInterface } from './UserSubscriptionServiceInterface' -import { UserSubscriptionType } from './UserSubscriptionType' - -@injectable() -export class UserSubscriptionService implements UserSubscriptionServiceInterface { - constructor( - @inject(TYPES.Auth_UserSubscriptionRepository) - private userSubscriptionRepository: UserSubscriptionRepositoryInterface, - ) {} - - async findRegularSubscriptionForUserUuid(userUuid: string): Promise { - const userSubscription = await this.userSubscriptionRepository.findOneByUserUuid(userUuid) - - return this.findRegularSubscription(userSubscription) - } - - async findRegularSubscriptionForUuid(uuid: string): Promise { - const userSubscription = await this.userSubscriptionRepository.findOneByUuid(uuid) - - return this.findRegularSubscription(userSubscription) - } - - private async findRegularSubscription( - userSubscription: UserSubscription | null, - ): Promise { - if (userSubscription === null) { - return { - regularSubscription: null, - sharedSubscription: null, - } - } - - if (userSubscription.subscriptionType === UserSubscriptionType.Regular) { - return { - regularSubscription: userSubscription, - sharedSubscription: null, - } - } - - const regularSubscriptions = await this.userSubscriptionRepository.findBySubscriptionIdAndType( - userSubscription.subscriptionId as number, - UserSubscriptionType.Regular, - ) - if (regularSubscriptions.length === 0) { - return { - regularSubscription: null, - sharedSubscription: userSubscription, - } - } - - return { - regularSubscription: regularSubscriptions[0], - sharedSubscription: userSubscription, - } - } -} diff --git a/packages/auth/src/Domain/Subscription/UserSubscriptionServiceInterface.ts b/packages/auth/src/Domain/Subscription/UserSubscriptionServiceInterface.ts deleted file mode 100644 index e37602bd8..000000000 --- a/packages/auth/src/Domain/Subscription/UserSubscriptionServiceInterface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { FindRegularSubscriptionResponse } from './FindRegularSubscriptionResponse' - -export interface UserSubscriptionServiceInterface { - findRegularSubscriptionForUuid(uuid: string): Promise - findRegularSubscriptionForUserUuid(userUuid: string): Promise -} diff --git a/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.spec.ts b/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.spec.ts index 398fad22d..1e973b66a 100644 --- a/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.spec.ts +++ b/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.spec.ts @@ -1,7 +1,7 @@ import 'reflect-metadata' import { SubscriptionName } from '@standardnotes/common' -import { RoleName } from '@standardnotes/domain-core' +import { Result, RoleName } from '@standardnotes/domain-core' import { TimerInterface } from '@standardnotes/time' import { RoleServiceInterface } from '../../Role/RoleServiceInterface' @@ -13,19 +13,21 @@ import { User } from '../../User/User' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { AcceptSharedSubscriptionInvitation } from './AcceptSharedSubscriptionInvitation' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' +import { ApplyDefaultSubscriptionSettings } from '../ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings' +import { Logger } from 'winston' describe('AcceptSharedSubscriptionInvitation', () => { let sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface let userRepository: UserRepositoryInterface let userSubscriptionRepository: UserSubscriptionRepositoryInterface let roleService: RoleServiceInterface - let subscriptionSettingService: SubscriptionSettingServiceInterface + let applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings let timer: TimerInterface let invitee: User let inviterSubscription: UserSubscription let inviteeSubscription: UserSubscription let invitation: SharedSubscriptionInvitation + let logger: Logger const createUseCase = () => new AcceptSharedSubscriptionInvitation( @@ -33,8 +35,9 @@ describe('AcceptSharedSubscriptionInvitation', () => { userRepository, userSubscriptionRepository, roleService, - subscriptionSettingService, + applyDefaultSubscriptionSettings, timer, + logger, ) beforeEach(() => { @@ -71,11 +74,14 @@ describe('AcceptSharedSubscriptionInvitation', () => { roleService = {} as jest.Mocked roleService.addUserRoleBasedOnSubscription = jest.fn() - subscriptionSettingService = {} as jest.Mocked - subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn() + applyDefaultSubscriptionSettings = {} as jest.Mocked + applyDefaultSubscriptionSettings.execute = jest.fn().mockReturnValue(Result.ok()) timer = {} as jest.Mocked timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1) + + logger = {} as jest.Mocked + logger.error = jest.fn() }) it('should create a shared subscription upon accepting the invitation', async () => { @@ -104,9 +110,7 @@ describe('AcceptSharedSubscriptionInvitation', () => { user: Promise.resolve(invitee), }) expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN') - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( - inviteeSubscription, - ) + expect(applyDefaultSubscriptionSettings.execute).toHaveBeenCalled() }) it('should create a shared subscription upon accepting the invitation if inviter has a second subscription', async () => { @@ -144,9 +148,7 @@ describe('AcceptSharedSubscriptionInvitation', () => { user: Promise.resolve(invitee), }) expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN') - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( - inviteeSubscription, - ) + expect(applyDefaultSubscriptionSettings.execute).toHaveBeenCalled() }) it('should not create a shared subscription if invitiation is not found', async () => { @@ -163,7 +165,7 @@ describe('AcceptSharedSubscriptionInvitation', () => { expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled() + expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled() }) it('should not create a shared subscription if invitee is not found', async () => { @@ -181,7 +183,7 @@ describe('AcceptSharedSubscriptionInvitation', () => { expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled() + expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled() }) it('should not create a shared subscription if invitee email is invalid', async () => { @@ -203,7 +205,7 @@ describe('AcceptSharedSubscriptionInvitation', () => { expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled() + expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled() }) it('should not create a shared subscription if inviter subscription is not found', async () => { @@ -220,7 +222,7 @@ describe('AcceptSharedSubscriptionInvitation', () => { expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled() + expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled() }) it('should not create a shared subscription if inviter subscriptions are not active', async () => { @@ -245,6 +247,6 @@ describe('AcceptSharedSubscriptionInvitation', () => { expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled() - expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled() + expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled() }) }) diff --git a/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.ts b/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.ts index d84172d5e..f16f1f9d9 100644 --- a/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.ts +++ b/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.ts @@ -1,11 +1,9 @@ import { SubscriptionName } from '@standardnotes/common' +import { Logger } from 'winston' import { Email } from '@standardnotes/domain-core' import { TimerInterface } from '@standardnotes/time' -import { inject, injectable } from 'inversify' -import TYPES from '../../../Bootstrap/Types' import { RoleServiceInterface } from '../../Role/RoleServiceInterface' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' import { InvitationStatus } from '../../SharedSubscription/InvitationStatus' import { SharedSubscriptionInvitationRepositoryInterface } from '../../SharedSubscription/SharedSubscriptionInvitationRepositoryInterface' import { UserSubscription } from '../../Subscription/UserSubscription' @@ -17,19 +15,17 @@ import { UseCaseInterface } from '../UseCaseInterface' import { AcceptSharedSubscriptionInvitationDTO } from './AcceptSharedSubscriptionInvitationDTO' import { AcceptSharedSubscriptionInvitationResponse } from './AcceptSharedSubscriptionInvitationResponse' +import { ApplyDefaultSubscriptionSettings } from '../ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings' -@injectable() export class AcceptSharedSubscriptionInvitation implements UseCaseInterface { constructor( - @inject(TYPES.Auth_SharedSubscriptionInvitationRepository) private sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface, - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_UserSubscriptionRepository) + private userRepository: UserRepositoryInterface, private userSubscriptionRepository: UserSubscriptionRepositoryInterface, - @inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface, - @inject(TYPES.Auth_SubscriptionSettingService) - private subscriptionSettingService: SubscriptionSettingServiceInterface, - @inject(TYPES.Auth_Timer) private timer: TimerInterface, + private roleService: RoleServiceInterface, + private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings, + private timer: TimerInterface, + private logger: Logger, ) {} async execute(dto: AcceptSharedSubscriptionInvitationDTO): Promise { @@ -92,7 +88,15 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface { await this.addUserRole(invitee, inviterUserSubscription.planName as SubscriptionName) - await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(inviteeSubscription) + const result = await this.applyDefaultSubscriptionSettings.execute({ + subscriptionPlanName: inviterUserSubscription.planName, + userSubscriptionUuid: inviteeSubscription.uuid, + userUuid: invitee.uuid, + }) + /* istanbul ignore next */ + if (result.isFailed()) { + this.logger.error(`Could not apply default subscription settings for user with uuid ${invitee.uuid}`) + } return { success: true, diff --git a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.spec.ts b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.spec.ts index d52842590..652a1abbb 100644 --- a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.spec.ts +++ b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.spec.ts @@ -5,13 +5,14 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { ActivatePremiumFeatures } from './ActivatePremiumFeatures' import { User } from '../../User/User' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' import { UserSubscription } from '../../Subscription/UserSubscription' +import { ApplyDefaultSubscriptionSettings } from '../ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings' +import { Result } from '@standardnotes/domain-core' describe('ActivatePremiumFeatures', () => { let userRepository: UserRepositoryInterface let userSubscriptionRepository: UserSubscriptionRepositoryInterface - let subscriptionSettingsService: SubscriptionSettingServiceInterface + let applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings let roleService: RoleServiceInterface let timer: TimerInterface let user: User @@ -20,7 +21,7 @@ describe('ActivatePremiumFeatures', () => { new ActivatePremiumFeatures( userRepository, userSubscriptionRepository, - subscriptionSettingsService, + applyDefaultSubscriptionSettings, roleService, timer, ) @@ -43,8 +44,8 @@ describe('ActivatePremiumFeatures', () => { timer.convertDateToMicroseconds = jest.fn().mockReturnValue(123456789) timer.getUTCDateNDaysAhead = jest.fn().mockReturnValue(new Date('2024-01-01T00:00:00.000Z')) - subscriptionSettingsService = {} as jest.Mocked - subscriptionSettingsService.applyDefaultSubscriptionSettingsForSubscription = jest.fn() + applyDefaultSubscriptionSettings = {} as jest.Mocked + applyDefaultSubscriptionSettings.execute = jest.fn().mockReturnValue(Result.ok()) }) it('should return error when username is invalid', async () => { diff --git a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts index 77632f7ed..769a03522 100644 --- a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts +++ b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts @@ -7,14 +7,14 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { UserSubscription } from '../../Subscription/UserSubscription' import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' import { ActivatePremiumFeaturesDTO } from './ActivatePremiumFeaturesDTO' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' import { SettingName } from '@standardnotes/settings' +import { ApplyDefaultSubscriptionSettings } from '../ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings' export class ActivatePremiumFeatures implements UseCaseInterface { constructor( private userRepository: UserRepositoryInterface, private userSubscriptionRepository: UserSubscriptionRepositoryInterface, - private subscriptionSettingService: SubscriptionSettingServiceInterface, + private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings, private roleService: RoleServiceInterface, private timer: TimerInterface, ) {} @@ -66,10 +66,12 @@ export class ActivatePremiumFeatures implements UseCaseInterface { await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionPlanName.value) - await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription( - subscription, - new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]), - ) + await this.applyDefaultSubscriptionSettings.execute({ + userSubscriptionUuid: subscription.uuid, + userUuid: user.uuid, + subscriptionPlanName: subscriptionPlanName.value, + overrides: new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]), + }) return Result.ok('Premium features activated.') } diff --git a/packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings.spec.ts b/packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings.spec.ts new file mode 100644 index 000000000..15ba27cec --- /dev/null +++ b/packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings.spec.ts @@ -0,0 +1,68 @@ +import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface' +import { SetSettingValue } from '../SetSettingValue/SetSettingValue' +import { ApplyDefaultSettings } from './ApplyDefaultSettings' + +describe('ApplyDefaultSettings', () => { + let settingsAssociationService: SettingsAssociationServiceInterface + let setSettingValue: SetSettingValue + + const createUseCase = () => new ApplyDefaultSettings(settingsAssociationService, setSettingValue) + + beforeEach(() => { + settingsAssociationService = {} as jest.Mocked + settingsAssociationService.getDefaultSettingsAndValuesForNewUser = jest.fn().mockReturnValue( + new Map([ + ['setting1', { value: 'value1', sensitive: false, serverEncryptionVersion: 0 }], + ['setting2', { value: 'value2', sensitive: false, serverEncryptionVersion: 0 }], + ]), + ) + + setSettingValue = {} as jest.Mocked + setSettingValue.execute = jest.fn().mockReturnValue(Promise.resolve()) + }) + + it('should set default settings for a new user', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userName: 'test@test.te', + }) + + expect(result.isFailed()).toBeFalsy() + expect(setSettingValue.execute).toHaveBeenCalledTimes(2) + }) + + it('should set default settings for a new private username account', async () => { + settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount = jest.fn().mockReturnValue( + new Map([ + ['setting1', { value: 'value1', sensitive: false, serverEncryptionVersion: 0 }], + ['setting2', { value: 'value2', sensitive: false, serverEncryptionVersion: 0 }], + ]), + ) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userName: 'a75a31ce95365904ef0e0a8e6cefc1f5e99adfef81bbdb6d4499eeb10ae0ff67', + }) + + expect(result.isFailed()).toBeFalsy() + expect(setSettingValue.execute).toHaveBeenCalledTimes(2) + }) + + it('should fail if user uuid is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: 'invalid', + userName: 'test@test.te', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should fail if user name is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userName: '', + }) + + expect(result.isFailed()).toBeTruthy() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings.ts b/packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings.ts new file mode 100644 index 000000000..16fcde4c0 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings.ts @@ -0,0 +1,45 @@ +import { Result, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core' + +import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface' +import { ApplyDefaultSettingsDTO } from './ApplyDefaultSettingsDTO' +import { SetSettingValue } from '../SetSettingValue/SetSettingValue' +import { SettingDescription } from '../../Setting/SettingDescription' + +export class ApplyDefaultSettings implements UseCaseInterface { + constructor( + private settingsAssociationService: SettingsAssociationServiceInterface, + private setSettingValue: SetSettingValue, + ) {} + + async execute(dto: ApplyDefaultSettingsDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + const userNameOrError = Username.create(dto.userName) + if (userNameOrError.isFailed()) { + return Result.fail(userNameOrError.getError()) + } + const userName = userNameOrError.getValue() + + let defaultSettingsWithValues = this.settingsAssociationService.getDefaultSettingsAndValuesForNewUser() + if (userName.isPotentiallyAPrivateUsernameAccount()) { + defaultSettingsWithValues = + this.settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount() + } + + for (const settingName of defaultSettingsWithValues.keys()) { + const setting = defaultSettingsWithValues.get(settingName) as SettingDescription + + await this.setSettingValue.execute({ + settingName: settingName, + userUuid: userUuid.value, + value: setting.value, + }) + } + + return Result.ok() + } +} diff --git a/packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettingsDTO.ts b/packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettingsDTO.ts new file mode 100644 index 000000000..b9f982d0e --- /dev/null +++ b/packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettingsDTO.ts @@ -0,0 +1,4 @@ +export interface ApplyDefaultSettingsDTO { + userUuid: string + userName: string +} diff --git a/packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings.spec.ts b/packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings.spec.ts new file mode 100644 index 000000000..04061204f --- /dev/null +++ b/packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings.spec.ts @@ -0,0 +1,195 @@ +import { Result, SubscriptionPlanName, Timestamps, Uuid } from '@standardnotes/domain-core' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface' +import { UserSubscription } from '../../Subscription/UserSubscription' +import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' +import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue' +import { ApplyDefaultSubscriptionSettings } from './ApplyDefaultSubscriptionSettings' +import { SettingName } from '@standardnotes/settings' +import { EncryptionVersion } from '../../Encryption/EncryptionVersion' + +describe('ApplyDefaultSubscriptionSettings', () => { + let subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface + let userSubscriptionRepository: UserSubscriptionRepositoryInterface + let getSubscriptionSetting: GetSubscriptionSetting + let setSubscriptionSettingValue: SetSubscriptionSettingValue + + const createUseCase = () => + new ApplyDefaultSubscriptionSettings( + subscriptionSettingAssociationService, + userSubscriptionRepository, + getSubscriptionSetting, + setSubscriptionSettingValue, + ) + + beforeEach(() => { + subscriptionSettingAssociationService = {} as jest.Mocked + subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( + new Map([ + [ + SettingName.NAMES.MuteSignInEmails, + { value: 'value1', sensitive: false, serverEncryptionVersion: 0, replaceable: true }, + ], + [ + SettingName.NAMES.FileUploadBytesLimit, + { value: 'value2', sensitive: false, serverEncryptionVersion: 0, replaceable: false }, + ], + ]), + ) + + userSubscriptionRepository = {} as jest.Mocked + userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([ + { + uuid: '1-2-3', + } as jest.Mocked, + ]) + + getSubscriptionSetting = {} as jest.Mocked + getSubscriptionSetting.execute = jest.fn().mockReturnValue( + Result.ok({ + setting: SubscriptionSetting.create({ + sensitive: false, + name: SettingName.NAMES.FileUploadBytesLimit, + value: '100', + timestamps: Timestamps.create(123456789, 123456789).getValue(), + serverEncryptionVersion: EncryptionVersion.Unencrypted, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + }).getValue(), + }), + ) + + setSubscriptionSettingValue = {} as jest.Mocked + setSubscriptionSettingValue.execute = jest.fn().mockReturnValue(Result.ok()) + }) + + it('should set default settings for a subscription', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan, + }) + + expect(result.isFailed()).toBeFalsy() + expect(setSubscriptionSettingValue.execute).toHaveBeenCalledTimes(2) + }) + + it('should fail if user uuid is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: 'invalid', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan, + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should fail if user subscription uuid is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userSubscriptionUuid: 'invalid', + subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan, + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should fail if subscription plan name is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + subscriptionPlanName: 'invalid', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should fail if subscription setting for plan name are not found', async () => { + subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest + .fn() + .mockReturnValue(undefined) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan, + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('shold fail if subscription setting name is invalid', async () => { + subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest + .fn() + .mockReturnValue(new Map([['invalid', { value: 'value1', sensitive: false, serverEncryptionVersion: 0 }]])) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan, + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should fail if subscription setting name is not a subscription setting', async () => { + subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest + .fn() + .mockReturnValue( + new Map([ + [ + SettingName.NAMES.MuteFailedCloudBackupsEmails, + { value: 'value1', sensitive: false, serverEncryptionVersion: 0 }, + ], + ]), + ) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan, + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should fail if setting the subcription setting value fails', async () => { + setSubscriptionSettingValue.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan, + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should create new setting if cannot find previous subscription for a non replacable setting', async () => { + userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([]) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan, + }) + + expect(result.isFailed()).toBeFalsy() + expect(setSubscriptionSettingValue.execute).toHaveBeenCalledTimes(2) + }) + + it('should allow to override setting values if setting is replacable', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan, + overrides: new Map([ + [SettingName.NAMES.MuteSignInEmails, '000'], + [SettingName.NAMES.FileUploadBytesLimit, '000'], + ]), + }) + + expect(result.isFailed()).toBeFalsy() + expect(setSubscriptionSettingValue.execute).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings.ts b/packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings.ts new file mode 100644 index 000000000..d560eb52d --- /dev/null +++ b/packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings.ts @@ -0,0 +1,119 @@ +import { Result, SubscriptionPlanName, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { SettingName } from '@standardnotes/settings' + +import { ApplyDefaultSubscriptionSettingsDTO } from './ApplyDefaultSubscriptionSettingsDTO' +import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface' +import { SettingDescription } from '../../Setting/SettingDescription' +import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue' + +export class ApplyDefaultSubscriptionSettings implements UseCaseInterface { + constructor( + private subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface, + private userSubscriptionRepository: UserSubscriptionRepositoryInterface, + private getSubscriptionSetting: GetSubscriptionSetting, + private setSubscriptionSettingValue: SetSubscriptionSettingValue, + ) {} + + async execute(dto: ApplyDefaultSubscriptionSettingsDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + const userSubscriptionUuidOrError = Uuid.create(dto.userSubscriptionUuid) + if (userSubscriptionUuidOrError.isFailed()) { + return Result.fail(userSubscriptionUuidOrError.getError()) + } + const userSubscriptionUuid = userSubscriptionUuidOrError.getValue() + + const subscriptionPlanNameOrError = SubscriptionPlanName.create(dto.subscriptionPlanName) + if (subscriptionPlanNameOrError.isFailed()) { + return Result.fail(subscriptionPlanNameOrError.getError()) + } + const subscriptionPlanName = subscriptionPlanNameOrError.getValue() + + const defaultSettingsWithValues = + await this.subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName( + subscriptionPlanName.value, + ) + if (defaultSettingsWithValues === undefined) { + return Result.fail(`Could not find default settings for subscription plan ${subscriptionPlanName.value}.`) + } + + for (const settingNameString of defaultSettingsWithValues.keys()) { + const settingNameOrError = SettingName.create(settingNameString) + if (settingNameOrError.isFailed()) { + return Result.fail(settingNameOrError.getError()) + } + const settingName = settingNameOrError.getValue() + if (!settingName.isASubscriptionSetting()) { + return Result.fail(`Setting ${settingName.value} is not a subscription setting!`) + } + + const setting = defaultSettingsWithValues.get(settingName.value) as SettingDescription + if (!setting.replaceable) { + const existingSettingOrError = await this.findPreviousSubscriptionSetting( + settingName, + userSubscriptionUuid.value, + userUuid.value, + ) + if (!existingSettingOrError.isFailed()) { + const existingSetting = existingSettingOrError.getValue() + const result = await this.setSubscriptionSettingValue.execute({ + userSubscriptionUuid: existingSetting.setting.props.userSubscriptionUuid.value, + settingName: existingSetting.setting.props.name, + value: existingSetting.setting.props.value, + newUserSubscriptionUuid: userSubscriptionUuid.value, + }) + if (result.isFailed()) { + return Result.fail(result.getError()) + } + + continue + } + } + + let unencryptedValue = setting.value + if (dto.overrides && dto.overrides.has(settingName.value)) { + unencryptedValue = dto.overrides.get(settingName.value) as string + } + + await this.setSubscriptionSettingValue.execute({ + userSubscriptionUuid: userSubscriptionUuid.value, + settingName: settingName.value, + value: unencryptedValue, + }) + } + + return Result.ok() + } + + private async findPreviousSubscriptionSetting( + settingName: SettingName, + currentUserSubscriptionUuid: string, + userUuid: string, + ): Promise> { + const userSubscriptions = await this.userSubscriptionRepository.findByUserUuid(userUuid) + const previousSubscriptions = userSubscriptions.filter( + (subscription) => subscription.uuid !== currentUserSubscriptionUuid, + ) + const lastSubscription = previousSubscriptions.shift() + + if (!lastSubscription) { + return Result.fail(`Could not find previous subscription for user ${userUuid}.`) + } + + return this.getSubscriptionSetting.execute({ + userSubscriptionUuid: lastSubscription.uuid, + settingName: settingName.value, + allowSensitiveRetrieval: true, + decryptWith: { + userUuid, + }, + }) + } +} diff --git a/packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettingsDTO.ts b/packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettingsDTO.ts new file mode 100644 index 000000000..50d29cfe6 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettingsDTO.ts @@ -0,0 +1,6 @@ +export interface ApplyDefaultSubscriptionSettingsDTO { + userUuid: string + userSubscriptionUuid: string + subscriptionPlanName: string + overrides?: Map +} diff --git a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts index 89f4ede9c..016b7928c 100644 --- a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts +++ b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts @@ -8,9 +8,14 @@ import { Role } from '../../Role/Role' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { CreateCrossServiceToken } from './CreateCrossServiceToken' -import { GetSetting } from '../GetSetting/GetSetting' import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core' import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' +import { UserSubscription } from '../../Subscription/UserSubscription' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { SettingName } from '@standardnotes/settings' +import { EncryptionVersion } from '../../Encryption/EncryptionVersion' describe('CreateCrossServiceToken', () => { let userProjector: ProjectorInterface @@ -18,7 +23,8 @@ describe('CreateCrossServiceToken', () => { let roleProjector: ProjectorInterface let tokenEncoder: TokenEncoderInterface let userRepository: UserRepositoryInterface - let getSettingUseCase: GetSetting + let getRegularSubscription: GetRegularSubscriptionForUser + let getSubscriptionSetting: GetSubscriptionSetting let sharedVaultUserRepository: SharedVaultUserRepositoryInterface const jwtTTL = 60 @@ -34,7 +40,8 @@ describe('CreateCrossServiceToken', () => { tokenEncoder, userRepository, jwtTTL, - getSettingUseCase, + getRegularSubscription, + getSubscriptionSetting, sharedVaultUserRepository, ) @@ -65,8 +72,22 @@ describe('CreateCrossServiceToken', () => { userRepository = {} as jest.Mocked userRepository.findOneByUuid = jest.fn().mockReturnValue(user) - getSettingUseCase = {} as jest.Mocked - getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } })) + getSubscriptionSetting = {} as jest.Mocked + getSubscriptionSetting.execute = jest.fn().mockReturnValue( + Result.ok({ + setting: SubscriptionSetting.create({ + sensitive: false, + name: SettingName.NAMES.FileUploadBytesLimit, + value: '100', + timestamps: Timestamps.create(123456789, 123456789).getValue(), + serverEncryptionVersion: EncryptionVersion.Unencrypted, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + }).getValue(), + }), + ) + + getRegularSubscription = {} as jest.Mocked + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found')) sharedVaultUserRepository = {} as jest.Mocked sharedVaultUserRepository.findByUserUuid = jest.fn().mockReturnValue([ @@ -188,6 +209,9 @@ describe('CreateCrossServiceToken', () => { describe('shared vault context', () => { it('should add shared vault context if shared vault owner uuid is provided', async () => { + const regularSubscription = {} as jest.Mocked + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription)) + await createUseCase().execute({ user, session, @@ -223,9 +247,7 @@ describe('CreateCrossServiceToken', () => { ) }) - it('should throw an error if shared vault owner context is sensitive', async () => { - getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sensitive: true })) - + it('should return an error if it fails to retrieve shared vault owner subscription', async () => { const result = await createUseCase().execute({ user, session, @@ -235,8 +257,11 @@ describe('CreateCrossServiceToken', () => { expect(result.isFailed()).toBeTruthy() }) - it('should throw an error if it fails to retrieve shared vault owner setting', async () => { - getSettingUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops')) + it('should return an error if it fails to retrieve shared vault owner setting', async () => { + const regularSubscription = {} as jest.Mocked + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription)) + + getSubscriptionSetting.execute = jest.fn().mockReturnValue(Result.fail('error')) const result = await createUseCase().execute({ user, diff --git a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts index a6b694c13..ad7625e4b 100644 --- a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts +++ b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts @@ -1,8 +1,7 @@ import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security' -import { inject, injectable } from 'inversify' import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { SettingName } from '@standardnotes/settings' -import TYPES from '../../../Bootstrap/Types' import { ProjectorInterface } from '../../../Projection/ProjectorInterface' import { Role } from '../../Role/Role' import { Session } from '../../Session/Session' @@ -10,22 +9,21 @@ import { User } from '../../User/User' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO' -import { GetSetting } from '../GetSetting/GetSetting' -import { SettingName } from '@standardnotes/settings' import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' -@injectable() export class CreateCrossServiceToken implements UseCaseInterface { constructor( - @inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface, - @inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface, - @inject(TYPES.Auth_RoleProjector) private roleProjector: ProjectorInterface, - @inject(TYPES.Auth_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface, - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number, - @inject(TYPES.Auth_GetSetting) - private getSettingUseCase: GetSetting, - @inject(TYPES.Auth_SharedVaultUserRepository) private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, + private userProjector: ProjectorInterface, + private sessionProjector: ProjectorInterface, + private roleProjector: ProjectorInterface, + private tokenEncoder: TokenEncoderInterface, + private userRepository: UserRepositoryInterface, + private jwtTTL: number, + private getRegularSubscription: GetRegularSubscriptionForUser, + private getSubscriptionSettingUseCase: GetSubscriptionSetting, + private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, ) {} async execute(dto: CreateCrossServiceTokenDTO): Promise> { @@ -61,18 +59,24 @@ export class CreateCrossServiceToken implements UseCaseInterface { } if (dto.sharedVaultOwnerContext !== undefined) { - const uploadBytesLimitSettingOrError = await this.getSettingUseCase.execute({ - settingName: SettingName.NAMES.FileUploadBytesLimit, + const regularSubscriptionOrError = await this.getRegularSubscription.execute({ userUuid: dto.sharedVaultOwnerContext, }) + if (regularSubscriptionOrError.isFailed()) { + return Result.fail(regularSubscriptionOrError.getError()) + } + const regularSubscription = regularSubscriptionOrError.getValue() + + const uploadBytesLimitSettingOrError = await this.getSubscriptionSettingUseCase.execute({ + settingName: SettingName.NAMES.FileUploadBytesLimit, + userSubscriptionUuid: regularSubscription.uuid, + allowSensitiveRetrieval: false, + }) if (uploadBytesLimitSettingOrError.isFailed()) { return Result.fail(uploadBytesLimitSettingOrError.getError()) } const uploadBytesLimitSetting = uploadBytesLimitSettingOrError.getValue() - if (uploadBytesLimitSetting.sensitive) { - return Result.fail('Shared vault owner upload bytes limit setting is sensitive!') - } - const uploadBytesLimit = parseInt(uploadBytesLimitSetting.setting.value as string) + const uploadBytesLimit = parseInt(uploadBytesLimitSetting.setting.props.value as string) authTokenData.shared_vault_owner_context = { upload_bytes_limit: uploadBytesLimit, diff --git a/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.spec.ts b/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.spec.ts index 19e3ff3ab..54c28c189 100644 --- a/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.spec.ts +++ b/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.spec.ts @@ -5,17 +5,23 @@ import { TokenEncoderInterface, ValetTokenData, ValetTokenOperation } from '@sta import { CreateValetToken } from './CreateValetToken' import { UserSubscription } from '../../Subscription/UserSubscription' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' import { User } from '../../User/User' import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' +import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser' +import { Result, Timestamps, Uuid } from '@standardnotes/domain-core' +import { SettingName } from '@standardnotes/settings' +import { EncryptionVersion } from '../../Encryption/EncryptionVersion' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' describe('CreateValetToken', () => { let tokenEncoder: TokenEncoderInterface - let subscriptionSettingService: SubscriptionSettingServiceInterface + let getRegularSubscription: GetRegularSubscriptionForUser let subscriptionSettingsAssociationService: SubscriptionSettingsAssociationServiceInterface - let userSubscriptionService: UserSubscriptionServiceInterface + let getSharedSubscription: GetSharedSubscriptionForUser + let getSubscriptionSetting: GetSubscriptionSetting let timer: TimerInterface const valetTokenTTL = 123 let regularSubscription: UserSubscription @@ -25,9 +31,10 @@ describe('CreateValetToken', () => { const createUseCase = () => new CreateValetToken( tokenEncoder, - subscriptionSettingService, subscriptionSettingsAssociationService, - userSubscriptionService, + getRegularSubscription, + getSharedSubscription, + getSubscriptionSetting, timer, valetTokenTTL, ) @@ -36,10 +43,19 @@ describe('CreateValetToken', () => { tokenEncoder = {} as jest.Mocked> tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar') - subscriptionSettingService = {} as jest.Mocked - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({ - value: '123', - }) + getSubscriptionSetting = {} as jest.Mocked + getSubscriptionSetting.execute = jest.fn().mockReturnValue( + Result.ok({ + setting: SubscriptionSetting.create({ + sensitive: false, + name: SettingName.NAMES.FileUploadBytesUsed, + value: '123', + timestamps: Timestamps.create(123456789, 123456789).getValue(), + serverEncryptionVersion: EncryptionVersion.Unencrypted, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + }).getValue(), + }), + ) subscriptionSettingsAssociationService = {} as jest.Mocked subscriptionSettingsAssociationService.getFileUploadLimit = jest.fn().mockReturnValue(5_368_709_120) @@ -60,10 +76,11 @@ describe('CreateValetToken', () => { user: Promise.resolve(user), } as jest.Mocked - userSubscriptionService = {} as jest.Mocked - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) + getRegularSubscription = {} as jest.Mocked + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription)) + + getSharedSubscription = {} as jest.Mocked + getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found')) timer = {} as jest.Mocked timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(100) @@ -88,9 +105,7 @@ describe('CreateValetToken', () => { }) it('should not create a valet token if a user has no subscription', async () => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found')) const response = await createUseCase().execute({ operation: ValetTokenOperation.Read, @@ -111,9 +126,7 @@ describe('CreateValetToken', () => { it('should not create a valet token if a user has an expired subscription', async () => { regularSubscription.endsAt = 1 - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription)) timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(150) @@ -188,9 +201,7 @@ describe('CreateValetToken', () => { }) it('should create a write valet token for shared subscription', async () => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription }) + getSharedSubscription.execute = jest.fn().mockReturnValue(Result.ok(sharedSubscription)) const response = await createUseCase().execute({ operation: ValetTokenOperation.Write, @@ -228,9 +239,8 @@ describe('CreateValetToken', () => { }) it('should not create a write valet token for shared subscription if regular subscription could not be found', async () => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription: null, sharedSubscription }) + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found')) + getSharedSubscription.execute = jest.fn().mockReturnValue(Result.ok(sharedSubscription)) const response = await createUseCase().execute({ operation: ValetTokenOperation.Write, @@ -250,7 +260,7 @@ describe('CreateValetToken', () => { }) it('should create a write valet token with default subscription upload limit if upload bytes settings do not exist', async () => { - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null) + getSubscriptionSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) const response = await createUseCase().execute({ operation: ValetTokenOperation.Write, diff --git a/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts b/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts index cc1901315..adfcad288 100644 --- a/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts +++ b/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts @@ -1,42 +1,42 @@ -import { inject, injectable } from 'inversify' import { SubscriptionName } from '@standardnotes/common' import { TimerInterface } from '@standardnotes/time' import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security' import { CreateValetTokenResponseData } from '@standardnotes/responses' import { SettingName } from '@standardnotes/settings' -import TYPES from '../../../Bootstrap/Types' import { UseCaseInterface } from '../UseCaseInterface' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' import { CreateValetTokenDTO } from './CreateValetTokenDTO' import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' import { CreateValetTokenPayload } from '../../ValetToken/CreateValetTokenPayload' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' +import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' -@injectable() export class CreateValetToken implements UseCaseInterface { constructor( - @inject(TYPES.Auth_ValetTokenEncoder) private tokenEncoder: TokenEncoderInterface, - @inject(TYPES.Auth_SubscriptionSettingService) - private subscriptionSettingService: SubscriptionSettingServiceInterface, - @inject(TYPES.Auth_SubscriptionSettingsAssociationService) + private tokenEncoder: TokenEncoderInterface, private subscriptionSettingsAssociationService: SubscriptionSettingsAssociationServiceInterface, - @inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, - @inject(TYPES.Auth_Timer) private timer: TimerInterface, - @inject(TYPES.Auth_VALET_TOKEN_TTL) private valetTokenTTL: number, + private getRegularSubscription: GetRegularSubscriptionForUser, + private getSharedSubscription: GetSharedSubscriptionForUser, + private getSubscriptionSetting: GetSubscriptionSetting, + private timer: TimerInterface, + private valetTokenTTL: number, ) {} async execute(dto: CreateValetTokenDTO): Promise { const { userUuid, ...payload } = dto - const { regularSubscription, sharedSubscription } = - await this.userSubscriptionService.findRegularSubscriptionForUserUuid(userUuid) - if (regularSubscription === null) { + + const regularSubscriptionOrError = await this.getRegularSubscription.execute({ + userUuid: dto.userUuid, + }) + if (regularSubscriptionOrError.isFailed()) { return { success: false, reason: 'no-subscription', } } + const regularSubscription = regularSubscriptionOrError.getValue() if (regularSubscription.endsAt < this.timer.getTimestampInMicroseconds()) { return { @@ -52,34 +52,37 @@ export class CreateValetToken implements UseCaseInterface { } } - const regularSubscriptionUserUuid = (await regularSubscription.user).uuid - let uploadBytesUsed = 0 - const uploadBytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ - userUuid: regularSubscriptionUserUuid, + const uploadBytesUsedSettingOrError = await this.getSubscriptionSetting.execute({ userSubscriptionUuid: regularSubscription.uuid, - subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(), + settingName: SettingName.NAMES.FileUploadBytesUsed, + allowSensitiveRetrieval: false, }) - if (uploadBytesUsedSetting !== null) { - uploadBytesUsed = +(uploadBytesUsedSetting.value as string) + if (!uploadBytesUsedSettingOrError.isFailed()) { + const uploadBytesUsedSetting = uploadBytesUsedSettingOrError.getValue() + uploadBytesUsed = +(uploadBytesUsedSetting.setting.props.value as string) } const defaultUploadBytesLimitForSubscription = await this.subscriptionSettingsAssociationService.getFileUploadLimit( regularSubscription.planName as SubscriptionName, ) let uploadBytesLimit = defaultUploadBytesLimitForSubscription - const overwriteWithUserUploadBytesLimitSetting = - await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ - userUuid: regularSubscriptionUserUuid, - userSubscriptionUuid: regularSubscription.uuid, - subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), - }) - if (overwriteWithUserUploadBytesLimitSetting !== null) { - uploadBytesLimit = +(overwriteWithUserUploadBytesLimitSetting.value as string) + const overwriteWithUserUploadBytesLimitSettingOrError = await this.getSubscriptionSetting.execute({ + userSubscriptionUuid: regularSubscription.uuid, + settingName: SettingName.NAMES.FileUploadBytesLimit, + allowSensitiveRetrieval: false, + }) + if (!overwriteWithUserUploadBytesLimitSettingOrError.isFailed()) { + const overwriteWithUserUploadBytesLimitSetting = overwriteWithUserUploadBytesLimitSettingOrError.getValue() + uploadBytesLimit = +(overwriteWithUserUploadBytesLimitSetting.setting.props.value as string) } let sharedSubscriptionUuid = undefined - if (sharedSubscription !== null) { + const sharedSubscriptionOrError = await this.getSharedSubscription.execute({ + userUuid, + }) + if (!sharedSubscriptionOrError.isFailed()) { + const sharedSubscription = sharedSubscriptionOrError.getValue() sharedSubscriptionUuid = sharedSubscription.uuid } diff --git a/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts b/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts index 1b338aba2..25eda53ca 100644 --- a/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts +++ b/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts @@ -7,22 +7,22 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { DeleteAccount } from './DeleteAccount' import { UserSubscription } from '../../Subscription/UserSubscription' import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' import { TimerInterface } from '@standardnotes/time' -import { RoleName } from '@standardnotes/domain-core' +import { Result, RoleName } from '@standardnotes/domain-core' import { Role } from '../../Role/Role' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' describe('DeleteAccount', () => { let userRepository: UserRepositoryInterface let domainEventPublisher: DomainEventPublisherInterface let domainEventFactory: DomainEventFactoryInterface - let userSubscriptionService: UserSubscriptionServiceInterface + let getRegularSubscription: GetRegularSubscriptionForUser let user: User let regularSubscription: UserSubscription let timer: TimerInterface const createUseCase = () => - new DeleteAccount(userRepository, userSubscriptionService, domainEventPublisher, domainEventFactory, timer) + new DeleteAccount(userRepository, getRegularSubscription, domainEventPublisher, domainEventFactory, timer) beforeEach(() => { user = { @@ -40,10 +40,8 @@ describe('DeleteAccount', () => { userRepository.findOneByUuid = jest.fn().mockReturnValue(user) userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - userSubscriptionService = {} as jest.Mocked - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) + getRegularSubscription = {} as jest.Mocked + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription)) domainEventPublisher = {} as jest.Mocked domainEventPublisher.publish = jest.fn() @@ -59,9 +57,7 @@ describe('DeleteAccount', () => { describe('when user uuid is provided', () => { it('should trigger account deletion - no subscription', async () => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found')) const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) @@ -76,10 +72,6 @@ describe('DeleteAccount', () => { }) it('should trigger account deletion - subscription present', async () => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) - const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) expect(result.isFailed()).toBeFalsy() @@ -116,9 +108,7 @@ describe('DeleteAccount', () => { describe('when username is provided', () => { it('should trigger account deletion - no subscription', async () => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found')) const result = await createUseCase().execute({ username: 'test@test.te' }) @@ -133,10 +123,6 @@ describe('DeleteAccount', () => { }) it('should trigger account deletion - subscription present', async () => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) - const result = await createUseCase().execute({ username: 'test@test.te' }) expect(result.isFailed()).toBeFalsy() diff --git a/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts b/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts index 5f797b0de..4b7b120b3 100644 --- a/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts +++ b/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts @@ -1,24 +1,21 @@ import { Result, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core' import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { TimerInterface } from '@standardnotes/time' -import { inject, injectable } from 'inversify' -import TYPES from '../../../Bootstrap/Types' import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { DeleteAccountDTO } from './DeleteAccountDTO' import { User } from '../../User/User' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' -@injectable() export class DeleteAccount implements UseCaseInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, - @inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface, - @inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface, - @inject(TYPES.Auth_Timer) private timer: TimerInterface, + private userRepository: UserRepositoryInterface, + private getRegularSubscription: GetRegularSubscriptionForUser, + private domainEventPublisher: DomainEventPublisherInterface, + private domainEventFactory: DomainEventFactoryInterface, + private timer: TimerInterface, ) {} async execute(dto: DeleteAccountDTO): Promise> { @@ -49,9 +46,12 @@ export class DeleteAccount implements UseCaseInterface { const roles = await user.roles - let regularSubscriptionUuid = undefined - const { regularSubscription } = await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid) - if (regularSubscription !== null) { + let regularSubscriptionUuid: string | undefined + const result = await this.getRegularSubscription.execute({ + userUuid: user.uuid, + }) + if (!result.isFailed()) { + const regularSubscription = result.getValue() regularSubscriptionUuid = regularSubscription.uuid } diff --git a/packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.spec.ts b/packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.spec.ts index 81865ebc5..00b4626f6 100644 --- a/packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.spec.ts +++ b/packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.spec.ts @@ -6,6 +6,8 @@ import { Setting } from '../../Setting/Setting' import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' import { DeleteSetting } from './DeleteSetting' +import { Timestamps, Uuid } from '@standardnotes/domain-core' +import { SettingName } from '@standardnotes/settings' describe('DeleteSetting', () => { let setting: Setting @@ -15,13 +17,20 @@ describe('DeleteSetting', () => { const createUseCase = () => new DeleteSetting(settingRepository, timer) beforeEach(() => { - setting = {} as jest.Mocked + setting = Setting.create({ + name: SettingName.NAMES.LogSessionUserAgent, + value: 'test', + serverEncryptionVersion: 0, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() settingRepository = {} as jest.Mocked settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting) settingRepository.findOneByUuid = jest.fn().mockReturnValue(setting) settingRepository.deleteByUserUuid = jest.fn() - settingRepository.save = jest.fn() + settingRepository.update = jest.fn() timer = {} as jest.Mocked timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1) @@ -76,10 +85,7 @@ describe('DeleteSetting', () => { softDelete: true, }) - expect(settingRepository.save).toHaveBeenCalledWith({ - updatedAt: 1, - value: null, - }) + expect(settingRepository.update).toHaveBeenCalled() }) it('should soft delete a setting with timestamp', async () => { @@ -90,9 +96,6 @@ describe('DeleteSetting', () => { timestamp: 123, }) - expect(settingRepository.save).toHaveBeenCalledWith({ - updatedAt: 123, - value: null, - }) + expect(settingRepository.update).toHaveBeenCalled() }) }) diff --git a/packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.ts b/packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.ts index 3f65744e8..8a0ae9a53 100644 --- a/packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.ts +++ b/packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.ts @@ -6,6 +6,7 @@ import TYPES from '../../../Bootstrap/Types' import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' import { TimerInterface } from '@standardnotes/time' import { Setting } from '../../Setting/Setting' +import { Timestamps } from '@standardnotes/domain-core' @injectable() export class DeleteSetting implements UseCaseInterface { @@ -29,10 +30,13 @@ export class DeleteSetting implements UseCaseInterface { } if (dto.softDelete) { - setting.value = null - setting.updatedAt = dto.timestamp ?? this.timer.getTimestampInMicroseconds() + setting.props.value = null + setting.props.timestamps = Timestamps.create( + setting.props.timestamps.createdAt, + dto.timestamp ?? this.timer.getTimestampInMicroseconds(), + ).getValue() - await this.settingRepository.save(setting) + await this.settingRepository.update(setting) } else { await this.settingRepository.deleteByUserUuid({ userUuid, diff --git a/packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.spec.ts b/packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.spec.ts index 7003f099e..79611ceda 100644 --- a/packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.spec.ts +++ b/packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.spec.ts @@ -1,19 +1,28 @@ -import { EmailLevel } from '@standardnotes/domain-core' -import { Setting } from '../../Setting/Setting' -import { SettingFactoryInterface } from '../../Setting/SettingFactoryInterface' -import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' +import { EmailLevel, Result } from '@standardnotes/domain-core' import { User } from '../../User/User' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { DisableEmailSettingBasedOnEmailSubscription } from './DisableEmailSettingBasedOnEmailSubscription' +import { SetSettingValue } from '../SetSettingValue/SetSettingValue' +import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue' +import { GetSharedOrRegularSubscriptionForUser } from '../GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser' +import { UserSubscription } from '../../Subscription/UserSubscription' describe('DisableEmailSettingBasedOnEmailSubscription', () => { let userRepository: UserRepositoryInterface - let settingRepository: SettingRepositoryInterface - let factory: SettingFactoryInterface + let setSettingValue: SetSettingValue + let setSubscriptionSetting: SetSubscriptionSettingValue + let getSharedOrRegularSubscriptionForUser: GetSharedOrRegularSubscriptionForUser + let regularSubscription: UserSubscription + let user: User const createUseCase = () => - new DisableEmailSettingBasedOnEmailSubscription(userRepository, settingRepository, factory) + new DisableEmailSettingBasedOnEmailSubscription( + userRepository, + setSettingValue, + setSubscriptionSetting, + getSharedOrRegularSubscriptionForUser, + ) beforeEach(() => { user = {} as jest.Mocked @@ -22,13 +31,51 @@ describe('DisableEmailSettingBasedOnEmailSubscription', () => { userRepository = {} as jest.Mocked userRepository.findOneByUsernameOrEmail = jest.fn().mockResolvedValue(user) - settingRepository = {} as jest.Mocked - settingRepository.findLastByNameAndUserUuid = jest.fn().mockResolvedValue({} as jest.Mocked) - settingRepository.save = jest.fn() + setSettingValue = {} as jest.Mocked + setSettingValue.execute = jest.fn().mockReturnValue(Result.ok()) - factory = {} as jest.Mocked - factory.create = jest.fn().mockResolvedValue({} as jest.Mocked) - factory.createReplacement = jest.fn().mockResolvedValue({} as jest.Mocked) + setSubscriptionSetting = {} as jest.Mocked + setSubscriptionSetting.execute = jest.fn().mockResolvedValue(Result.ok()) + + regularSubscription = {} as jest.Mocked + + getSharedOrRegularSubscriptionForUser = {} as jest.Mocked + getSharedOrRegularSubscriptionForUser.execute = jest.fn().mockResolvedValue(Result.ok(regularSubscription)) + }) + + it('should set the setting value when muting non subscription setting value', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userEmail: 'test@test.te', + level: EmailLevel.LEVELS.Marketing, + }) + + expect(result.isFailed()).toBeFalsy() + }) + + it('should set the subscription setting value when muting a subscription setting value', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userEmail: 'test@test.te', + level: EmailLevel.LEVELS.SignIn, + }) + + expect(result.isFailed()).toBeFalsy() + }) + + it('should return error if subscription could not be found', async () => { + getSharedOrRegularSubscriptionForUser.execute = jest.fn().mockResolvedValue(Result.fail('error')) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userEmail: 'test@test.te', + level: EmailLevel.LEVELS.SignIn, + }) + + expect(result.isFailed()).toBeTruthy() }) it('should fail if the username is empty', async () => { @@ -65,32 +112,4 @@ describe('DisableEmailSettingBasedOnEmailSubscription', () => { expect(result.isFailed()).toBeTruthy() }) - - it('should create a new setting if it does not exist', async () => { - settingRepository.findLastByNameAndUserUuid = jest.fn().mockResolvedValue(null) - - const useCase = createUseCase() - - const result = await useCase.execute({ - userEmail: 'test@test.te', - level: EmailLevel.LEVELS.Marketing, - }) - - expect(result.isFailed()).toBeFalsy() - expect(factory.create).toHaveBeenCalled() - expect(factory.createReplacement).not.toHaveBeenCalled() - }) - - it('should replace the setting if it exists', async () => { - const useCase = createUseCase() - - const result = await useCase.execute({ - userEmail: 'test@test.te', - level: EmailLevel.LEVELS.Marketing, - }) - - expect(result.isFailed()).toBeFalsy() - expect(factory.create).not.toHaveBeenCalled() - expect(factory.createReplacement).toHaveBeenCalled() - }) }) diff --git a/packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.ts b/packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.ts index 04e6c952d..87aec1919 100644 --- a/packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.ts +++ b/packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.ts @@ -3,14 +3,16 @@ import { SettingName } from '@standardnotes/settings' import { DisableEmailSettingBasedOnEmailSubscriptionDTO } from './DisableEmailSettingBasedOnEmailSubscriptionDTO' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' -import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' -import { SettingFactoryInterface } from '../../Setting/SettingFactoryInterface' +import { SetSettingValue } from '../SetSettingValue/SetSettingValue' +import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue' +import { GetSharedOrRegularSubscriptionForUser } from '../GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser' export class DisableEmailSettingBasedOnEmailSubscription implements UseCaseInterface { constructor( private userRepository: UserRepositoryInterface, - private settingRepository: SettingRepositoryInterface, - private factory: SettingFactoryInterface, + private setSettingValue: SetSettingValue, + private setSubscriptionSetting: SetSubscriptionSettingValue, + private getSharedOrRegularSubscriptionForUser: GetSharedOrRegularSubscriptionForUser, ) {} async execute(dto: DisableEmailSettingBasedOnEmailSubscriptionDTO): Promise> { @@ -31,40 +33,40 @@ export class DisableEmailSettingBasedOnEmailSubscription implements UseCaseInter } const settingName = settingNameOrError.getValue() - let setting = await this.settingRepository.findLastByNameAndUserUuid(settingName, user.uuid) - if (!setting) { - setting = await this.factory.create( - { - name: settingName, - unencryptedValue: 'muted', - sensitive: false, - }, - user, - ) + if (settingName.isASubscriptionSetting()) { + const subscriptionOrError = await this.getSharedOrRegularSubscriptionForUser.execute({ + userUuid: user.uuid, + }) + if (subscriptionOrError.isFailed()) { + return Result.fail(subscriptionOrError.getError()) + } + const subscription = subscriptionOrError.getValue() + + return this.setSubscriptionSetting.execute({ + settingName: settingName.value, + userSubscriptionUuid: subscription.uuid, + value: 'muted', + }) } else { - setting = await this.factory.createReplacement(setting, { - name: settingName, - unencryptedValue: 'muted', - sensitive: false, + return this.setSettingValue.execute({ + settingName: settingName.value, + userUuid: user.uuid, + value: 'muted', }) } - - await this.settingRepository.save(setting) - - return Result.ok() } - private getSettingNameFromLevel(level: string): Result { + private getSettingNameFromLevel(level: string): Result { /* istanbul ignore next */ switch (level) { case EmailLevel.LEVELS.FailedCloudBackup: - return Result.ok(SettingName.NAMES.MuteFailedCloudBackupsEmails) + return Result.ok(SettingName.create(SettingName.NAMES.MuteFailedCloudBackupsEmails).getValue()) case EmailLevel.LEVELS.FailedEmailBackup: - return Result.ok(SettingName.NAMES.MuteFailedBackupsEmails) + return Result.ok(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue()) case EmailLevel.LEVELS.Marketing: - return Result.ok(SettingName.NAMES.MuteMarketingEmails) + return Result.ok(SettingName.create(SettingName.NAMES.MuteMarketingEmails).getValue()) case EmailLevel.LEVELS.SignIn: - return Result.ok(SettingName.NAMES.MuteSignInEmails) + return Result.ok(SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue()) default: return Result.fail(`Unknown level: ${level}`) } diff --git a/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.spec.ts b/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.spec.ts index ed0eafd97..588c8c1f0 100644 --- a/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.spec.ts +++ b/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.spec.ts @@ -1,22 +1,23 @@ import { CryptoNode } from '@standardnotes/sncrypto-node' -import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' import { User } from '../../User/User' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { GenerateRecoveryCodes } from './GenerateRecoveryCodes' +import { SetSettingValue } from '../SetSettingValue/SetSettingValue' +import { Result } from '@standardnotes/domain-core' describe('GenerateRecoveryCodes', () => { let userRepository: UserRepositoryInterface - let settingService: SettingServiceInterface + let setSettingValue: SetSettingValue let cryptoNode: CryptoNode - const createUseCase = () => new GenerateRecoveryCodes(userRepository, settingService, cryptoNode) + const createUseCase = () => new GenerateRecoveryCodes(userRepository, setSettingValue, cryptoNode) beforeEach(() => { userRepository = {} as jest.Mocked userRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked) - settingService = {} as jest.Mocked - settingService.createOrReplace = jest.fn() + setSettingValue = {} as jest.Mocked + setSettingValue.execute = jest.fn().mockReturnValue(Result.ok()) cryptoNode = {} as jest.Mocked cryptoNode.generateRandomKey = jest.fn().mockReturnValue('randomKey123') @@ -27,11 +28,22 @@ describe('GenerateRecoveryCodes', () => { const result = await useCase.execute({ userUuid: '2221101c-1da9-4d2b-9b32-b8be2a8d1c82' }) - expect(settingService.createOrReplace).toHaveBeenCalled() + expect(setSettingValue.execute).toHaveBeenCalled() expect(result.isFailed()).toBeFalsy() expect(result.getValue()).toEqual('RAND OMKE Y123') }) + it('should return error if could not persist recovery codes setting', async () => { + setSettingValue.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const useCase = createUseCase() + + const result = await useCase.execute({ userUuid: '2221101c-1da9-4d2b-9b32-b8be2a8d1c82' }) + + expect(result.isFailed()).toBeTruthy() + expect(result.getError()).toEqual('Could not generate recovery codes: error') + }) + it('should return error if empty random string', async () => { cryptoNode.generateRandomKey = jest.fn().mockReturnValue('') diff --git a/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.ts b/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.ts index 4a444bbb6..a58c94998 100644 --- a/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.ts +++ b/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.ts @@ -1,17 +1,17 @@ import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { SettingName } from '@standardnotes/settings' import { CryptoNode } from '@standardnotes/sncrypto-node' -import { EncryptionVersion } from '../../Encryption/EncryptionVersion' -import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { GenerateRecoveryCodesDTO } from './GenerateRecoveryCodesDTO' +import { SetSettingValue } from '../SetSettingValue/SetSettingValue' export class GenerateRecoveryCodes implements UseCaseInterface { constructor( private userRepository: UserRepositoryInterface, - private settingService: SettingServiceInterface, + private setSettingValue: SetSettingValue, private cryptoNode: CryptoNode, ) {} + async execute(dto: GenerateRecoveryCodesDTO): Promise> { const userUuidOrError = Uuid.create(dto.userUuid) if (userUuidOrError.isFailed()) { @@ -32,15 +32,14 @@ export class GenerateRecoveryCodes implements UseCaseInterface { const recoveryCodes = recoveryCodesSplit.join(' ') - await this.settingService.createOrReplace({ - user, - props: { - name: SettingName.NAMES.RecoveryCodes, - unencryptedValue: recoveryCodes, - serverEncryptionVersion: EncryptionVersion.Default, - sensitive: false, - }, + const result = await this.setSettingValue.execute({ + settingName: SettingName.NAMES.RecoveryCodes, + value: recoveryCodes, + userUuid: user.uuid, }) + if (result.isFailed()) { + return Result.fail(`Could not generate recovery codes: ${result.getError()}`) + } return Result.ok(recoveryCodes) } diff --git a/packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser.spec.ts b/packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser.spec.ts new file mode 100644 index 000000000..21cbe0eba --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser.spec.ts @@ -0,0 +1,75 @@ +import { Result } from '@standardnotes/domain-core' + +import { GetSettings } from '../GetSettings/GetSettings' +import { GetSubscriptionSettings } from '../GetSubscriptionSettings/GetSubscriptionSettings' +import { GetAllSettingsForUser } from './GetAllSettingsForUser' +import { GetSharedOrRegularSubscriptionForUser } from '../GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser' + +describe('GetAllSettingsForUser', () => { + let getSettings: GetSettings + let getSharedOrRegularSubscription: GetSharedOrRegularSubscriptionForUser + let getSubscriptionSettings: GetSubscriptionSettings + + const createUseCase = () => + new GetAllSettingsForUser(getSettings, getSharedOrRegularSubscription, getSubscriptionSettings) + + beforeEach(() => { + getSettings = {} as jest.Mocked + getSettings.execute = jest.fn().mockReturnValue(Result.ok([])) + + getSharedOrRegularSubscription = {} as jest.Mocked + getSharedOrRegularSubscription.execute = jest + .fn() + .mockReturnValue(Result.ok({ uuid: '00000000-0000-0000-0000-000000000000' })) + + getSubscriptionSettings = {} as jest.Mocked + getSubscriptionSettings.execute = jest.fn().mockReturnValue(Result.ok([])) + }) + + it('should return settings for a user', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + }) + + it('should fail if user uuid is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: 'invalid', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should fail if getting settings fails', async () => { + getSettings.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return just the settings if there is no subscription', async () => { + getSharedOrRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + expect(result.getValue().subscriptionSettings).toEqual([]) + }) + + it('should fail if getting subscription settings fails', async () => { + getSubscriptionSettings.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeTruthy() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser.ts b/packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser.ts new file mode 100644 index 000000000..3b390ea95 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser.ts @@ -0,0 +1,72 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { GetAllSettingsForUserDTO } from './GetAllSettingsForUserDTO' +import { Setting } from '../../Setting/Setting' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { GetSubscriptionSettings } from '../GetSubscriptionSettings/GetSubscriptionSettings' +import { GetSettings } from '../GetSettings/GetSettings' +import { GetSharedOrRegularSubscriptionForUser } from '../GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser' + +export class GetAllSettingsForUser + implements + UseCaseInterface<{ + settings: { setting: Setting; decryptedValue?: string | null }[] + subscriptionSettings: { setting: SubscriptionSetting; decryptedValue?: string | null }[] + }> +{ + constructor( + private getSettings: GetSettings, + private getSharedOrRegularSubscription: GetSharedOrRegularSubscriptionForUser, + private getSubscriptionSettings: GetSubscriptionSettings, + ) {} + + async execute(dto: GetAllSettingsForUserDTO): Promise< + Result<{ + settings: { setting: Setting; decryptedValue?: string | null }[] + subscriptionSettings: { setting: SubscriptionSetting; decryptedValue?: string | null }[] + }> + > { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + const settingsOrError = await this.getSettings.execute({ + userUuid: userUuid.value, + decrypted: true, + }) + if (settingsOrError.isFailed()) { + return Result.fail(settingsOrError.getError()) + } + const settings = settingsOrError.getValue() + + const subscriptionOrError = await this.getSharedOrRegularSubscription.execute({ + userUuid: userUuid.value, + }) + + if (subscriptionOrError.isFailed()) { + return Result.ok({ + settings, + subscriptionSettings: [], + }) + } + + const subscription = subscriptionOrError.getValue() + const subscriptionSettingsOrError = await this.getSubscriptionSettings.execute({ + userSubscriptionUuid: subscription.uuid, + decryptWith: { + userUuid: userUuid.value, + }, + }) + if (subscriptionSettingsOrError.isFailed()) { + return Result.fail(subscriptionSettingsOrError.getError()) + } + const subscriptionSettings = subscriptionSettingsOrError.getValue() + + return Result.ok({ + settings, + subscriptionSettings, + }) + } +} diff --git a/packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUserDTO.ts b/packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUserDTO.ts new file mode 100644 index 000000000..f6f881691 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUserDTO.ts @@ -0,0 +1,3 @@ +export type GetAllSettingsForUserDTO = { + userUuid: string +} diff --git a/packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser.spec.ts b/packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser.spec.ts new file mode 100644 index 000000000..04311ad8a --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser.spec.ts @@ -0,0 +1,46 @@ +import { UserSubscription } from '../../Subscription/UserSubscription' +import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface' +import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' +import { GetRegularSubscriptionForUser } from './GetRegularSubscriptionForUser' + +describe('GetRegularSubscriptionForUser', () => { + let userSubscriptionRepository: UserSubscriptionRepositoryInterface + let regularSubscription: UserSubscription + + const createUseCase = () => new GetRegularSubscriptionForUser(userSubscriptionRepository) + + beforeEach(() => { + regularSubscription = { + subscriptionType: UserSubscriptionType.Regular, + } as jest.Mocked + + userSubscriptionRepository = {} as jest.Mocked + userSubscriptionRepository.findOneByUserUuidAndType = jest.fn().mockResolvedValue(regularSubscription) + }) + + it('returns error when user uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ userUuid: 'invalid' }) + + expect(result.isFailed()).toBe(true) + }) + + it('returns error when user subscription is not found', async () => { + const useCase = createUseCase() + userSubscriptionRepository.findOneByUserUuidAndType = jest.fn().mockResolvedValue(null) + + const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) + + expect(result.isFailed()).toBe(true) + }) + + it('returns regular subscription when user subscription is regular', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) + + expect(result.isFailed()).toBe(false) + expect(result.getValue()).toBe(regularSubscription) + }) +}) diff --git a/packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser.ts b/packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser.ts new file mode 100644 index 000000000..5523aa76c --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser.ts @@ -0,0 +1,28 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { UserSubscription } from '../../Subscription/UserSubscription' +import { GetRegularSubscriptionForUserDTO } from './GetRegularSubscriptionForUserDTO' +import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface' +import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' + +export class GetRegularSubscriptionForUser implements UseCaseInterface { + constructor(private userSubscriptionRepository: UserSubscriptionRepositoryInterface) {} + + async execute(dto: GetRegularSubscriptionForUserDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(`Could not get regular subscription for user: ${userUuidOrError.getError()}`) + } + const userUuid = userUuidOrError.getValue() + + const userSubscription = await this.userSubscriptionRepository.findOneByUserUuidAndType( + userUuid.value, + UserSubscriptionType.Regular, + ) + if (userSubscription === null) { + return Result.fail(`User subscription for user ${userUuid.value} not found.`) + } + + return Result.ok(userSubscription) + } +} diff --git a/packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUserDTO.ts b/packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUserDTO.ts new file mode 100644 index 000000000..330958460 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUserDTO.ts @@ -0,0 +1,3 @@ +export interface GetRegularSubscriptionForUserDTO { + userUuid: string +} diff --git a/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.spec.ts b/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.spec.ts index 16bf899fe..48c8bd781 100644 --- a/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.spec.ts +++ b/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.spec.ts @@ -1,236 +1,142 @@ -import 'reflect-metadata' - import { SettingName } from '@standardnotes/settings' - -import { SettingProjector } from '../../../Projection/SettingProjector' -import { Setting } from '../../Setting/Setting' -import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' - +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' +import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' import { GetSetting } from './GetSetting' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' -import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' -import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' -import { UserSubscription } from '../../Subscription/UserSubscription' -import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' +import { Setting } from '../../Setting/Setting' +import { Uuid, Timestamps } from '@standardnotes/domain-core' describe('GetSetting', () => { - let settingProjector: SettingProjector - let setting: Setting - let subscriptionSetting: SubscriptionSetting - let settingService: SettingServiceInterface - let userSubscriptionService: UserSubscriptionServiceInterface - let subscriptionSettingProjector: SubscriptionSettingProjector - let subscriptionSettingService: SubscriptionSettingServiceInterface - let regularSubscription: UserSubscription - let sharedSubscription: UserSubscription + let settingRepository: SettingRepositoryInterface + let settingCrypter: SettingCrypterInterface - const createUseCase = () => - new GetSetting( - settingProjector, - subscriptionSettingProjector, - settingService, - subscriptionSettingService, - userSubscriptionService, - ) + const createUseCase = () => new GetSetting(settingRepository, settingCrypter) beforeEach(() => { - setting = {} as jest.Mocked - - subscriptionSetting = { + const setting = Setting.create({ + name: SettingName.NAMES.LogSessionUserAgent, + value: 'test', + serverEncryptionVersion: 0, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), sensitive: false, - } as jest.Mocked + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + settingRepository = {} as jest.Mocked + settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting) - settingService = {} as jest.Mocked - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) - - settingProjector = {} as jest.Mocked - settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' }) - - subscriptionSettingService = {} as jest.Mocked - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest - .fn() - .mockReturnValue(subscriptionSetting) - - regularSubscription = { - uuid: '1-2-3', - subscriptionType: UserSubscriptionType.Regular, - } as jest.Mocked - - sharedSubscription = { - uuid: '2-3-4', - subscriptionType: UserSubscriptionType.Shared, - } as jest.Mocked - - userSubscriptionService = {} as jest.Mocked - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) - - subscriptionSettingProjector = {} as jest.Mocked - subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' }) + settingCrypter = {} as jest.Mocked + settingCrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted') }) - describe('no subscription', () => { - it('should find a setting for user', async () => { - const result = await createUseCase().execute({ - userUuid: '1-2-3', - settingName: SettingName.NAMES.DropboxBackupFrequency, - }) - expect(result.isFailed()).toBeFalsy() - expect(result.getValue()).toEqual({ - userUuid: '1-2-3', - setting: { foo: 'bar' }, - }) + it('should return a setting', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.ExtensionKey, + allowSensitiveRetrieval: false, + decrypted: false, }) - it('should not find a setting if the setting name is invalid', async () => { - const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' }) - expect(result.isFailed()).toBeTruthy() - }) - - it('should not get a setting for user if it does not exist', async () => { - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) - - const result = await createUseCase().execute({ - userUuid: '1-2-3', - settingName: SettingName.NAMES.DropboxBackupFrequency, - }) - expect(result.isFailed()).toBeTruthy() - }) - - it('should not retrieve a sensitive setting for user', async () => { - setting = { - sensitive: true, - name: SettingName.NAMES.MfaSecret, - } as jest.Mocked - - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) - - const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret }) - expect(result.isFailed()).toBeFalsy() - expect(result.getValue()).toEqual({ - sensitive: true, - }) - }) - - it('should not retrieve a subscription setting for user', async () => { - const result = await createUseCase().execute({ - userUuid: '1-2-3', - settingName: SettingName.NAMES.MuteSignInEmails, - }) - expect(result.isFailed()).toBeTruthy() - }) - - it('should retrieve a sensitive setting for user if explicitly told to', async () => { - setting = { - sensitive: true, - name: SettingName.NAMES.MfaSecret, - } as jest.Mocked - - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) - - const result = await createUseCase().execute({ - userUuid: '1-2-3', - settingName: SettingName.NAMES.MfaSecret, - allowSensitiveRetrieval: true, - }) - expect(result.isFailed()).toBeFalsy() - expect(result.getValue()).toEqual({ - userUuid: '1-2-3', - setting: { foo: 'bar' }, - }) - }) + expect(result.isFailed()).toBeFalsy() }) - describe('regular subscription', () => { - beforeEach(() => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) + it('should return error if setting is a subscription setting', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MuteSignInEmails, + allowSensitiveRetrieval: false, + decrypted: false, }) - it('should find a setting for user', async () => { - const result = await createUseCase().execute({ - userUuid: '1-2-3', - settingName: SettingName.NAMES.MuteSignInEmails, - }) - expect(result.isFailed()).toBeFalsy() - expect(result.getValue()).toEqual({ - userUuid: '1-2-3', - setting: { foo: 'sub-bar' }, - }) - }) - - it('should not get a suscription setting for user if it does not exist', async () => { - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null) - - const result = await createUseCase().execute({ - userUuid: '1-2-3', - settingName: SettingName.NAMES.MuteSignInEmails, - }) - expect(result.isFailed()).toBeTruthy() - }) - - it('should not retrieve a sensitive subscription setting for user', async () => { - subscriptionSetting.sensitive = true - - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest - .fn() - .mockReturnValue(subscriptionSetting) - - const result = await createUseCase().execute({ - userUuid: '1-2-3', - settingName: SettingName.NAMES.MuteSignInEmails, - }) - expect(result.isFailed()).toBeFalsy() - expect(result.getValue()).toEqual({ - sensitive: true, - }) - }) + expect(result.isFailed()).toBeTruthy() }) - describe('shared subscription', () => { - beforeEach(() => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription }) + it('should return a decrypted setting', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.ExtensionKey, + allowSensitiveRetrieval: false, + decrypted: true, }) - it('should find a setting for user', async () => { - const result = await createUseCase().execute({ - userUuid: '1-2-3', - settingName: SettingName.NAMES.MuteSignInEmails, - }) - expect(result.isFailed()).toBeFalsy() - expect(result.getValue()).toEqual({ - userUuid: '1-2-3', - setting: { foo: 'sub-bar' }, - }) + expect(result.isFailed()).toBeFalsy() + expect(result.getValue().decryptedValue).toEqual('decrypted') + }) - expect(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({ - subscriptionSettingName: SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue(), - userSubscriptionUuid: '2-3-4', - userUuid: '1-2-3', - }) + it('should not allow sensitive retrieval', async () => { + const setting = Setting.create({ + name: SettingName.NAMES.LogSessionUserAgent, + value: 'test', + serverEncryptionVersion: 0, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: true, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + settingRepository = {} as jest.Mocked + settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.ExtensionKey, + allowSensitiveRetrieval: false, + decrypted: true, }) - it('should find a regular subscription only setting for user', async () => { - const result = await createUseCase().execute({ - userUuid: '1-2-3', - settingName: SettingName.NAMES.FileUploadBytesLimit, - }) - expect(result.isFailed()).toBeFalsy() - expect(result.getValue()).toEqual({ - userUuid: '1-2-3', - setting: { foo: 'sub-bar' }, - }) + expect(result.isFailed()).toBeTruthy() + }) - expect(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({ - subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), - userSubscriptionUuid: '1-2-3', - userUuid: '1-2-3', - }) + it('should allow sensitive retrieval if explicitly allowed', async () => { + const setting = Setting.create({ + name: SettingName.NAMES.LogSessionUserAgent, + value: 'test', + serverEncryptionVersion: 0, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: true, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + settingRepository = {} as jest.Mocked + settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.ExtensionKey, + allowSensitiveRetrieval: true, + decrypted: true, }) + + expect(result.isFailed()).toBeFalsy() + }) + + it('should return error if user uuid is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: 'invalid', + settingName: SettingName.NAMES.ExtensionKey, + allowSensitiveRetrieval: false, + decrypted: false, + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return error if setting name is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: 'invalid', + allowSensitiveRetrieval: false, + decrypted: false, + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should fail if the setting is not found', async () => { + settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.ExtensionKey, + allowSensitiveRetrieval: false, + decrypted: false, + }) + + expect(result.isFailed()).toBeTruthy() }) }) diff --git a/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.ts b/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.ts index 3890b8716..fc640990c 100644 --- a/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.ts +++ b/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.ts @@ -1,30 +1,24 @@ import { SettingName } from '@standardnotes/settings' -import { inject, injectable } from 'inversify' -import { Result, UseCaseInterface } from '@standardnotes/domain-core' - -import TYPES from '../../../Bootstrap/Types' -import { SettingProjector } from '../../../Projection/SettingProjector' -import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' -import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { GetSettingDto } from './GetSettingDto' -import { GetSettingResponse } from './GetSettingResponse' -import { UserSubscription } from '../../Subscription/UserSubscription' +import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' +import { Setting } from '../../Setting/Setting' +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' -@injectable() -export class GetSetting implements UseCaseInterface { +export class GetSetting implements UseCaseInterface<{ setting: Setting; decryptedValue?: string | null }> { constructor( - @inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector, - @inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector, - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_SubscriptionSettingService) - private subscriptionSettingService: SubscriptionSettingServiceInterface, - @inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, + private settingRepository: SettingRepositoryInterface, + private settingCrypter: SettingCrypterInterface, ) {} - async execute(dto: GetSettingDto): Promise> { + async execute(dto: GetSettingDto): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + const settingNameOrError = SettingName.create(dto.settingName) if (settingNameOrError.isFailed()) { return Result.fail(settingNameOrError.getError()) @@ -32,63 +26,29 @@ export class GetSetting implements UseCaseInterface { const settingName = settingNameOrError.getValue() if (settingName.isASubscriptionSetting()) { - const { regularSubscription, sharedSubscription } = - await this.userSubscriptionService.findRegularSubscriptionForUserUuid(dto.userUuid) - let subscription: UserSubscription | null - if (settingName.isARegularOnlySubscriptionSetting()) { - subscription = regularSubscription - } else { - subscription = sharedSubscription ?? regularSubscription - } - - if (!subscription) { - return Result.fail('No subscription found.') - } - - const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ - userUuid: dto.userUuid, - subscriptionSettingName: settingName, - userSubscriptionUuid: subscription.uuid, - }) - - if (subscriptionSetting === null) { - return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`) - } - - if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) { - return Result.ok({ - sensitive: true, - }) - } - - const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting) - - return Result.ok({ - userUuid: dto.userUuid, - setting: simpleSubscriptionSetting, - }) + return Result.fail(`Setting ${settingName.value} is a subscription setting!`) } - const setting = await this.settingService.findSettingWithDecryptedValue({ - userUuid: dto.userUuid, - settingName, - }) - + const setting = await this.settingRepository.findLastByNameAndUserUuid(settingName.value, userUuid.value) if (setting === null) { return Result.fail(`Setting ${settingName.value} for user ${dto.userUuid} not found!`) } - if (setting.sensitive && !dto.allowSensitiveRetrieval) { + if (setting.props.sensitive && !dto.allowSensitiveRetrieval) { + return Result.fail(`Setting ${settingName.value} for user ${dto.userUuid} is sensitive!`) + } + + if (dto.decrypted) { + const decryptedValue = await this.settingCrypter.decryptSettingValue(setting, userUuid.value) + return Result.ok({ - sensitive: true, + setting, + decryptedValue, }) } - const simpleSetting = await this.settingProjector.projectSimple(setting) - return Result.ok({ - userUuid: dto.userUuid, - setting: simpleSetting, + setting, }) } } diff --git a/packages/auth/src/Domain/UseCase/GetSetting/GetSettingDto.ts b/packages/auth/src/Domain/UseCase/GetSetting/GetSettingDto.ts index b0e008933..5619253f3 100644 --- a/packages/auth/src/Domain/UseCase/GetSetting/GetSettingDto.ts +++ b/packages/auth/src/Domain/UseCase/GetSetting/GetSettingDto.ts @@ -1,5 +1,6 @@ export type GetSettingDto = { userUuid: string settingName: string - allowSensitiveRetrieval?: boolean + allowSensitiveRetrieval: boolean + decrypted: boolean } diff --git a/packages/auth/src/Domain/UseCase/GetSetting/GetSettingResponse.ts b/packages/auth/src/Domain/UseCase/GetSetting/GetSettingResponse.ts deleted file mode 100644 index ed812dbd4..000000000 --- a/packages/auth/src/Domain/UseCase/GetSetting/GetSettingResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Either } from '@standardnotes/common' - -import { SimpleSetting } from '../../Setting/SimpleSetting' - -export type GetSettingResponse = Either< - { - userUuid: string - setting: SimpleSetting - }, - { - sensitive: true - } -> diff --git a/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.spec.ts b/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.spec.ts index 579a9aad2..1cde8cdff 100644 --- a/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.spec.ts +++ b/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.spec.ts @@ -1,248 +1,66 @@ -import 'reflect-metadata' - +import { Uuid, Timestamps } from '@standardnotes/domain-core' import { SettingName } from '@standardnotes/settings' - -import { SettingProjector } from '../../../Projection/SettingProjector' import { Setting } from '../../Setting/Setting' +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' - import { GetSettings } from './GetSettings' -import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' -import { User } from '../../User/User' -import { CrypterInterface } from '../../Encryption/CrypterInterface' -import { EncryptionVersion } from '../../Encryption/EncryptionVersion' -import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' -import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' -import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' -import { UserSubscription } from '../../Subscription/UserSubscription' -import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' describe('GetSettings', () => { let settingRepository: SettingRepositoryInterface - let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface - let userSubscriptionService: UserSubscriptionServiceInterface - let settingProjector: SettingProjector - let subscriptionSettingProjector: SubscriptionSettingProjector - let setting: Setting - let mfaSetting: Setting - let signInEmailsSetting: SubscriptionSetting - let userRepository: UserRepositoryInterface - let user: User - let crypter: CrypterInterface - let regularSubscription: UserSubscription - let sharedSubscription: UserSubscription + let settingCrypter: SettingCrypterInterface - const createUseCase = () => - new GetSettings( - settingRepository, - subscriptionSettingRepository, - userSubscriptionService, - settingProjector, - subscriptionSettingProjector, - userRepository, - crypter, - ) + const createUseCase = () => new GetSettings(settingRepository, settingCrypter) beforeEach(() => { - user = { - uuid: '00000000-0000-0000-0000-000000000000', - } as jest.Mocked - - setting = new Setting() - setting.name = 'test' - setting.updatedAt = 345 - setting.sensitive = false - - mfaSetting = new Setting() - mfaSetting.name = SettingName.NAMES.MfaSecret - mfaSetting.updatedAt = 122 - mfaSetting.sensitive = true - - signInEmailsSetting = new SubscriptionSetting() - signInEmailsSetting.name = SettingName.NAMES.MuteSignInEmails - signInEmailsSetting.updatedAt = 122 - signInEmailsSetting.sensitive = false - signInEmailsSetting.value = 'not_muted' + const unsensitiveSetting = Setting.create({ + name: SettingName.NAMES.LogSessionUserAgent, + value: 'test', + serverEncryptionVersion: 0, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + const sensitiveSetting = Setting.create({ + name: SettingName.NAMES.MfaSecret, + value: 'test', + serverEncryptionVersion: 1, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: true, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() settingRepository = {} as jest.Mocked - settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting, mfaSetting]) + settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([unsensitiveSetting, sensitiveSetting]) - subscriptionSettingRepository = {} as jest.Mocked - subscriptionSettingRepository.findAllBySubscriptionUuid = jest.fn().mockReturnValue([signInEmailsSetting]) - - regularSubscription = { - uuid: '1-2-3', - subscriptionType: UserSubscriptionType.Regular, - user: Promise.resolve(user), - } as jest.Mocked - - sharedSubscription = { - uuid: '2-3-4', - subscriptionType: UserSubscriptionType.Shared, - user: Promise.resolve(user), - } as jest.Mocked - - userSubscriptionService = {} as jest.Mocked - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) - - settingProjector = {} as jest.Mocked - settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' }) - - subscriptionSettingProjector = {} as jest.Mocked - subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' }) - - userRepository = {} as jest.Mocked - userRepository.findOneByUuid = jest.fn().mockReturnValue(user) - - crypter = {} as jest.Mocked - crypter.decryptForUser = jest.fn().mockReturnValue('decrypted') + settingCrypter = {} as jest.Mocked + settingCrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted') }) - describe('no subscription', () => { - it('should fail if a user is not found', async () => { - userRepository.findOneByUuid = jest.fn().mockReturnValue(null) - - expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({ - success: false, - error: { - message: 'User 00000000-0000-0000-0000-000000000000 not found.', - }, - }) + it('should return settings', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + decrypted: false, }) - it('should fail if a user uuid is invalid', async () => { - expect(await createUseCase().execute({ userUuid: 'invalid' })).toEqual({ - success: false, - error: { - message: 'Given value is not a valid uuid: invalid', - }, - }) - }) - - it('should return all user settings except mfa', async () => { - expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({ - success: true, - userUuid: '00000000-0000-0000-0000-000000000000', - settings: [{ foo: 'bar' }], - }) - - expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) - expect(subscriptionSettingProjector.projectSimple).not.toHaveBeenCalled() - }) - - it('should return all setting with decrypted values', async () => { - setting = { - name: 'test', - updatedAt: 345, - value: 'encrypted', - serverEncryptionVersion: EncryptionVersion.Default, - } as jest.Mocked - settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting]) - - expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({ - success: true, - userUuid: '00000000-0000-0000-0000-000000000000', - settings: [{ foo: 'bar' }], - }) - - expect(settingProjector.projectSimple).toHaveBeenCalledWith({ - name: 'test', - updatedAt: 345, - value: 'decrypted', - serverEncryptionVersion: 1, - }) - }) - - it('should return all user settings of certain name', async () => { - expect( - await createUseCase().execute({ - userUuid: '00000000-0000-0000-0000-000000000000', - settingName: 'test', - allowSensitiveRetrieval: true, - }), - ).toEqual({ - success: true, - userUuid: '00000000-0000-0000-0000-000000000000', - settings: [{ foo: 'bar' }], - }) - - expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) - }) - - it('should return all user settings updated after', async () => { - expect( - await createUseCase().execute({ - userUuid: '00000000-0000-0000-0000-000000000000', - allowSensitiveRetrieval: true, - updatedAfter: 123, - }), - ).toEqual({ - success: true, - userUuid: '00000000-0000-0000-0000-000000000000', - settings: [{ foo: 'bar' }], - }) - - expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) - }) - - it('should return all sensitive user settings if explicit', async () => { - expect( - await createUseCase().execute({ - userUuid: '00000000-0000-0000-0000-000000000000', - allowSensitiveRetrieval: true, - }), - ).toEqual({ - success: true, - userUuid: '00000000-0000-0000-0000-000000000000', - settings: [{ foo: 'bar' }, { foo: 'bar' }], - }) - - expect(settingProjector.projectSimple).toHaveBeenCalledTimes(2) - expect(settingProjector.projectSimple).toHaveBeenNthCalledWith(1, setting) - expect(settingProjector.projectSimple).toHaveBeenNthCalledWith(2, mfaSetting) - }) + expect(result.isFailed()).toBeFalsy() }) - describe('regular subscription', () => { - beforeEach(() => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) + it('should return decrypted settings', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + decrypted: true, }) - it('should return all user settings except mfa', async () => { - expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({ - success: true, - userUuid: '00000000-0000-0000-0000-000000000000', - settings: [{ foo: 'bar' }, { foo: 'sub-bar' }], - }) - - expect(subscriptionSettingRepository.findAllBySubscriptionUuid).toHaveBeenCalledWith('1-2-3') - expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) - expect(subscriptionSettingProjector.projectSimple).toHaveBeenCalledWith(signInEmailsSetting) - }) + expect(result.isFailed()).toBeFalsy() + expect(result.getValue()[0].decryptedValue).toEqual('decrypted') }) - describe('shared subscription', () => { - beforeEach(() => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription }) + it('should fail if user uuid is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: 'invalid', + decrypted: false, }) - it('should return all user settings except mfa', async () => { - expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({ - success: true, - userUuid: '00000000-0000-0000-0000-000000000000', - settings: [{ foo: 'bar' }, { foo: 'sub-bar' }], - }) - - expect(subscriptionSettingRepository.findAllBySubscriptionUuid).toHaveBeenCalledWith('2-3-4') - expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) - expect(subscriptionSettingProjector.projectSimple).toHaveBeenCalledWith(signInEmailsSetting) - }) + expect(result.isFailed()).toBeTruthy() }) }) diff --git a/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.ts b/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.ts index 04b5b1cdc..77d49f4d2 100644 --- a/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.ts +++ b/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.ts @@ -1,100 +1,44 @@ -import { inject, injectable } from 'inversify' -import { GetSettingsDto } from './GetSettingsDto' -import { GetSettingsResponse } from './GetSettingsResponse' -import { UseCaseInterface } from '../UseCaseInterface' -import TYPES from '../../../Bootstrap/Types' -import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' -import { SettingProjector } from '../../../Projection/SettingProjector' +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { Setting } from '../../Setting/Setting' -import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' -import { CrypterInterface } from '../../Encryption/CrypterInterface' -import { EncryptionVersion } from '../../Encryption/EncryptionVersion' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' -import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' -import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' -import { SimpleSetting } from '../../Setting/SimpleSetting' -import { SimpleSubscriptionSetting } from '../../Setting/SimpleSubscriptionSetting' -import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' -import { Uuid } from '@standardnotes/domain-core' +import { GetSettingsDTO } from './GetSettingsDTO' +import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' -@injectable() -export class GetSettings implements UseCaseInterface { +export class GetSettings implements UseCaseInterface> { constructor( - @inject(TYPES.Auth_SettingRepository) private settingRepository: SettingRepositoryInterface, - @inject(TYPES.Auth_SubscriptionSettingRepository) - private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface, - @inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, - @inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector, - @inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector, - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_Crypter) private crypter: CrypterInterface, + private settingRepository: SettingRepositoryInterface, + private settingCrypter: SettingCrypterInterface, ) {} - async execute(dto: GetSettingsDto): Promise { + async execute(dto: GetSettingsDTO): Promise>> { const userUuidOrError = Uuid.create(dto.userUuid) if (userUuidOrError.isFailed()) { - return { - success: false, - error: { - message: userUuidOrError.getError(), - }, - } + return Result.fail(userUuidOrError.getError()) } const userUuid = userUuidOrError.getValue() - const user = await this.userRepository.findOneByUuid(userUuid) + const settings = await this.settingRepository.findAllByUserUuid(userUuid.value) - if (user === null) { - return { - success: false, - error: { - message: `User ${userUuid.value} not found.`, - }, - } - } + const unsensitiveSettings = settings.filter((setting) => !setting.props.sensitive) - let settings: Array - settings = await this.settingRepository.findAllByUserUuid(user.uuid) + if (dto.decrypted) { + const result = [] + for (const setting of unsensitiveSettings) { + const decryptedValue = await this.settingCrypter.decryptSettingValue(setting, userUuid.value) - const { regularSubscription, sharedSubscription } = - await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid) - const subscription = sharedSubscription ?? regularSubscription - if (subscription) { - const subscriptionSettings = await this.subscriptionSettingRepository.findAllBySubscriptionUuid(subscription.uuid) - settings = settings.concat(subscriptionSettings) - } - - if (dto.settingName !== undefined) { - settings = settings.filter((setting: Setting | SubscriptionSetting) => setting.name === dto.settingName) - } - - if (dto.updatedAfter !== undefined) { - settings = settings.filter( - (setting: Setting | SubscriptionSetting) => setting.updatedAt >= (dto.updatedAfter as number), - ) - } - - if (!dto.allowSensitiveRetrieval) { - settings = settings.filter((setting: Setting | SubscriptionSetting) => !setting.sensitive) - } - - const simpleSettings: Array = [] - for (const setting of settings) { - if (setting.value !== null && setting.serverEncryptionVersion === EncryptionVersion.Default) { - setting.value = await this.crypter.decryptForUser(setting.value, user) + result.push({ + setting, + decryptedValue, + }) } - if (setting instanceof SubscriptionSetting) { - simpleSettings.push(await this.subscriptionSettingProjector.projectSimple(setting)) - } else { - simpleSettings.push(await this.settingProjector.projectSimple(setting)) - } + return Result.ok(result) } - return { - success: true, - userUuid: user.uuid, - settings: simpleSettings, - } + return Result.ok( + unsensitiveSettings.map((setting) => ({ + setting, + })), + ) } } diff --git a/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsDTO.ts b/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsDTO.ts new file mode 100644 index 000000000..324d44a02 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsDTO.ts @@ -0,0 +1,4 @@ +export interface GetSettingsDTO { + userUuid: string + decrypted: boolean +} diff --git a/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsDto.ts b/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsDto.ts deleted file mode 100644 index b37ae8652..000000000 --- a/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsDto.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type GetSettingsDto = { - userUuid: string - settingName?: string - allowSensitiveRetrieval?: boolean - updatedAfter?: number -} diff --git a/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsResponse.ts b/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsResponse.ts deleted file mode 100644 index a258c2115..000000000 --- a/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SimpleSetting } from '../../Setting/SimpleSetting' -import { SimpleSubscriptionSetting } from '../../Setting/SimpleSubscriptionSetting' - -export type GetSettingsResponse = - | { - success: true - userUuid: string - settings: Array - } - | { - success: false - error: { - message: string - } - } diff --git a/packages/auth/src/Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser.spec.ts b/packages/auth/src/Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser.spec.ts new file mode 100644 index 000000000..8b3ae85d0 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser.spec.ts @@ -0,0 +1,57 @@ +import { Result } from '@standardnotes/domain-core' +import { UserSubscription } from '../../Subscription/UserSubscription' +import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' +import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser' +import { GetSharedOrRegularSubscriptionForUser } from './GetSharedOrRegularSubscriptionForUser' + +describe('GetSharedOrRegularSubscriptionForUser', () => { + let getRegularSubscriptionForUser: GetRegularSubscriptionForUser + let getSharedSubscriptionForUser: GetSharedSubscriptionForUser + + const createUseCase = () => + new GetSharedOrRegularSubscriptionForUser(getRegularSubscriptionForUser, getSharedSubscriptionForUser) + + beforeEach(() => { + getRegularSubscriptionForUser = {} as jest.Mocked + getRegularSubscriptionForUser.execute = jest + .fn() + .mockReturnValue(Result.ok({ subscriptionType: UserSubscriptionType.Regular } as jest.Mocked)) + + getSharedSubscriptionForUser = {} as jest.Mocked + getSharedSubscriptionForUser.execute = jest + .fn() + .mockReturnValue(Result.ok({ subscriptionType: UserSubscriptionType.Shared } as jest.Mocked)) + }) + + it('should return a shared subscription', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + expect(result.getValue().subscriptionType).toEqual(UserSubscriptionType.Shared) + }) + + it('should return a regular subscription', async () => { + getSharedSubscriptionForUser.execute = jest.fn().mockReturnValue(Result.fail('not found')) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + expect(result.getValue().subscriptionType).toEqual(UserSubscriptionType.Regular) + }) + + it('should return error if both shared and regular subscriptions are not found', async () => { + getSharedSubscriptionForUser.execute = jest.fn().mockReturnValue(Result.fail('not found')) + getRegularSubscriptionForUser.execute = jest.fn().mockReturnValue(Result.fail('not found')) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeTruthy() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser.ts b/packages/auth/src/Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser.ts new file mode 100644 index 000000000..b9b591eb9 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser.ts @@ -0,0 +1,23 @@ +import { Result, UseCaseInterface } from '@standardnotes/domain-core' + +import { UserSubscription } from '../../Subscription/UserSubscription' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' +import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser' +import { GetSharedOrRegularSubscriptionForUserDTO } from './GetSharedOrRegularSubscriptionForUserDTO' + +export class GetSharedOrRegularSubscriptionForUser implements UseCaseInterface { + constructor( + private getRegularSubscriptionForUser: GetRegularSubscriptionForUser, + private getSharedSubscriptionForUser: GetSharedSubscriptionForUser, + ) {} + + async execute(dto: GetSharedOrRegularSubscriptionForUserDTO): Promise> { + const sharedSubscriptionOrError = await this.getSharedSubscriptionForUser.execute(dto) + if (sharedSubscriptionOrError.isFailed()) { + return this.getRegularSubscriptionForUser.execute(dto) + } + const sharedSubscription = sharedSubscriptionOrError.getValue() + + return Result.ok(sharedSubscription) + } +} diff --git a/packages/auth/src/Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUserDTO.ts b/packages/auth/src/Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUserDTO.ts new file mode 100644 index 000000000..15645df63 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUserDTO.ts @@ -0,0 +1,3 @@ +export interface GetSharedOrRegularSubscriptionForUserDTO { + userUuid: string +} diff --git a/packages/auth/src/Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser.spec.ts b/packages/auth/src/Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser.spec.ts new file mode 100644 index 000000000..932e25f89 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser.spec.ts @@ -0,0 +1,44 @@ +import { UserSubscription } from '../../Subscription/UserSubscription' +import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface' +import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' +import { GetSharedSubscriptionForUser } from './GetSharedSubscriptionForUser' + +describe('GetSharedSubscriptionForUser', () => { + let userSubscriptionRepository: UserSubscriptionRepositoryInterface + + const createUseCase = () => new GetSharedSubscriptionForUser(userSubscriptionRepository) + + beforeEach(() => { + userSubscriptionRepository = {} as jest.Mocked + userSubscriptionRepository.findOneByUserUuidAndType = jest.fn().mockReturnValue({ + subscriptionType: UserSubscriptionType.Shared, + } as jest.Mocked) + }) + + it('should return a shared subscription', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + expect(result.getValue().subscriptionType).toEqual(UserSubscriptionType.Shared) + }) + + it('should return error if user subscription is not found', async () => { + userSubscriptionRepository.findOneByUserUuidAndType = jest.fn().mockReturnValue(null) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return error if user uuid is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: 'invalid', + }) + + expect(result.isFailed()).toBeTruthy() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser.ts b/packages/auth/src/Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser.ts new file mode 100644 index 000000000..9c39fcd69 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser.ts @@ -0,0 +1,27 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { UserSubscription } from '../../Subscription/UserSubscription' +import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface' +import { GetSharedSubscriptionForUserDTO } from './GetSharedSubscriptionForUserDTO' +import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' + +export class GetSharedSubscriptionForUser implements UseCaseInterface { + constructor(private userSubscriptionRepository: UserSubscriptionRepositoryInterface) {} + + async execute(dto: GetSharedSubscriptionForUserDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(`Could not get shared subscription for user: ${userUuidOrError.getError()}`) + } + const userUuid = userUuidOrError.getValue() + + const userSubscription = await this.userSubscriptionRepository.findOneByUserUuidAndType( + userUuid.value, + UserSubscriptionType.Shared, + ) + if (userSubscription === null) { + return Result.fail(`User subscription for user ${userUuid.value} not found.`) + } + + return Result.ok(userSubscription) + } +} diff --git a/packages/auth/src/Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUserDTO.ts b/packages/auth/src/Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUserDTO.ts new file mode 100644 index 000000000..b152684a4 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUserDTO.ts @@ -0,0 +1,3 @@ +export interface GetSharedSubscriptionForUserDTO { + userUuid: string +} diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.spec.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.spec.ts new file mode 100644 index 000000000..bdc4f735f --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.spec.ts @@ -0,0 +1,169 @@ +import { Timestamps, Uuid } from '@standardnotes/domain-core' +import { SettingName } from '@standardnotes/settings' +import { EncryptionVersion } from '../../Encryption/EncryptionVersion' +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' +import { GetSubscriptionSetting } from './GetSubscriptionSetting' + +describe('GetSubscriptionSetting', () => { + let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface + let settingCrypter: SettingCrypterInterface + + const createUseCase = () => new GetSubscriptionSetting(subscriptionSettingRepository, settingCrypter) + + beforeEach(() => { + const subscriptionSetting = SubscriptionSetting.create({ + sensitive: false, + name: SettingName.NAMES.FileUploadBytesLimit, + value: '100', + timestamps: Timestamps.create(123456789, 123456789).getValue(), + serverEncryptionVersion: EncryptionVersion.Unencrypted, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + }).getValue() + + subscriptionSettingRepository = {} as jest.Mocked + subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest + .fn() + .mockResolvedValue(subscriptionSetting) + + settingCrypter = {} as jest.Mocked + settingCrypter.decryptSubscriptionSettingValue = jest.fn().mockResolvedValue('decrypted') + }) + + it('returns error when setting name is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + settingName: 'invalid', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + allowSensitiveRetrieval: false, + }) + + expect(result.isFailed()).toBe(true) + }) + + it('returns error when user subscription uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + settingName: SettingName.NAMES.FileUploadBytesLimit, + userSubscriptionUuid: 'invalid', + allowSensitiveRetrieval: false, + }) + + expect(result.isFailed()).toBe(true) + }) + + it('returns error when setting is not found', async () => { + const useCase = createUseCase() + subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockResolvedValue(null) + + const result = await useCase.execute({ + settingName: SettingName.NAMES.FileUploadBytesLimit, + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + allowSensitiveRetrieval: false, + }) + + expect(result.isFailed()).toBe(true) + }) + + it('returns error when setting is sensitive and sensitive retrieval is not allowed', async () => { + const useCase = createUseCase() + subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockResolvedValue( + SubscriptionSetting.create({ + sensitive: true, + name: SettingName.NAMES.FileUploadBytesLimit, + value: '100', + timestamps: Timestamps.create(123456789, 123456789).getValue(), + serverEncryptionVersion: EncryptionVersion.Unencrypted, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + }).getValue(), + ) + + const result = await useCase.execute({ + settingName: SettingName.NAMES.FileUploadBytesLimit, + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + allowSensitiveRetrieval: false, + }) + + expect(result.isFailed()).toBe(true) + }) + + it('returns setting when setting is sensitive and sensitive retrieval is allowed', async () => { + const useCase = createUseCase() + subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockResolvedValue( + SubscriptionSetting.create({ + sensitive: true, + name: SettingName.NAMES.FileUploadBytesLimit, + value: '100', + timestamps: Timestamps.create(123456789, 123456789).getValue(), + serverEncryptionVersion: EncryptionVersion.Unencrypted, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + }).getValue(), + ) + + const result = await useCase.execute({ + settingName: SettingName.NAMES.FileUploadBytesLimit, + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + allowSensitiveRetrieval: true, + }) + + expect(result.isFailed()).toBe(false) + }) + + it('returns setting when setting is not sensitive', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + settingName: SettingName.NAMES.FileUploadBytesLimit, + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + allowSensitiveRetrieval: false, + }) + + expect(result.isFailed()).toBe(false) + }) + + it('return a decrypted setting', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + settingName: SettingName.NAMES.FileUploadBytesLimit, + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + allowSensitiveRetrieval: false, + decryptWith: { + userUuid: '00000000-0000-0000-0000-000000000000', + }, + }) + + expect(result.isFailed()).toBe(false) + expect(result.getValue().decryptedValue).toEqual('decrypted') + }) + + it('should return error if decrypting with invalid user uuid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + settingName: SettingName.NAMES.FileUploadBytesLimit, + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + allowSensitiveRetrieval: false, + decryptWith: { + userUuid: 'invalid', + }, + }) + + expect(result.isFailed()).toBe(true) + }) + + it('return error when setting name is not a subscription setting', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + settingName: SettingName.NAMES.EmailBackupFrequency, + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + allowSensitiveRetrieval: false, + }) + + expect(result.isFailed()).toBe(true) + }) +}) diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.ts new file mode 100644 index 000000000..a47c2adbd --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.ts @@ -0,0 +1,70 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' +import { GetSubscriptionSettingDTO } from './GetSubscriptionSettingDTO' +import { SettingName } from '@standardnotes/settings' +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' + +export class GetSubscriptionSetting + implements UseCaseInterface<{ setting: SubscriptionSetting; decryptedValue?: string | null }> +{ + constructor( + private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface, + private settingCrypter: SettingCrypterInterface, + ) {} + + async execute( + dto: GetSubscriptionSettingDTO, + ): Promise> { + const userSubscriptionUuidOrError = Uuid.create(dto.userSubscriptionUuid) + if (userSubscriptionUuidOrError.isFailed()) { + return Result.fail(`Could not get subscription setting: ${userSubscriptionUuidOrError.getError()}`) + } + const userSubscriptionUuid = userSubscriptionUuidOrError.getValue() + + const settingNameOrError = SettingName.create(dto.settingName) + if (settingNameOrError.isFailed()) { + return Result.fail(`Could not get subscription setting: ${settingNameOrError.getError()}`) + } + const settingName = settingNameOrError.getValue() + + if (!settingName.isASubscriptionSetting()) { + return Result.fail(`Setting ${settingName.value} is not a subscription setting!`) + } + + const subscriptionSetting = await this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid( + settingName.value, + userSubscriptionUuid, + ) + if (subscriptionSetting === null) { + return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userSubscriptionUuid} not found!`) + } + + if (subscriptionSetting.props.sensitive && !dto.allowSensitiveRetrieval) { + return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userSubscriptionUuid} is sensitive!`) + } + + if (dto.decryptWith) { + const userUuidOrError = Uuid.create(dto.decryptWith.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(`Could not get subscription setting: ${userUuidOrError.getError()}`) + } + const userUuid = userUuidOrError.getValue() + + const decryptedValue = await this.settingCrypter.decryptSubscriptionSettingValue( + subscriptionSetting, + userUuid.value, + ) + + return Result.ok({ + setting: subscriptionSetting, + decryptedValue, + }) + } + + return Result.ok({ + setting: subscriptionSetting, + }) + } +} diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSettingDTO.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSettingDTO.ts new file mode 100644 index 000000000..ced9fdc9b --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSettingDTO.ts @@ -0,0 +1,8 @@ +export interface GetSubscriptionSettingDTO { + userSubscriptionUuid: string + settingName: string + allowSensitiveRetrieval: boolean + decryptWith?: { + userUuid: string + } +} diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings.spec.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings.spec.ts new file mode 100644 index 000000000..fd8797482 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings.spec.ts @@ -0,0 +1,67 @@ +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' +import { GetSubscriptionSettings } from './GetSubscriptionSettings' + +describe('GetSubscriptionSettings', () => { + let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface + let subscriptionSetting: SubscriptionSetting + let settingCrypter: SettingCrypterInterface + + const createUseCase = () => new GetSubscriptionSettings(subscriptionSettingRepository, settingCrypter) + + beforeEach(() => { + subscriptionSetting = {} as jest.Mocked + + subscriptionSettingRepository = {} as jest.Mocked + subscriptionSettingRepository.findAllBySubscriptionUuid = jest.fn().mockResolvedValue([subscriptionSetting]) + + settingCrypter = {} as jest.Mocked + settingCrypter.decryptSubscriptionSettingValue = jest.fn().mockResolvedValue('decrypted') + }) + + it('should return subscription settings', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ userSubscriptionUuid: '00000000-0000-0000-0000-000000000000' }) + + expect(result.isFailed()).toBe(false) + expect(result.getValue()).toEqual([{ setting: subscriptionSetting }]) + }) + + it('should return decrypted subscription settings', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + decryptWith: { + userUuid: '00000000-0000-0000-0000-000000000000', + }, + }) + + expect(result.isFailed()).toBe(false) + expect(result.getValue()).toEqual([{ setting: subscriptionSetting, decryptedValue: 'decrypted' }]) + }) + + it('should return error when trying to decrypt setting with an invalid user uuid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + decryptWith: { + userUuid: 'invalid', + }, + }) + + expect(result.isFailed()).toBe(true) + }) + + it('should return error if user subscription uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ userSubscriptionUuid: 'invalid' }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Given value is not a valid uuid: invalid') + }) +}) diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings.ts new file mode 100644 index 000000000..b0717e71f --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings.ts @@ -0,0 +1,55 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' +import { GetSubscriptionSettingsDTO } from './GetSubscriptionSettingsDTO' +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' + +export class GetSubscriptionSettings + implements UseCaseInterface> +{ + constructor( + private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface, + private settingCrypter: SettingCrypterInterface, + ) {} + + async execute( + dto: GetSubscriptionSettingsDTO, + ): Promise>> { + const userSubscriptionUuidOrError = Uuid.create(dto.userSubscriptionUuid) + if (userSubscriptionUuidOrError.isFailed()) { + return Result.fail(userSubscriptionUuidOrError.getError()) + } + const userSubscriptionUuid = userSubscriptionUuidOrError.getValue() + + const subscriptionSettings = + await this.subscriptionSettingRepository.findAllBySubscriptionUuid(userSubscriptionUuid) + + if (dto.decryptWith) { + const userUuidOrError = Uuid.create(dto.decryptWith.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + const result = [] + for (const subscriptionSetting of subscriptionSettings) { + const decryptedValue = await this.settingCrypter.decryptSubscriptionSettingValue( + subscriptionSetting, + userUuid.value, + ) + result.push({ + setting: subscriptionSetting, + decryptedValue, + }) + } + + return Result.ok(result) + } + + return Result.ok( + subscriptionSettings.map((subscriptionSetting) => ({ + setting: subscriptionSetting, + })), + ) + } +} diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettingsDTO.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettingsDTO.ts new file mode 100644 index 000000000..f202d41b7 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettingsDTO.ts @@ -0,0 +1,6 @@ +export interface GetSubscriptionSettingsDTO { + userSubscriptionUuid: string + decryptWith?: { + userUuid: string + } +} diff --git a/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParams.spec.ts b/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParams.spec.ts index 72d7cbac6..d7fc24eec 100644 --- a/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParams.spec.ts +++ b/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParams.spec.ts @@ -1,20 +1,20 @@ +import { Result } from '@standardnotes/domain-core' import { Setting } from '../../Setting/Setting' -import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' import { KeyParamsFactoryInterface } from '../../User/KeyParamsFactoryInterface' import { PKCERepositoryInterface } from '../../User/PKCERepositoryInterface' import { User } from '../../User/User' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' +import { GetSetting } from '../GetSetting/GetSetting' import { GetUserKeyParamsRecovery } from './GetUserKeyParamsRecovery' describe('GetUserKeyParamsRecovery', () => { let keyParamsFactory: KeyParamsFactoryInterface let userRepository: UserRepositoryInterface - let settingService: SettingServiceInterface + let getSetting: GetSetting let user: User let pkceRepository: PKCERepositoryInterface - const createUseCase = () => - new GetUserKeyParamsRecovery(keyParamsFactory, userRepository, pkceRepository, settingService) + const createUseCase = () => new GetUserKeyParamsRecovery(keyParamsFactory, userRepository, pkceRepository, getSetting) beforeEach(() => { keyParamsFactory = {} as jest.Mocked @@ -26,8 +26,10 @@ describe('GetUserKeyParamsRecovery', () => { userRepository = {} as jest.Mocked userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - settingService = {} as jest.Mocked - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({ value: 'foo' } as Setting) + getSetting = {} as jest.Mocked + getSetting.execute = jest + .fn() + .mockReturnValue(Result.ok({ setting: {} as jest.Mocked, decryptedValue: 'foo' })) pkceRepository = {} as jest.Mocked pkceRepository.storeCodeChallenge = jest.fn() @@ -80,7 +82,7 @@ describe('GetUserKeyParamsRecovery', () => { }) it('should return error if user has no recovery codes generated', async () => { - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) + getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) const result = await createUseCase().execute({ username: 'username', diff --git a/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery.ts b/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery.ts index c7822d682..1276176b3 100644 --- a/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery.ts +++ b/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery.ts @@ -7,14 +7,14 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { GetUserKeyParamsRecoveryDTO } from './GetUserKeyParamsRecoveryDTO' import { User } from '../../User/User' import { PKCERepositoryInterface } from '../../User/PKCERepositoryInterface' -import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' +import { GetSetting } from '../GetSetting/GetSetting' export class GetUserKeyParamsRecovery implements UseCaseInterface { constructor( private keyParamsFactory: KeyParamsFactoryInterface, private userRepository: UserRepositoryInterface, private pkceRepository: PKCERepositoryInterface, - private settingService: SettingServiceInterface, + private getSetting: GetSetting, ) {} async execute(dto: GetUserKeyParamsRecoveryDTO): Promise> { @@ -39,15 +39,18 @@ export class GetUserKeyParamsRecovery implements UseCaseInterface return Result.ok(this.keyParamsFactory.createPseudoParams(username.value)) } - const recoveryCodesSetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(), + const recoveryCodesSettingOrError = await this.getSetting.execute({ + settingName: SettingName.NAMES.RecoveryCodes, userUuid: user.uuid, + allowSensitiveRetrieval: true, + decrypted: true, }) - if (!recoveryCodesSetting) { + if (recoveryCodesSettingOrError.isFailed()) { return Result.fail('User does not have recovery codes generated') } - if (recoveryCodesSetting.value !== dto.recoveryCodes) { + const recoveryCodesSetting = recoveryCodesSettingOrError.getValue() + if (recoveryCodesSetting.decryptedValue !== dto.recoveryCodes) { return Result.fail('Invalid recovery codes') } diff --git a/packages/auth/src/Domain/UseCase/Register.spec.ts b/packages/auth/src/Domain/UseCase/Register.spec.ts index f90c9910c..985ac0d3f 100644 --- a/packages/auth/src/Domain/UseCase/Register.spec.ts +++ b/packages/auth/src/Domain/UseCase/Register.spec.ts @@ -8,26 +8,32 @@ import { User } from '../User/User' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' import { Register } from './Register' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115' import { Session } from '../Session/Session' -import { RoleName } from '@standardnotes/domain-core' +import { Result, RoleName } from '@standardnotes/domain-core' +import { ApplyDefaultSettings } from './ApplyDefaultSettings/ApplyDefaultSettings' describe('Register', () => { let userRepository: UserRepositoryInterface let roleRepository: RoleRepositoryInterface let authResponseFactory: AuthResponseFactory20200115 - let settingService: SettingServiceInterface + let applyDefaultSettings: ApplyDefaultSettings let user: User let crypter: CrypterInterface let timer: TimerInterface const createUseCase = () => - new Register(userRepository, roleRepository, authResponseFactory, crypter, false, settingService, timer) + new Register(userRepository, roleRepository, authResponseFactory, crypter, false, timer, applyDefaultSettings) beforeEach(() => { userRepository = {} as jest.Mocked - userRepository.save = jest.fn() + userRepository.save = jest.fn().mockImplementation((user: User) => { + user.uuid = 'test' + user.createdAt = new Date(1) + user.updatedAt = new Date(1) + + return user + }) userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) roleRepository = {} as jest.Mocked @@ -43,8 +49,8 @@ describe('Register', () => { user = {} as jest.Mocked - settingService = {} as jest.Mocked - settingService.applyDefaultSettingsUponRegistration = jest.fn() + applyDefaultSettings = {} as jest.Mocked + applyDefaultSettings.execute = jest.fn().mockReturnValue(Result.ok()) timer = {} as jest.Mocked timer.getUTCDate = jest.fn().mockReturnValue(new Date(1)) @@ -81,7 +87,7 @@ describe('Register', () => { updatedAt: new Date(1), }) - expect(settingService.applyDefaultSettingsUponRegistration).toHaveBeenCalled() + expect(applyDefaultSettings.execute).toHaveBeenCalled() }) it('should register a new user with default set of roles', async () => { @@ -121,6 +127,27 @@ describe('Register', () => { }) }) + it('should fail to register if applying default settings fails', async () => { + applyDefaultSettings.execute = jest.fn().mockReturnValue(Result.fail('error')) + + expect( + await createUseCase().execute({ + email: 'test@test.te', + password: 'asdzxc', + updatedWithUserAgent: 'Mozilla', + apiVersion: '20200115', + ephemeralSession: false, + version: '004', + pwCost: 11, + pwSalt: 'qweqwe', + pwNonce: undefined, + }), + ).toEqual({ + success: false, + errorMessage: 'error', + }) + }) + it('should fail to register if username is invalid', async () => { expect( await createUseCase().execute({ @@ -196,8 +223,8 @@ describe('Register', () => { authResponseFactory, crypter, true, - settingService, timer, + applyDefaultSettings, ).execute({ email: 'test@test.te', password: 'asdzxc', diff --git a/packages/auth/src/Domain/UseCase/Register.ts b/packages/auth/src/Domain/UseCase/Register.ts index 998b4f06e..64684e77e 100644 --- a/packages/auth/src/Domain/UseCase/Register.ts +++ b/packages/auth/src/Domain/UseCase/Register.ts @@ -1,10 +1,8 @@ import * as bcrypt from 'bcryptjs' import { RoleName, Username } from '@standardnotes/domain-core' import { v4 as uuidv4 } from 'uuid' -import { inject, injectable } from 'inversify' import { TimerInterface } from '@standardnotes/time' -import TYPES from '../../Bootstrap/Types' import { User } from '../User/User' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' import { RegisterDTO } from './RegisterDTO' @@ -12,21 +10,20 @@ import { RegisterResponse } from './RegisterResponse' import { UseCaseInterface } from './UseCaseInterface' import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface' import { CrypterInterface } from '../Encryption/CrypterInterface' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115' import { AuthResponse20200115 } from '../Auth/AuthResponse20200115' import { ApiVersion } from '../Api/ApiVersion' +import { ApplyDefaultSettings } from './ApplyDefaultSettings/ApplyDefaultSettings' -@injectable() export class Register implements UseCaseInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_RoleRepository) private roleRepository: RoleRepositoryInterface, - @inject(TYPES.Auth_AuthResponseFactory20200115) private authResponseFactory20200115: AuthResponseFactory20200115, - @inject(TYPES.Auth_Crypter) private crypter: CrypterInterface, - @inject(TYPES.Auth_DISABLE_USER_REGISTRATION) private disableUserRegistration: boolean, - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_Timer) private timer: TimerInterface, + private userRepository: UserRepositoryInterface, + private roleRepository: RoleRepositoryInterface, + private authResponseFactory20200115: AuthResponseFactory20200115, + private crypter: CrypterInterface, + private disableUserRegistration: boolean, + private timer: TimerInterface, + private applyDefaultSettings: ApplyDefaultSettings, ) {} async execute(dto: RegisterDTO): Promise { @@ -83,7 +80,16 @@ export class Register implements UseCaseInterface { user = await this.userRepository.save(user) - await this.settingService.applyDefaultSettingsUponRegistration(user) + const settingsApplicationResult = await this.applyDefaultSettings.execute({ + userName: user.email, + userUuid: user.uuid, + }) + if (settingsApplicationResult.isFailed()) { + return { + success: false, + errorMessage: settingsApplicationResult.getError(), + } + } const result = await this.authResponseFactory20200115.createResponse({ user, diff --git a/packages/auth/src/Domain/UseCase/SetSettingValue/SetSettingValue.spec.ts b/packages/auth/src/Domain/UseCase/SetSettingValue/SetSettingValue.spec.ts new file mode 100644 index 000000000..d9beed51c --- /dev/null +++ b/packages/auth/src/Domain/UseCase/SetSettingValue/SetSettingValue.spec.ts @@ -0,0 +1,192 @@ +import { TimerInterface } from '@standardnotes/time' +import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' +import { GetSetting } from '../GetSetting/GetSetting' +import { SetSettingValue } from './SetSettingValue' +import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface' +import { RoleServiceInterface } from '../../Role/RoleServiceInterface' +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' +import { Result, Timestamps, Uuid } from '@standardnotes/domain-core' +import { EncryptionVersion } from '../../Encryption/EncryptionVersion' +import { SettingName } from '@standardnotes/settings' +import { PermissionName } from '@standardnotes/features' +import { Setting } from '../../Setting/Setting' + +describe('SetSettingValue', () => { + let getSetting: GetSetting + let settingRepository: SettingRepositoryInterface + let timer: TimerInterface + let settingsAssociationService: SettingsAssociationServiceInterface + let roleService: RoleServiceInterface + let settingCrypter: SettingCrypterInterface + + const createUseCase = () => + new SetSettingValue(getSetting, settingRepository, timer, settingsAssociationService, roleService, settingCrypter) + + beforeEach(() => { + getSetting = {} as jest.Mocked + getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) + + settingRepository = {} as jest.Mocked + settingRepository.insert = jest.fn() + settingRepository.update = jest.fn() + + timer = {} as jest.Mocked + timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123) + + settingsAssociationService = {} as jest.Mocked + settingsAssociationService.getSensitivityForSetting = jest.fn().mockReturnValue(false) + settingsAssociationService.getEncryptionVersionForSetting = jest.fn().mockReturnValue(EncryptionVersion.Unencrypted) + settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(true) + settingsAssociationService.getPermissionAssociatedWithSetting = jest.fn().mockReturnValue(undefined) + + roleService = {} as jest.Mocked + roleService.userHasPermission = jest.fn().mockReturnValue(false) + + settingCrypter = {} as jest.Mocked + settingCrypter.encryptValue = jest.fn().mockReturnValue('encrypted') + }) + + it('should return error when user uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: 'invalid', + settingName: SettingName.NAMES.MfaSecret, + value: 'value', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Given value is not a valid uuid: invalid') + }) + + it('should return error when setting name is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: 'invalid', + value: 'value', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Invalid setting name: invalid') + }) + + it('should return error when setting name is a subscription setting', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MuteSignInEmails, + value: 'value', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Setting MUTE_SIGN_IN_EMAILS is a subscription setting!') + }) + + it('should return error if the setting is not mutable by client', async () => { + settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(false) + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.ListedAuthorSecrets, + value: 'value', + }) + + expect(result.isFailed()).toBe(true) + }) + + it('should return error if user does not have permission to update setting', async () => { + settingsAssociationService.getPermissionAssociatedWithSetting = jest + .fn() + .mockReturnValue(PermissionName.FilesLowStorageTier) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MfaSecret, + value: 'value', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe( + 'User 00000000-0000-0000-0000-000000000000 does not have permission to update setting MFA_SECRET.', + ) + }) + + it('should update an existing setting', async () => { + const setting = Setting.create({ + name: SettingName.NAMES.MfaSecret, + value: '1243359u42395834', + serverEncryptionVersion: EncryptionVersion.Default, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: true, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + getSetting.execute = jest.fn().mockReturnValue(Result.ok({ setting })) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MfaSecret, + value: 'value', + }) + + expect(result.isFailed()).toBe(false) + expect(settingRepository.update).toHaveBeenCalled() + }) + + it('should insert a new setting if one does not exist', async () => { + getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MfaSecret, + value: 'value', + }) + + expect(result.isFailed()).toBe(false) + expect(settingRepository.insert).toHaveBeenCalled() + }) + + it('should insert a new setting with encrypted value if encryption version is default', async () => { + settingsAssociationService.getEncryptionVersionForSetting = jest.fn().mockReturnValue(EncryptionVersion.Default) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MfaSecret, + value: 'value', + }) + + expect(result.isFailed()).toBe(false) + expect(settingRepository.insert).toHaveBeenCalled() + expect(settingCrypter.encryptValue).toHaveBeenCalled() + }) + + it('should return error if new setting cannot be created', async () => { + getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) + const mock = jest.spyOn(Setting, 'create') + mock.mockReturnValue(Result.fail('Oops')) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MfaSecret, + value: 'value', + }) + + expect(result.isFailed()).toBe(true) + + mock.mockRestore() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/SetSettingValue/SetSettingValue.ts b/packages/auth/src/Domain/UseCase/SetSettingValue/SetSettingValue.ts new file mode 100644 index 000000000..47eed0349 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/SetSettingValue/SetSettingValue.ts @@ -0,0 +1,108 @@ +import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { SettingName } from '@standardnotes/settings' +import { TimerInterface } from '@standardnotes/time' + +import { Setting } from '../../Setting/Setting' +import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface' +import { GetSetting } from '../GetSetting/GetSetting' +import { SetSettingValueDTO } from './SetSettingValueDTO' +import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface' +import { RoleServiceInterface } from '../../Role/RoleServiceInterface' +import { EncryptionVersion } from '../../Encryption/EncryptionVersion' +import { SettingCrypterInterface } from '../../Setting/SettingCrypterInterface' + +export class SetSettingValue implements UseCaseInterface { + constructor( + private getSetting: GetSetting, + private settingRepository: SettingRepositoryInterface, + private timer: TimerInterface, + private settingsAssociationService: SettingsAssociationServiceInterface, + private roleService: RoleServiceInterface, + private settingCrypter: SettingCrypterInterface, + ) {} + + async execute(dto: SetSettingValueDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + const settingNameOrError = SettingName.create(dto.settingName) + if (settingNameOrError.isFailed()) { + return Result.fail(settingNameOrError.getError()) + } + const settingName = settingNameOrError.getValue() + + if (settingName.isASubscriptionSetting()) { + return Result.fail(`Setting ${settingName.value} is a subscription setting!`) + } + + if (!(await this.userHasPermissionToUpdateSetting(userUuid, settingName))) { + return Result.fail(`User ${userUuid.value} does not have permission to update setting ${settingName.value}.`) + } + + const settingExists = await this.getSetting.execute({ + userUuid: userUuid.value, + settingName: settingName.value, + allowSensitiveRetrieval: true, + decrypted: false, + }) + + const sensitive = this.settingsAssociationService.getSensitivityForSetting(settingName) + + const encryptionVersion = this.settingsAssociationService.getEncryptionVersionForSetting(settingName) + let settingValue = dto.value + if (encryptionVersion === EncryptionVersion.Default) { + settingValue = await this.settingCrypter.encryptValue(settingValue, userUuid) + } + + if (settingExists.isFailed()) { + const settingOrError = Setting.create({ + name: settingName.value, + value: settingValue, + userUuid, + serverEncryptionVersion: encryptionVersion, + sensitive, + timestamps: Timestamps.create( + this.timer.getTimestampInMicroseconds(), + this.timer.getTimestampInMicroseconds(), + ).getValue(), + }) + if (settingOrError.isFailed()) { + return Result.fail(settingOrError.getError()) + } + const setting = settingOrError.getValue() + + await this.settingRepository.insert(setting) + + return Result.ok(setting) + } + + const { setting } = settingExists.getValue() + setting.props.value = settingValue + setting.props.timestamps = Timestamps.create( + setting.props.timestamps.createdAt, + this.timer.getTimestampInMicroseconds(), + ).getValue() + + await this.settingRepository.update(setting) + + return Result.ok(setting) + } + + private async userHasPermissionToUpdateSetting(userUuid: Uuid, settingName: SettingName): Promise { + const settingIsMutableByClient = this.settingsAssociationService.isSettingMutableByClient(settingName) + if (!settingIsMutableByClient) { + return false + } + + const permissionAssociatedWithSetting = + this.settingsAssociationService.getPermissionAssociatedWithSetting(settingName) + if (permissionAssociatedWithSetting === undefined) { + return true + } + + return this.roleService.userHasPermission(userUuid.value, permissionAssociatedWithSetting) + } +} diff --git a/packages/auth/src/Domain/UseCase/SetSettingValue/SetSettingValueDTO.ts b/packages/auth/src/Domain/UseCase/SetSettingValue/SetSettingValueDTO.ts new file mode 100644 index 000000000..3645497d2 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/SetSettingValue/SetSettingValueDTO.ts @@ -0,0 +1,5 @@ +export interface SetSettingValueDTO { + settingName: string + userUuid: string + value: string | null +} diff --git a/packages/auth/src/Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue.spec.ts b/packages/auth/src/Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue.spec.ts new file mode 100644 index 000000000..402bd317e --- /dev/null +++ b/packages/auth/src/Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue.spec.ts @@ -0,0 +1,157 @@ +import { TimerInterface } from '@standardnotes/time' +import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' +import { SetSubscriptionSettingValue } from './SetSubscriptionSettingValue' +import { Result, Timestamps, Uuid } from '@standardnotes/domain-core' +import { SettingName } from '@standardnotes/settings' +import { EncryptionVersion } from '../../Encryption/EncryptionVersion' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' + +describe('SetSubscriptionSettingValue', () => { + let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface + let getSubscriptionSetting: GetSubscriptionSetting + let timer: TimerInterface + + const createUseCase = () => + new SetSubscriptionSettingValue(subscriptionSettingRepository, getSubscriptionSetting, timer) + + beforeEach(() => { + subscriptionSettingRepository = {} as jest.Mocked + subscriptionSettingRepository.insert = jest.fn() + subscriptionSettingRepository.update = jest.fn() + + getSubscriptionSetting = {} as jest.Mocked + getSubscriptionSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) + + timer = {} as jest.Mocked + timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123) + }) + + it('should return error when user subscription uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: 'invalid', + settingName: SettingName.NAMES.MuteSignInEmails, + value: 'value', + }) + + expect(result.isFailed()).toBe(true) + }) + + it('should return error when setting name is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + settingName: 'invalid', + value: 'value', + }) + + expect(result.isFailed()).toBe(true) + }) + + it('should return error when setting name is not a subscription setting', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MfaSecret, + value: 'value', + }) + + expect(result.isFailed()).toBe(true) + }) + + it('should return error when provided new subscription uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MuteSignInEmails, + value: 'value', + newUserSubscriptionUuid: 'invalid', + }) + + expect(result.isFailed()).toBe(true) + }) + + it('should update an existing subscription setting', async () => { + const setting = SubscriptionSetting.create({ + name: SettingName.NAMES.MuteSignInEmails, + value: 'encrypted', + sensitive: true, + serverEncryptionVersion: EncryptionVersion.Default, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + getSubscriptionSetting.execute = jest.fn().mockReturnValue(Result.ok({ setting })) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MuteSignInEmails, + value: 'encrypted', + }) + + expect(result.isFailed()).toBe(false) + expect(subscriptionSettingRepository.update).toHaveBeenCalled() + }) + + it('should insert a new subscription setting', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MuteSignInEmails, + value: 'encrypted', + }) + + expect(result.isFailed()).toBe(false) + expect(subscriptionSettingRepository.insert).toHaveBeenCalled() + }) + + it('should return error if subscription setting could not be created', async () => { + const mock = jest.spyOn(SubscriptionSetting, 'create') + mock.mockReturnValue(Result.fail('Oops')) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MuteSignInEmails, + value: 'encrypted', + }) + + expect(result.isFailed()).toBe(true) + + mock.mockRestore() + }) + + it('should update an existing subscription setting with a new user subscription uuid', async () => { + const setting = SubscriptionSetting.create({ + name: SettingName.NAMES.MuteSignInEmails, + value: 'encrypted', + sensitive: true, + serverEncryptionVersion: EncryptionVersion.Default, + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + getSubscriptionSetting.execute = jest.fn().mockReturnValue(Result.ok({ setting })) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + settingName: SettingName.NAMES.MuteSignInEmails, + value: 'encrypted', + newUserSubscriptionUuid: '00000000-0000-0000-0000-000000000001', + }) + + expect(result.isFailed()).toBe(false) + expect(subscriptionSettingRepository.update).toHaveBeenCalled() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue.ts b/packages/auth/src/Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue.ts new file mode 100644 index 000000000..fa532f381 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue.ts @@ -0,0 +1,87 @@ +import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { TimerInterface } from '@standardnotes/time' +import { SettingName } from '@standardnotes/settings' + +import { SetSubscriptionSettingValueDTO } from './SetSubscriptionSettingValueDTO' +import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { EncryptionVersion } from '../../Encryption/EncryptionVersion' + +export class SetSubscriptionSettingValue implements UseCaseInterface { + constructor( + private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface, + private getSubscriptionSetting: GetSubscriptionSetting, + private timer: TimerInterface, + ) {} + + async execute(dto: SetSubscriptionSettingValueDTO): Promise> { + const userSubscriptionUuidOrError = Uuid.create(dto.userSubscriptionUuid) + if (userSubscriptionUuidOrError.isFailed()) { + return Result.fail(userSubscriptionUuidOrError.getError()) + } + const userSubscriptionUuid = userSubscriptionUuidOrError.getValue() + + let newUserSubscriptionUuid: Uuid | undefined + if (dto.newUserSubscriptionUuid !== undefined) { + const newUserSubscriptionUuidOrError = Uuid.create(dto.newUserSubscriptionUuid) + if (newUserSubscriptionUuidOrError.isFailed()) { + return Result.fail(newUserSubscriptionUuidOrError.getError()) + } + newUserSubscriptionUuid = newUserSubscriptionUuidOrError.getValue() + } + + const settingNameOrError = SettingName.create(dto.settingName) + if (settingNameOrError.isFailed()) { + return Result.fail(settingNameOrError.getError()) + } + const settingName = settingNameOrError.getValue() + + if (!settingName.isASubscriptionSetting()) { + return Result.fail(`Setting ${settingName.value} is not a subscription setting!`) + } + + const settingExists = await this.getSubscriptionSetting.execute({ + userSubscriptionUuid: userSubscriptionUuid.value, + settingName: settingName.value, + allowSensitiveRetrieval: true, + }) + if (settingExists.isFailed()) { + const timestamps = Timestamps.create( + this.timer.getTimestampInMicroseconds(), + this.timer.getTimestampInMicroseconds(), + ).getValue() + + const subscriptionSettingOrError = SubscriptionSetting.create({ + name: settingName.value, + value: dto.value, + userSubscriptionUuid: newUserSubscriptionUuid ?? userSubscriptionUuid, + serverEncryptionVersion: EncryptionVersion.Unencrypted, + timestamps, + sensitive: false, + }) + if (subscriptionSettingOrError.isFailed()) { + return Result.fail(subscriptionSettingOrError.getError()) + } + const subscriptionSetting = subscriptionSettingOrError.getValue() + + await this.subscriptionSettingRepository.insert(subscriptionSetting) + + return Result.ok() + } + + const { setting } = settingExists.getValue() + setting.props.value = dto.value + setting.props.timestamps = Timestamps.create( + setting.props.timestamps.createdAt, + this.timer.getTimestampInMicroseconds(), + ).getValue() + if (newUserSubscriptionUuid !== undefined) { + setting.props.userSubscriptionUuid = newUserSubscriptionUuid + } + + await this.subscriptionSettingRepository.update(setting) + + return Result.ok() + } +} diff --git a/packages/auth/src/Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValueDTO.ts b/packages/auth/src/Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValueDTO.ts new file mode 100644 index 000000000..f2995dfdc --- /dev/null +++ b/packages/auth/src/Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValueDTO.ts @@ -0,0 +1,6 @@ +export interface SetSubscriptionSettingValueDTO { + settingName: string + userSubscriptionUuid: string + value: string | null + newUserSubscriptionUuid?: string +} diff --git a/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.spec.ts b/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.spec.ts index 6c4f3ecd5..7d55e5d58 100644 --- a/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.spec.ts +++ b/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.spec.ts @@ -5,7 +5,6 @@ import { AuthResponseFactory20200115 } from '../../Auth/AuthResponseFactory20200 import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' import { CrypterInterface } from '../../Encryption/CrypterInterface' import { Setting } from '../../Setting/Setting' -import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' import { PKCERepositoryInterface } from '../../User/PKCERepositoryInterface' import { User } from '../../User/User' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' @@ -14,18 +13,19 @@ import { DeleteSetting } from '../DeleteSetting/DeleteSetting' import { GenerateRecoveryCodes } from '../GenerateRecoveryCodes/GenerateRecoveryCodes' import { IncreaseLoginAttempts } from '../IncreaseLoginAttempts' import { SignInWithRecoveryCodes } from './SignInWithRecoveryCodes' +import { GetSetting } from '../GetSetting/GetSetting' describe('SignInWithRecoveryCodes', () => { let userRepository: UserRepositoryInterface let authResponseFactory: AuthResponseFactory20200115 let pkceRepository: PKCERepositoryInterface let crypter: CrypterInterface - let settingService: SettingServiceInterface let generateRecoveryCodes: GenerateRecoveryCodes let increaseLoginAttempts: IncreaseLoginAttempts let clearLoginAttempts: ClearLoginAttempts let deleteSetting: DeleteSetting let authenticatorRepository: AuthenticatorRepositoryInterface + let getSetting: GetSetting const createUseCase = () => new SignInWithRecoveryCodes( @@ -33,7 +33,7 @@ describe('SignInWithRecoveryCodes', () => { authResponseFactory, pkceRepository, crypter, - settingService, + getSetting, generateRecoveryCodes, increaseLoginAttempts, clearLoginAttempts, @@ -58,8 +58,10 @@ describe('SignInWithRecoveryCodes', () => { crypter.base64URLEncode = jest.fn().mockReturnValue('base64-url-encoded') crypter.sha256Hash = jest.fn().mockReturnValue('sha256-hashed') - settingService = {} as jest.Mocked - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({ value: 'foo' } as Setting) + getSetting = {} as jest.Mocked + getSetting.execute = jest + .fn() + .mockReturnValue(Result.ok({ setting: {} as jest.Mocked, decryptedValue: 'foo' })) generateRecoveryCodes = {} as jest.Mocked generateRecoveryCodes.execute = jest.fn().mockReturnValue(Result.ok('1234 5678')) @@ -186,7 +188,7 @@ describe('SignInWithRecoveryCodes', () => { }) it('should return error if recovery codes are not generated for user', async () => { - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) + getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) const result = await createUseCase().execute({ userAgent: 'user-agent', diff --git a/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts b/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts index b6b0f99a0..bc3978bbe 100644 --- a/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts +++ b/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts @@ -3,7 +3,6 @@ import { Result, UseCaseInterface, Username, Uuid, Validator } from '@standardno import { SettingName } from '@standardnotes/settings' import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115' -import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' import { CrypterInterface } from '../../Encryption/CrypterInterface' import { PKCERepositoryInterface } from '../../User/PKCERepositoryInterface' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' @@ -16,6 +15,7 @@ import { ClearLoginAttempts } from '../ClearLoginAttempts' import { DeleteSetting } from '../DeleteSetting/DeleteSetting' import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' import { ApiVersion } from '../../Api/ApiVersion' +import { GetSetting } from '../GetSetting/GetSetting' export class SignInWithRecoveryCodes implements UseCaseInterface { constructor( @@ -23,7 +23,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface { - let settingService: SettingServiceInterface - let settingProjection: SimpleSetting - let settingProjector: SettingProjector - let settingsAssociationService: SettingsAssociationServiceInterface - let setting: Setting - let user: User - let userRepository: UserRepositoryInterface - let roleService: RoleServiceInterface - let logger: Logger - let userSubscriptionService: UserSubscriptionServiceInterface - let subscriptionSettingProjector: SubscriptionSettingProjector - let subscriptionSettingService: SubscriptionSettingServiceInterface - let regularSubscription: UserSubscription - let sharedSubscription: UserSubscription - - const createUseCase = () => - new UpdateSetting( - settingService, - subscriptionSettingService, - userSubscriptionService, - settingProjector, - subscriptionSettingProjector, - settingsAssociationService, - userRepository, - roleService, - logger, - ) - - beforeEach(() => { - setting = {} as jest.Mocked - - settingService = {} as jest.Mocked - settingService.createOrReplace = jest.fn().mockReturnValue({ status: 'created', setting }) - - subscriptionSettingService = {} as jest.Mocked - subscriptionSettingService.createOrReplace = jest.fn().mockReturnValue({ status: 'created', setting }) - - settingProjector = {} as jest.Mocked - settingProjector.projectSimple = jest.fn().mockReturnValue(settingProjection) - - regularSubscription = { - uuid: '00000000-0000-0000-0000-000000000000', - subscriptionType: UserSubscriptionType.Regular, - user: Promise.resolve(user), - } as jest.Mocked - - sharedSubscription = { - uuid: '2-3-4', - subscriptionType: UserSubscriptionType.Shared, - user: Promise.resolve(user), - } as jest.Mocked - - userSubscriptionService = {} as jest.Mocked - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) - - subscriptionSettingProjector = {} as jest.Mocked - subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' }) - - user = {} as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUuid = jest.fn().mockReturnValue(user) - - settingsAssociationService = {} as jest.Mocked - settingsAssociationService.getPermissionAssociatedWithSetting = jest.fn().mockReturnValue(undefined) - settingsAssociationService.getEncryptionVersionForSetting = jest.fn().mockReturnValue(EncryptionVersion.Default) - settingsAssociationService.getSensitivityForSetting = jest.fn().mockReturnValue(false) - settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(true) - - roleService = {} as jest.Mocked - roleService.addUserRoleBasedOnSubscription = jest.fn() - - logger = {} as jest.Mocked - logger.debug = jest.fn() - logger.error = jest.fn() - }) - - describe('no subscription', () => { - it('should create a setting', async () => { - const props = { - name: SettingName.NAMES.ExtensionKey, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Default, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '00000000-0000-0000-0000-000000000000' }) - - expect(settingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'EXTENSION_KEY', - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: 1, - sensitive: false, - }, - user, - }) - - expect(response).toEqual({ - success: true, - setting: settingProjection, - statusCode: 201, - }) - }) - - it('should not create a setting if user does not exist', async () => { - userRepository.findOneByUuid = jest.fn().mockReturnValue(null) - - const props = { - name: SettingName.NAMES.ExtensionKey, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '00000000-0000-0000-0000-000000000000' }) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - - expect(response).toEqual({ - success: false, - error: { - message: 'User 00000000-0000-0000-0000-000000000000 not found.', - }, - statusCode: 404, - }) - }) - - it('should not create a setting if user uuid is invalid', async () => { - const props = { - name: SettingName.NAMES.ExtensionKey, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: 'invalid' }) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - - expect(response).toEqual({ - success: false, - error: { - message: 'Given value is not a valid uuid: invalid', - }, - statusCode: 400, - }) - }) - - it('should not create a subscription setting', async () => { - const props = { - name: SettingName.NAMES.MuteSignInEmails, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '00000000-0000-0000-0000-000000000000' }) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - - expect(response).toEqual({ - success: false, - error: { - message: 'User 00000000-0000-0000-0000-000000000000 has no subscription to change a subscription setting.', - }, - statusCode: 400, - }) - }) - - it('should not create a setting if the setting name is invalid', async () => { - const props = { - name: 'random-setting', - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '00000000-0000-0000-0000-000000000000' }) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - - expect(response).toEqual({ - success: false, - error: { - message: 'Invalid setting name: random-setting', - }, - statusCode: 400, - }) - }) - - it('should not create a setting if user is not permitted to', async () => { - settingsAssociationService.getPermissionAssociatedWithSetting = jest - .fn() - .mockReturnValue(PermissionName.DailyEmailBackup) - - roleService.userHasPermission = jest.fn().mockReturnValue(false) - - const props = { - name: SettingName.NAMES.ExtensionKey, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '00000000-0000-0000-0000-000000000000' }) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - - expect(response).toEqual({ - success: false, - error: { - message: 'User 00000000-0000-0000-0000-000000000000 is not permitted to change the setting.', - }, - statusCode: 401, - }) - }) - - it('should not create a setting if setting is not mutable by the client', async () => { - settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(false) - - const props = { - name: SettingName.NAMES.ExtensionKey, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '00000000-0000-0000-0000-000000000000' }) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - - expect(response).toEqual({ - success: false, - error: { - message: 'User 00000000-0000-0000-0000-000000000000 is not permitted to change the setting.', - }, - statusCode: 401, - }) - }) - }) - - describe('regular subscription', () => { - beforeEach(() => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) - }) - - it('should create a subscription setting', async () => { - const props = { - name: SettingName.NAMES.MuteSignInEmails, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Default, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '00000000-0000-0000-0000-000000000000' }) - - expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'MUTE_SIGN_IN_EMAILS', - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: 1, - sensitive: false, - }, - user, - userSubscription: regularSubscription, - }) - - expect(response).toEqual({ - success: true, - setting: { foo: 'sub-bar' }, - statusCode: 201, - }) - }) - }) - - describe('shared subscription', () => { - beforeEach(() => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription }) - }) - - it('should create a subscription setting', async () => { - const props = { - name: SettingName.NAMES.MuteSignInEmails, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Default, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '00000000-0000-0000-0000-000000000000' }) - - expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'MUTE_SIGN_IN_EMAILS', - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: 1, - sensitive: false, - }, - user, - userSubscription: sharedSubscription, - }) - - expect(response).toEqual({ - success: true, - setting: { foo: 'sub-bar' }, - statusCode: 201, - }) - }) - }) -}) diff --git a/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSetting.ts b/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSetting.ts deleted file mode 100644 index 58e9d9a85..000000000 --- a/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSetting.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { inject, injectable } from 'inversify' -import { UpdateSettingDto } from './UpdateSettingDto' -import { UpdateSettingResponse } from './UpdateSettingResponse' -import { UseCaseInterface } from '../UseCaseInterface' -import TYPES from '../../../Bootstrap/Types' -import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' -import { CreateOrReplaceSettingResponse } from '../../Setting/CreateOrReplaceSettingResponse' -import { SettingProjector } from '../../../Projection/SettingProjector' -import { Logger } from 'winston' -import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' -import { User } from '../../User/User' -import { SettingName } from '@standardnotes/settings' -import { RoleServiceInterface } from '../../Role/RoleServiceInterface' -import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' -import { CreateOrReplaceSubscriptionSettingResponse } from '../../Setting/CreateOrReplaceSubscriptionSettingResponse' -import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' -import { Uuid } from '@standardnotes/domain-core' - -@injectable() -export class UpdateSetting implements UseCaseInterface { - constructor( - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_SubscriptionSettingService) - private subscriptionSettingService: SubscriptionSettingServiceInterface, - @inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, - @inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector, - @inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector, - @inject(TYPES.Auth_SettingsAssociationService) - private settingsAssociationService: SettingsAssociationServiceInterface, - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, - ) {} - - async execute(dto: UpdateSettingDto): Promise { - const settingNameOrError = SettingName.create(dto.props.name) - if (settingNameOrError.isFailed()) { - return { - success: false, - error: { - message: settingNameOrError.getError(), - }, - statusCode: 400, - } - } - const settingName = settingNameOrError.getValue() - - this.logger.debug('[%s] Updating setting: %O', dto.userUuid, dto) - - const userUuidOrError = Uuid.create(dto.userUuid) - if (userUuidOrError.isFailed()) { - return { - success: false, - error: { - message: userUuidOrError.getError(), - }, - statusCode: 400, - } - } - const userUuid = userUuidOrError.getValue() - - const user = await this.userRepository.findOneByUuid(userUuid) - - if (user === null) { - return { - success: false, - error: { - message: `User ${userUuid.value} not found.`, - }, - statusCode: 404, - } - } - - if (!(await this.userHasPermissionToUpdateSetting(user, settingName))) { - return { - success: false, - error: { - message: `User ${userUuid.value} is not permitted to change the setting.`, - }, - statusCode: 401, - } - } - - dto.props.serverEncryptionVersion = this.settingsAssociationService.getEncryptionVersionForSetting(settingName) - dto.props.sensitive = this.settingsAssociationService.getSensitivityForSetting(settingName) - - if (settingName.isASubscriptionSetting()) { - const { regularSubscription, sharedSubscription } = - await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid) - const subscription = sharedSubscription ?? regularSubscription - if (!subscription) { - return { - success: false, - error: { - message: `User ${userUuid.value} has no subscription to change a subscription setting.`, - }, - statusCode: 400, - } - } - - const response = await this.subscriptionSettingService.createOrReplace({ - userSubscription: subscription, - user, - props: dto.props, - }) - - return { - success: true, - setting: await this.subscriptionSettingProjector.projectSimple(response.subscriptionSetting), - statusCode: this.statusToStatusCode(response), - } - } - - const response = await this.settingService.createOrReplace({ - user, - props: dto.props, - }) - - return { - success: true, - setting: await this.settingProjector.projectSimple(response.setting), - statusCode: this.statusToStatusCode(response), - } - } - - /* istanbul ignore next */ - private statusToStatusCode( - response: CreateOrReplaceSettingResponse | CreateOrReplaceSubscriptionSettingResponse, - ): number { - if (response.status === 'created') { - return 201 - } - if (response.status === 'replaced') { - return 200 - } - - const exhaustiveCheck: never = response.status - throw new Error(`Unrecognized status: ${exhaustiveCheck}!`) - } - - private async userHasPermissionToUpdateSetting(user: User, settingName: SettingName): Promise { - const settingIsMutableByClient = this.settingsAssociationService.isSettingMutableByClient(settingName) - if (!settingIsMutableByClient) { - return false - } - - const permissionAssociatedWithSetting = - this.settingsAssociationService.getPermissionAssociatedWithSetting(settingName) - if (permissionAssociatedWithSetting === undefined) { - return true - } - - return this.roleService.userHasPermission(user.uuid, permissionAssociatedWithSetting) - } -} diff --git a/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSettingDto.ts b/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSettingDto.ts deleted file mode 100644 index 9df456531..000000000 --- a/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSettingDto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { SettingProps } from '../../Setting/SettingProps' - -export type UpdateSettingDto = { - userUuid: string - props: SettingProps -} diff --git a/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSettingResponse.ts b/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSettingResponse.ts deleted file mode 100644 index b648b385d..000000000 --- a/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSettingResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SimpleSetting } from '../../Setting/SimpleSetting' - -export type UpdateSettingResponse = - | { - success: true - setting: SimpleSetting - statusCode: number - } - | { - success: false - error: { - message: string - } - statusCode: number - } diff --git a/packages/auth/src/Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser.spec.ts b/packages/auth/src/Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser.spec.ts index 71cf421d0..9edb98c51 100644 --- a/packages/auth/src/Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser.spec.ts +++ b/packages/auth/src/Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser.spec.ts @@ -1,22 +1,39 @@ import { UpdateStorageQuotaUsedForUser } from './UpdateStorageQuotaUsedForUser' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' import { UserSubscription } from '../../Subscription/UserSubscription' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' import { User } from '../../User/User' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' +import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' +import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue' +import { Logger } from 'winston' +import { Result, Timestamps, Uuid } from '@standardnotes/domain-core' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { SettingName } from '@standardnotes/settings' +import { EncryptionVersion } from '../../Encryption/EncryptionVersion' describe('UpdateStorageQuotaUsedForUser', () => { let userRepository: UserRepositoryInterface - let userSubscriptionService: UserSubscriptionServiceInterface let user: User - let subscriptionSettingService: SubscriptionSettingServiceInterface let regularSubscription: UserSubscription let sharedSubscription: UserSubscription + let getSharedSubscription: GetSharedSubscriptionForUser + let getRegularSubscription: GetRegularSubscriptionForUser + let getSubscriptionSetting: GetSubscriptionSetting + let setSubscriptonSettingValue: SetSubscriptionSettingValue + let logger: Logger const createUseCase = () => - new UpdateStorageQuotaUsedForUser(userRepository, userSubscriptionService, subscriptionSettingService) + new UpdateStorageQuotaUsedForUser( + userRepository, + getRegularSubscription, + getSharedSubscription, + getSubscriptionSetting, + setSubscriptonSettingValue, + logger, + ) beforeEach(() => { user = { @@ -38,14 +55,20 @@ describe('UpdateStorageQuotaUsedForUser', () => { user: Promise.resolve(user), } as jest.Mocked - userSubscriptionService = {} as jest.Mocked - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) + getSharedSubscription = {} as jest.Mocked + getSharedSubscription.execute = jest.fn().mockReturnValue(Result.ok(sharedSubscription)) - subscriptionSettingService = {} as jest.Mocked - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null) - subscriptionSettingService.createOrReplace = jest.fn() + getRegularSubscription = {} as jest.Mocked + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription)) + + getSubscriptionSetting = {} as jest.Mocked + getSubscriptionSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) + + setSubscriptonSettingValue = {} as jest.Mocked + setSubscriptonSettingValue.execute = jest.fn().mockReturnValue(Result.ok()) + + logger = {} as jest.Mocked + logger.error = jest.fn() }) it('should create a bytes used setting if one does not exist', async () => { @@ -55,19 +78,10 @@ describe('UpdateStorageQuotaUsedForUser', () => { }) expect(result.isFailed()).toBeFalsy() - expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'FILE_UPLOAD_BYTES_USED', - sensitive: false, - unencryptedValue: '123', - serverEncryptionVersion: 0, - }, - user, - userSubscription: { - uuid: '00000000-0000-0000-0000-000000000000', - subscriptionType: 'regular', - user: Promise.resolve(user), - }, + expect(setSubscriptonSettingValue.execute).toHaveBeenCalledWith({ + settingName: 'FILE_UPLOAD_BYTES_USED', + value: '123', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', }) }) @@ -78,7 +92,7 @@ describe('UpdateStorageQuotaUsedForUser', () => { }) expect(result.isFailed()).toBeTruthy() - expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled() + expect(setSubscriptonSettingValue.execute).not.toHaveBeenCalled() }) it('should not do anything if a user is not found', async () => { @@ -90,120 +104,84 @@ describe('UpdateStorageQuotaUsedForUser', () => { }) expect(result.isFailed()).toBeTruthy() - expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled() + expect(setSubscriptonSettingValue.execute).not.toHaveBeenCalled() }) - it('should not do anything if a user subscription is not found', async () => { - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({ - value: 345, - }) - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) - - const result = await createUseCase().execute({ - userUuid: '00000000-0000-0000-0000-000000000000', - bytesUsed: 123, - }) - expect(result.isFailed()).toBeTruthy() - - expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled() - }) - - it('should add bytes used setting if one does exist', async () => { - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({ - value: 345, - }) - const result = await createUseCase().execute({ - userUuid: '00000000-0000-0000-0000-000000000000', - bytesUsed: 123, - }) - expect(result.isFailed()).toBeFalsy() - - expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'FILE_UPLOAD_BYTES_USED', - sensitive: false, - unencryptedValue: '468', - serverEncryptionVersion: 0, - }, - user, - userSubscription: { - uuid: '00000000-0000-0000-0000-000000000000', - subscriptionType: 'regular', - user: Promise.resolve(user), - }, - }) - }) - - it('should subtract bytes used setting if one does exist', async () => { - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({ - value: 345, - }) - const result = await createUseCase().execute({ - userUuid: '00000000-0000-0000-0000-000000000000', - bytesUsed: -123, - }) - expect(result.isFailed()).toBeFalsy() - - expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'FILE_UPLOAD_BYTES_USED', - sensitive: false, - unencryptedValue: '222', - serverEncryptionVersion: 0, - }, - user, - userSubscription: { - uuid: '00000000-0000-0000-0000-000000000000', - subscriptionType: 'regular', - user: Promise.resolve(user), - }, - }) - }) - - it('should update a bytes used setting on both regular and shared subscription', async () => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription }) - - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({ - value: 345, - }) - const result = await createUseCase().execute({ - userUuid: '00000000-0000-0000-0000-000000000000', - bytesUsed: 123, - }) - expect(result.isFailed()).toBeFalsy() - - expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'FILE_UPLOAD_BYTES_USED', - sensitive: false, - unencryptedValue: '468', - serverEncryptionVersion: 0, - }, - user, - userSubscription: { - uuid: '00000000-0000-0000-0000-000000000000', - subscriptionType: 'regular', - user: Promise.resolve(user), - }, + describe('updating existing quota', () => { + beforeEach(() => { + getSubscriptionSetting.execute = jest.fn().mockReturnValue( + Result.ok({ + setting: SubscriptionSetting.create({ + name: SettingName.NAMES.FileUploadBytesUsed, + sensitive: false, + serverEncryptionVersion: EncryptionVersion.Unencrypted, + timestamps: Timestamps.create(123, 123).getValue(), + userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + value: '345', + }).getValue(), + }), + ) }) - expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'FILE_UPLOAD_BYTES_USED', - sensitive: false, - unencryptedValue: '468', - serverEncryptionVersion: 0, - }, - user, - userSubscription: { - uuid: '2-3-4', - subscriptionType: 'shared', - user: Promise.resolve(user), - }, + it('should not do anything if a user subscription is not found', async () => { + getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('error')) + getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + bytesUsed: 123, + }) + expect(result.isFailed()).toBeTruthy() + + expect(setSubscriptonSettingValue.execute).not.toHaveBeenCalled() + }) + + it('should add bytes used setting if one does exist', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + bytesUsed: 123, + }) + expect(result.isFailed()).toBeFalsy() + + expect(setSubscriptonSettingValue.execute).toHaveBeenCalledWith({ + settingName: 'FILE_UPLOAD_BYTES_USED', + value: '468', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + }) + }) + + it('should subtract bytes used setting if one does exist', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + bytesUsed: -123, + }) + expect(result.isFailed()).toBeFalsy() + + expect(setSubscriptonSettingValue.execute).toHaveBeenCalledWith({ + settingName: 'FILE_UPLOAD_BYTES_USED', + value: '222', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + }) + }) + + it('should update a bytes used setting on both regular and shared subscription', async () => { + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + bytesUsed: 123, + }) + expect(result.isFailed()).toBeFalsy() + + expect(setSubscriptonSettingValue.execute).toHaveBeenCalledWith({ + settingName: 'FILE_UPLOAD_BYTES_USED', + value: '468', + userSubscriptionUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(setSubscriptonSettingValue.execute).toHaveBeenCalledWith({ + settingName: 'FILE_UPLOAD_BYTES_USED', + value: '468', + userSubscriptionUuid: '2-3-4', + }) }) }) }) diff --git a/packages/auth/src/Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser.ts b/packages/auth/src/Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser.ts index abf6550dd..99427c05e 100644 --- a/packages/auth/src/Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser.ts +++ b/packages/auth/src/Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser.ts @@ -1,18 +1,23 @@ import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { SettingName } from '@standardnotes/settings' -import { EncryptionVersion } from '../../Encryption/EncryptionVersion' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' import { UserSubscription } from '../../Subscription/UserSubscription' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO' +import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser' +import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting' +import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue' +import { Logger } from 'winston' +import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser' export class UpdateStorageQuotaUsedForUser implements UseCaseInterface { constructor( private userRepository: UserRepositoryInterface, - private userSubscriptionService: UserSubscriptionServiceInterface, - private subscriptionSettingService: SubscriptionSettingServiceInterface, + private getRegularSubscription: GetRegularSubscriptionForUser, + private getSharedSubscription: GetSharedSubscriptionForUser, + private getSubscriptionSetting: GetSubscriptionSetting, + private setSubscriptonSettingValue: SetSubscriptionSettingValue, + private logger: Logger, ) {} async execute(dto: UpdateStorageQuotaUsedForUserDTO): Promise> { @@ -27,15 +32,21 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface { return Result.fail(`Could not find user with uuid: ${userUuid.value}`) } - const { regularSubscription, sharedSubscription } = - await this.userSubscriptionService.findRegularSubscriptionForUserUuid(userUuid.value) - if (regularSubscription === null) { + const regularSubscriptionOrError = await this.getRegularSubscription.execute({ + userUuid: user.uuid, + }) + if (regularSubscriptionOrError.isFailed()) { return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`) } + const regularSubscription = regularSubscriptionOrError.getValue() await this.updateUploadBytesUsedSetting(regularSubscription, dto.bytesUsed) - if (sharedSubscription !== null) { + const sharedSubscriptionOrError = await this.getSharedSubscription.execute({ + userUuid: user.uuid, + }) + if (!sharedSubscriptionOrError.isFailed()) { + const sharedSubscription = sharedSubscriptionOrError.getValue() await this.updateUploadBytesUsedSetting(sharedSubscription, dto.bytesUsed) } @@ -45,24 +56,27 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface { private async updateUploadBytesUsedSetting(subscription: UserSubscription, bytesUsed: number): Promise { let bytesAlreadyUsed = '0' const subscriptionUser = await subscription.user - const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ - userUuid: subscriptionUser.uuid, + + const bytesUsedSettingExists = await this.getSubscriptionSetting.execute({ userSubscriptionUuid: subscription.uuid, - subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(), + settingName: SettingName.NAMES.FileUploadBytesUsed, + allowSensitiveRetrieval: false, }) - if (bytesUsedSetting !== null) { - bytesAlreadyUsed = bytesUsedSetting.value as string + + if (!bytesUsedSettingExists.isFailed()) { + const bytesUsedSetting = bytesUsedSettingExists.getValue() + bytesAlreadyUsed = bytesUsedSetting.setting.props.value as string } - await this.subscriptionSettingService.createOrReplace({ - userSubscription: subscription, - user: subscriptionUser, - props: { - name: SettingName.NAMES.FileUploadBytesUsed, - unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(), - sensitive: false, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - }, + const result = await this.setSubscriptonSettingValue.execute({ + userSubscriptionUuid: subscription.uuid, + settingName: SettingName.NAMES.FileUploadBytesUsed, + value: (+bytesAlreadyUsed + bytesUsed).toString(), }) + + /* istanbul ignore next */ + if (result.isFailed()) { + this.logger.error(`Could not set file upload bytes used for user ${subscriptionUser.uuid}`) + } } } diff --git a/packages/auth/src/Domain/UseCase/VerifyMFA.spec.ts b/packages/auth/src/Domain/UseCase/VerifyMFA.spec.ts index 35611c748..7b635035c 100644 --- a/packages/auth/src/Domain/UseCase/VerifyMFA.spec.ts +++ b/packages/auth/src/Domain/UseCase/VerifyMFA.spec.ts @@ -2,40 +2,42 @@ import 'reflect-metadata' import { authenticator } from 'otplib' import { SettingName } from '@standardnotes/settings' import { SelectorInterface } from '@standardnotes/security' -import { Result, UseCaseInterface } from '@standardnotes/domain-core' +import { Result, Timestamps, Uuid } from '@standardnotes/domain-core' import { User } from '../User/User' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' import { Setting } from '../Setting/Setting' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { LockRepositoryInterface } from '../User/LockRepositoryInterface' import { AuthenticatorRepositoryInterface } from '../Authenticator/AuthenticatorRepositoryInterface' import { VerifyMFA } from './VerifyMFA' import { Logger } from 'winston' import { Authenticator } from '../Authenticator/Authenticator' +import { GetSetting } from './GetSetting/GetSetting' +import { EncryptionVersion } from '../Encryption/EncryptionVersion' +import { VerifyAuthenticatorAuthenticationResponse } from './VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse' describe('VerifyMFA', () => { let user: User let setting: Setting let userRepository: UserRepositoryInterface - let settingService: SettingServiceInterface + let getSetting: GetSetting let booleanSelector: SelectorInterface let lockRepository: LockRepositoryInterface let authenticatorRepository: AuthenticatorRepositoryInterface - let verifyAuthenticatorAuthenticationResponse: UseCaseInterface + let verifyAuthenticatorAuthenticationResponse: VerifyAuthenticatorAuthenticationResponse let logger: Logger const pseudoKeyParamsKey = 'foobar' const createVerifyMFA = () => new VerifyMFA( userRepository, - settingService, booleanSelector, lockRepository, pseudoKeyParamsKey, authenticatorRepository, verifyAuthenticatorAuthenticationResponse, + getSetting, logger, ) @@ -54,18 +56,22 @@ describe('VerifyMFA', () => { lockRepository.isOTPLocked = jest.fn().mockReturnValue(false) lockRepository.lockSuccessfullOTP = jest.fn() - setting = { + setting = Setting.create({ name: SettingName.NAMES.MfaSecret, - value: 'shhhh', - } as jest.Mocked + value: '1243359u42395834', + serverEncryptionVersion: EncryptionVersion.Default, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: true, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() - settingService = {} as jest.Mocked - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) + getSetting = {} as jest.Mocked + getSetting.execute = jest.fn().mockReturnValue(Result.ok({ setting, decryptedValue: 'shhhh' })) authenticatorRepository = {} as jest.Mocked authenticatorRepository.findByUserUuid = jest.fn().mockReturnValue([]) - verifyAuthenticatorAuthenticationResponse = {} as jest.Mocked> + verifyAuthenticatorAuthenticationResponse = {} as jest.Mocked verifyAuthenticatorAuthenticationResponse.execute = jest.fn().mockReturnValue(Result.ok()) logger = {} as jest.Mocked @@ -74,7 +80,7 @@ describe('VerifyMFA', () => { describe('2FA', () => { it('should pass MFA verification if user has no MFA enabled', async () => { - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) + getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) expect( await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }), @@ -86,12 +92,16 @@ describe('VerifyMFA', () => { }) it('should pass MFA verification if user has MFA deleted', async () => { - setting = { + setting = Setting.create({ name: SettingName.NAMES.MfaSecret, value: null, - } as jest.Mocked + serverEncryptionVersion: EncryptionVersion.Default, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: true, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) + getSetting.execute = jest.fn().mockReturnValue(Result.ok({ setting, decryptedValue: null })) expect( await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }), @@ -192,13 +202,16 @@ describe('VerifyMFA', () => { }) it('should not pass MFA verification if mfa is not correct', async () => { - setting = { + setting = Setting.create({ name: SettingName.NAMES.MfaSecret, - value: 'shhhh2', - } as jest.Mocked + value: 'aaa324523534werfe', + serverEncryptionVersion: EncryptionVersion.Default, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: true, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() - settingService = {} as jest.Mocked - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) + getSetting.execute = jest.fn().mockReturnValue(Result.ok({ setting, decryptedValue: 'shhhh2' })) expect( await createVerifyMFA().execute({ @@ -230,7 +243,7 @@ describe('VerifyMFA', () => { }) it('should throw an error if the error is not handled mfa validation error', async () => { - settingService.findSettingWithDecryptedValue = jest.fn().mockImplementation(() => { + getSetting.execute = jest.fn().mockImplementation(() => { throw new Error('oops!') }) @@ -251,7 +264,7 @@ describe('VerifyMFA', () => { describe('U2F', () => { beforeEach(() => { - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) + getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found')) authenticatorRepository.findByUserUuid = jest.fn().mockReturnValue([{} as jest.Mocked]) }) diff --git a/packages/auth/src/Domain/UseCase/VerifyMFA.ts b/packages/auth/src/Domain/UseCase/VerifyMFA.ts index d849fd2a5..51e4a9eac 100644 --- a/packages/auth/src/Domain/UseCase/VerifyMFA.ts +++ b/packages/auth/src/Domain/UseCase/VerifyMFA.ts @@ -1,16 +1,14 @@ import * as crypto from 'crypto' +import { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types' import { ErrorTag } from '@standardnotes/responses' import { SettingName } from '@standardnotes/settings' import { v4 as uuidv4 } from 'uuid' -import { inject, injectable } from 'inversify' import { authenticator } from 'otplib' import { SelectorInterface } from '@standardnotes/security' -import { UseCaseInterface as DomainUseCaseInterface, Username, Uuid } from '@standardnotes/domain-core' +import { Username, Uuid } from '@standardnotes/domain-core' -import TYPES from '../../Bootstrap/Types' import { MFAValidationError } from '../Error/MFAValidationError' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { LockRepositoryInterface } from '../User/LockRepositoryInterface' import { AuthenticatorRepositoryInterface } from '../Authenticator/AuthenticatorRepositoryInterface' @@ -18,20 +16,19 @@ import { UseCaseInterface } from './UseCaseInterface' import { VerifyMFADTO } from './VerifyMFADTO' import { VerifyMFAResponse } from './VerifyMFAResponse' import { Logger } from 'winston' -import { Setting } from '../Setting/Setting' +import { GetSetting } from './GetSetting/GetSetting' +import { VerifyAuthenticatorAuthenticationResponse } from './VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse' -@injectable() export class VerifyMFA implements UseCaseInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, - @inject(TYPES.Auth_BooleanSelector) private booleanSelector: SelectorInterface, - @inject(TYPES.Auth_LockRepository) private lockRepository: LockRepositoryInterface, - @inject(TYPES.Auth_PSEUDO_KEY_PARAMS_KEY) private pseudoKeyParamsKey: string, - @inject(TYPES.Auth_AuthenticatorRepository) private authenticatorRepository: AuthenticatorRepositoryInterface, - @inject(TYPES.Auth_VerifyAuthenticatorAuthenticationResponse) - private verifyAuthenticatorAuthenticationResponse: DomainUseCaseInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, + private userRepository: UserRepositoryInterface, + private booleanSelector: SelectorInterface, + private lockRepository: LockRepositoryInterface, + private pseudoKeyParamsKey: string, + private authenticatorRepository: AuthenticatorRepositoryInterface, + private verifyAuthenticatorAuthenticationResponse: VerifyAuthenticatorAuthenticationResponse, + private getSetting: GetSetting, + private logger: Logger, ) {} async execute(dto: VerifyMFADTO): Promise { @@ -98,11 +95,13 @@ export class VerifyMFA implements UseCaseInterface { u2fEnabled = true } - const mfaSecret = await this.settingService.findSettingWithDecryptedValue({ - userUuid: user.uuid, - settingName: SettingName.create(SettingName.NAMES.MfaSecret).getValue(), + const mfaSecretOrError = await this.getSetting.execute({ + userUuid: userUuid.value, + settingName: SettingName.NAMES.MfaSecret, + allowSensitiveRetrieval: true, + decrypted: true, }) - const twoFactorEnabled = mfaSecret !== null && mfaSecret.value !== null + const twoFactorEnabled = !mfaSecretOrError.isFailed() && mfaSecretOrError.getValue().decryptedValue !== null if (u2fEnabled === false && twoFactorEnabled === false) { return { @@ -121,7 +120,7 @@ export class VerifyMFA implements UseCaseInterface { const verificationResultOrError = await this.verifyAuthenticatorAuthenticationResponse.execute({ userUuid: userUuid.value, - authenticatorResponse: dto.requestParams.authenticator_response, + authenticatorResponse: dto.requestParams.authenticator_response as AuthenticationResponseJSON, }) if (verificationResultOrError.isFailed()) { this.logger.debug(`Could not verify U2F authentication: ${verificationResultOrError.getError()}`) @@ -148,7 +147,7 @@ export class VerifyMFA implements UseCaseInterface { } else { const verificationResult = await this.verifyMFASecret( dto.email, - (mfaSecret as Setting).value as string, + mfaSecretOrError.getValue().decryptedValue as string, dto.requestParams, dto.preventOTPFromFurtherUsage, ) diff --git a/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.spec.ts b/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.spec.ts index 21b9f66c4..019ecf6a8 100644 --- a/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.spec.ts +++ b/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.spec.ts @@ -8,7 +8,8 @@ import { UserSubscription } from '../../Subscription/UserSubscription' import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface' import { VerifyPredicate } from './VerifyPredicate' -import { EmailBackupFrequency } from '@standardnotes/settings' +import { EmailBackupFrequency, SettingName } from '@standardnotes/settings' +import { Uuid, Timestamps } from '@standardnotes/domain-core' describe('VerifyPredicate', () => { let settingRepository: SettingRepositoryInterface @@ -30,7 +31,14 @@ describe('VerifyPredicate', () => { }) it('should tell that a user has enabled email backups', async () => { - setting = { value: EmailBackupFrequency.Weekly } as jest.Mocked + const setting = Setting.create({ + name: SettingName.NAMES.EmailBackupFrequency, + value: EmailBackupFrequency.Weekly, + serverEncryptionVersion: 0, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting) expect( @@ -44,7 +52,14 @@ describe('VerifyPredicate', () => { }) it('should tell that a user has disabled email backups', async () => { - setting = { value: EmailBackupFrequency.Disabled } as jest.Mocked + const setting = Setting.create({ + name: SettingName.NAMES.EmailBackupFrequency, + value: EmailBackupFrequency.Disabled, + serverEncryptionVersion: 0, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting) expect( diff --git a/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.ts b/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.ts index 20035ac34..4e28e0e4f 100644 --- a/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.ts +++ b/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.ts @@ -45,7 +45,7 @@ export class VerifyPredicate implements UseCaseInterface { userUuid, ) - if (setting === null || setting.value === EmailBackupFrequency.Disabled) { + if (setting === null || setting.props.value === EmailBackupFrequency.Disabled) { return false } diff --git a/packages/auth/src/Domain/User/User.ts b/packages/auth/src/Domain/User/User.ts index ceaac70c5..c0e9faafc 100644 --- a/packages/auth/src/Domain/User/User.ts +++ b/packages/auth/src/Domain/User/User.ts @@ -1,7 +1,6 @@ import { Column, Entity, Index, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn } from 'typeorm' import { RevokedSession } from '../Session/RevokedSession' import { Role } from '../Role/Role' -import { Setting } from '../Setting/Setting' import { UserSubscription } from '../Subscription/UserSubscription' import { ProtocolVersion } from '@standardnotes/common' import { TypeORMEmergencyAccessInvitation } from '../../Infra/TypeORM/TypeORMEmergencyAccessInvitation' @@ -143,16 +142,6 @@ export class User { ) declare revokedSessions: Promise - @OneToMany( - /* istanbul ignore next */ - () => Setting, - /* istanbul ignore next */ - (setting) => setting.user, - /* istanbul ignore next */ - { lazy: true, eager: false }, - ) - declare settings: Promise - @ManyToMany( /* istanbul ignore next */ () => Role, diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedAdminController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedAdminController.spec.ts deleted file mode 100644 index e9625024a..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedAdminController.spec.ts +++ /dev/null @@ -1,192 +0,0 @@ -import 'reflect-metadata' - -import { AnnotatedAdminController } from './AnnotatedAdminController' -import { results } from 'inversify-express-utils' -import { User } from '../../Domain/User/User' -import { UserRepositoryInterface } from '../../Domain/User/UserRepositoryInterface' -import * as express from 'express' -import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting' -import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken' -import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken' - -describe('AnnotatedAdminController', () => { - let deleteSetting: DeleteSetting - let userRepository: UserRepositoryInterface - let createSubscriptionToken: CreateSubscriptionToken - let createOfflineSubscriptionToken: CreateOfflineSubscriptionToken - let request: express.Request - let user: User - - const createController = () => - new AnnotatedAdminController(deleteSetting, userRepository, createSubscriptionToken, createOfflineSubscriptionToken) - - beforeEach(() => { - user = {} as jest.Mocked - user.uuid = '123' - - deleteSetting = {} as jest.Mocked - deleteSetting.execute = jest.fn().mockReturnValue({ success: true }) - - userRepository = {} as jest.Mocked - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) - - createSubscriptionToken = {} as jest.Mocked - createSubscriptionToken.execute = jest.fn().mockReturnValue({ - subscriptionToken: { - token: '123-sub-token', - }, - }) - - createOfflineSubscriptionToken = {} as jest.Mocked - createOfflineSubscriptionToken.execute = jest.fn().mockReturnValue({ - success: true, - offlineSubscriptionToken: { - token: '123-sub-token', - }, - }) - - request = { - headers: {}, - body: {}, - params: {}, - } as jest.Mocked - }) - - it('should return error if missing email parameter', async () => { - const httpResponse = await createController().getUser(request) - const result = await httpResponse.executeAsync() - - expect(httpResponse).toBeInstanceOf(results.JsonResult) - - expect(result.statusCode).toBe(400) - expect(await result.content.readAsStringAsync()).toEqual('{"error":{"message":"Missing email parameter."}}') - }) - - it('should return error if no user with such email exists', async () => { - request.params.email = 'test@sn.org' - - userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null) - - const httpResponse = await createController().getUser(request) - const result = await httpResponse.executeAsync() - - expect(httpResponse).toBeInstanceOf(results.JsonResult) - - expect(result.statusCode).toBe(400) - expect(await result.content.readAsStringAsync()).toEqual( - '{"error":{"message":"No user with email \'test@sn.org\'."}}', - ) - }) - - it("should return the user's uuid", async () => { - request.params.email = 'test@sn.org' - - const httpResponse = await createController().getUser(request) - const result = await httpResponse.executeAsync() - - expect(httpResponse).toBeInstanceOf(results.JsonResult) - - expect(result.statusCode).toBe(200) - expect(await result.content.readAsStringAsync()).toEqual('{"uuid":"123"}') - }) - - it('should delete user mfa setting', async () => { - request.params.userUuid = '1-2-3' - - deleteSetting.execute = jest.fn().mockReturnValue({ success: true }) - - const httpResponse = await createController().deleteMFASetting(request) - const result = await httpResponse.executeAsync() - - expect(deleteSetting.execute).toHaveBeenCalledWith({ - userUuid: '1-2-3', - settingName: 'MFA_SECRET', - softDelete: true, - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should fail if could not delete user mfa setting', async () => { - request.params.userUuid = '1-2-3' - - deleteSetting.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().deleteMFASetting(request) - const result = await httpResponse.executeAsync() - - expect(deleteSetting.execute).toHaveBeenCalledWith({ - userUuid: '1-2-3', - settingName: 'MFA_SECRET', - softDelete: true, - }) - - expect(result.statusCode).toEqual(400) - }) - - it("should return a new subscription token for the user's uuid", async () => { - request.params.userUuid = '1-2-3' - - const httpResponse = await createController().createToken(request) - const result = await httpResponse.executeAsync() - - expect(httpResponse).toBeInstanceOf(results.JsonResult) - - expect(result.statusCode).toBe(200) - expect(await result.content.readAsStringAsync()).toEqual('{"token":"123-sub-token"}') - }) - - it("should return a new offline subscription token for the user's email", async () => { - request.params.email = 'test@test.te' - - const httpResponse = await createController().createOfflineToken(request) - const result = await httpResponse.executeAsync() - - expect(httpResponse).toBeInstanceOf(results.JsonResult) - - expect(result.statusCode).toBe(200) - expect(await result.content.readAsStringAsync()).toEqual('{"token":"123-sub-token"}') - }) - - it('should not return a new offline subscription token if the workflow fails', async () => { - request.params.email = 'test@test.te' - - createOfflineSubscriptionToken.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().createOfflineToken(request) - const result = await httpResponse.executeAsync() - - expect(httpResponse).toBeInstanceOf(results.BadRequestResult) - - expect(result.statusCode).toBe(400) - }) - - it('should not delete email backup setting if value is null', async () => { - request.body = {} - request.params = { - userUuid: '1-2-3', - } - - deleteSetting.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().disableEmailBackups(request) - const result = await httpResponse.executeAsync() - - expect(result.statusCode).toEqual(400) - expect(await result.content.readAsStringAsync()).toEqual('No email backups found') - }) - - it('should disable email backups by deleting the setting', async () => { - request.body = {} - request.params = { - userUuid: '1-2-3', - } - - deleteSetting.execute = jest.fn().mockReturnValue({ success: true }) - - const httpResponse = await createController().disableEmailBackups(request) - const result = await httpResponse.executeAsync() - - expect(result.statusCode).toEqual(200) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedFeaturesController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedFeaturesController.spec.ts deleted file mode 100644 index 3dc88cf8e..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedFeaturesController.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' - -import { AnnotatedFeaturesController } from './AnnotatedFeaturesController' -import { results } from 'inversify-express-utils' -import { User } from '../../Domain/User/User' -import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures' - -describe('AnnotatedFeaturesController', () => { - let getUserFeatures: GetUserFeatures - - let request: express.Request - let response: express.Response - let user: User - - const createController = () => new AnnotatedFeaturesController(getUserFeatures) - - beforeEach(() => { - user = {} as jest.Mocked - user.uuid = '123' - - getUserFeatures = {} as jest.Mocked - getUserFeatures.execute = jest.fn() - - request = { - headers: {}, - body: {}, - params: {}, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - }) - - it('should get authenticated user features', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - getUserFeatures.execute = jest.fn().mockReturnValue({ success: true }) - - const httpResponse = await createController().getFeatures(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserFeatures.execute).toHaveBeenCalledWith({ - userUuid: '1-2-3', - offline: false, - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not get user features if the user with provided uuid does not exist', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - getUserFeatures.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().getFeatures(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserFeatures.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', offline: false }) - - expect(result.statusCode).toEqual(400) - }) - - it('should not get user features if not allowed', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '2-3-4', - } - - getUserFeatures.execute = jest.fn() - - const httpResponse = await createController().getFeatures(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserFeatures.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedInternalController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedInternalController.spec.ts deleted file mode 100644 index cbffa7570..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedInternalController.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' - -import { AnnotatedInternalController } from './AnnotatedInternalController' -import { results } from 'inversify-express-utils' -import { User } from '../../Domain/User/User' -import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures' -import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting' -import { Result } from '@standardnotes/domain-core' - -describe('AnnotatedInternalController', () => { - let getUserFeatures: GetUserFeatures - let getSetting: GetSetting - - let request: express.Request - let response: express.Response - let user: User - - const createController = () => new AnnotatedInternalController(getUserFeatures, getSetting) - - beforeEach(() => { - user = {} as jest.Mocked - user.uuid = '123' - - getUserFeatures = {} as jest.Mocked - getUserFeatures.execute = jest.fn() - - getSetting = {} as jest.Mocked - getSetting.execute = jest.fn() - - request = { - headers: {}, - body: {}, - params: {}, - } as jest.Mocked - - response = {} as jest.Mocked - response.setHeader = jest.fn() - response.status = jest.fn().mockReturnThis() - response.send = jest.fn() - }) - - it('should get user features', async () => { - request.params.userUuid = '1-2-3' - - getUserFeatures.execute = jest.fn().mockReturnValue({ success: true }) - - const httpResponse = await createController().getFeatures(request) - const result = await httpResponse.executeAsync() - - expect(getUserFeatures.execute).toHaveBeenCalledWith({ - userUuid: '1-2-3', - offline: false, - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not get user features if the user with provided uuid does not exist', async () => { - request.params.userUuid = '1-2-3' - - getUserFeatures.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().getFeatures(request) - const result = await httpResponse.executeAsync() - - expect(getUserFeatures.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', offline: false }) - - expect(result.statusCode).toEqual(400) - }) - - it('should get user setting', async () => { - request.params.userUuid = '1-2-3' - request.params.settingName = 'foobar' - - getSetting.execute = jest.fn().mockReturnValue(Result.ok()) - - const httpResponse = await createController().getSetting(request) - const result = await httpResponse.executeAsync() - - expect(getSetting.execute).toHaveBeenCalledWith({ - userUuid: '1-2-3', - settingName: 'foobar', - allowSensitiveRetrieval: true, - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not get user setting if the use case fails', async () => { - request.params.userUuid = '1-2-3' - request.params.settingName = 'foobar' - - getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops')) - - const httpResponse = await createController().getSetting(request) - const result = await httpResponse.executeAsync() - - expect(getSetting.execute).toHaveBeenCalledWith({ - userUuid: '1-2-3', - settingName: 'foobar', - allowSensitiveRetrieval: true, - }) - - expect(result.statusCode).toEqual(400) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedInternalController.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedInternalController.ts index 3ace51c69..c22462305 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedInternalController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedInternalController.ts @@ -10,12 +10,17 @@ import { import TYPES from '../../Bootstrap/Types' import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting' import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures' +import { MapperInterface } from '@standardnotes/domain-core' +import { Setting } from '../../Domain/Setting/Setting' +import { SettingHttpRepresentation } from '../../Mapping/Http/SettingHttpRepresentation' @controller('/internal') export class AnnotatedInternalController extends BaseHttpController { constructor( @inject(TYPES.Auth_GetUserFeatures) private doGetUserFeatures: GetUserFeatures, @inject(TYPES.Auth_GetSetting) private doGetSetting: GetSetting, + @inject(TYPES.Auth_SettingHttpMapper) + private settingHttpMapper: MapperInterface, ) { super() } @@ -40,6 +45,7 @@ export class AnnotatedInternalController extends BaseHttpController { userUuid: request.params.userUuid, settingName: request.params.settingName, allowSensitiveRetrieval: true, + decrypted: true, }) if (resultOrError.isFailed()) { @@ -53,9 +59,13 @@ export class AnnotatedInternalController extends BaseHttpController { ) } - return this.json({ - success: true, - ...resultOrError.getValue(), - }) + const settingAndValue = resultOrError.getValue() + + const settingHttpRepresentation = { + ...this.settingHttpMapper.toProjection(settingAndValue.setting), + value: settingAndValue.decryptedValue, + } + + return this.json(settingHttpRepresentation) } } diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedListedController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedListedController.spec.ts deleted file mode 100644 index 7eb89343c..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedListedController.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' -import { results } from 'inversify-express-utils' - -import { AnnotatedListedController } from './AnnotatedListedController' -import { User } from '../../Domain/User/User' -import { CreateListedAccount } from '../../Domain/UseCase/CreateListedAccount/CreateListedAccount' - -describe('AnnotatedListedController', () => { - let createListedAccount: CreateListedAccount - - let request: express.Request - let response: express.Response - let user: User - - const createController = () => new AnnotatedListedController(createListedAccount) - - beforeEach(() => { - user = {} as jest.Mocked - user.uuid = '123' - - createListedAccount = {} as jest.Mocked - createListedAccount.execute = jest.fn() - - request = { - headers: {}, - body: {}, - params: {}, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - }) - - it('should create a listed account for user', async () => { - response.locals.user = { - uuid: '1-2-3', - email: 'test@test.com', - } - - const httpResponse = await createController().createListedAccount(request, response) - const result = await httpResponse.executeAsync() - - expect(createListedAccount.execute).toHaveBeenCalledWith({ - userUuid: '1-2-3', - userEmail: 'test@test.com', - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not create a listed account if sessions is read only', async () => { - response.locals.readOnlyAccess = true - response.locals.user = { - uuid: '1-2-3', - email: 'test@test.com', - } - - const httpResponse = await createController().createListedAccount(request, response) - const result = await httpResponse.executeAsync() - - expect(createListedAccount.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedOfflineController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedOfflineController.spec.ts deleted file mode 100644 index 398bab364..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedOfflineController.spec.ts +++ /dev/null @@ -1,234 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' - -import { AnnotatedOfflineController } from './AnnotatedOfflineController' -import { results } from 'inversify-express-utils' -import { User } from '../../Domain/User/User' -import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures' -import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken' -import { CreateOfflineSubscriptionTokenResponse } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionTokenResponse' -import { AuthenticateOfflineSubscriptionToken } from '../../Domain/UseCase/AuthenticateOfflineSubscriptionToken/AuthenticateOfflineSubscriptionToken' -import { OfflineUserSubscription } from '../../Domain/Subscription/OfflineUserSubscription' -import { GetUserOfflineSubscription } from '../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription' -import { OfflineUserTokenData, TokenEncoderInterface } from '@standardnotes/security' -import { SubscriptionName } from '@standardnotes/common' -import { Logger } from 'winston' - -describe('AnnotatedOfflineController', () => { - let getUserFeatures: GetUserFeatures - let getUserOfflineSubscription: GetUserOfflineSubscription - let createOfflineSubscriptionToken: CreateOfflineSubscriptionToken - let authenticateToken: AuthenticateOfflineSubscriptionToken - let logger: Logger - let tokenEncoder: TokenEncoderInterface - const jwtTTL = 60 - - let request: express.Request - let response: express.Response - let user: User - - const createController = () => - new AnnotatedOfflineController( - getUserFeatures, - getUserOfflineSubscription, - createOfflineSubscriptionToken, - authenticateToken, - tokenEncoder, - jwtTTL, - logger, - ) - - beforeEach(() => { - user = {} as jest.Mocked - user.uuid = '123' - - getUserFeatures = {} as jest.Mocked - getUserFeatures.execute = jest.fn() - - createOfflineSubscriptionToken = {} as jest.Mocked - createOfflineSubscriptionToken.execute = jest.fn().mockReturnValue({ - success: true, - offlineSubscriptionToken: { - token: 'test', - }, - } as jest.Mocked) - - getUserOfflineSubscription = {} as jest.Mocked - getUserOfflineSubscription.execute = jest.fn().mockReturnValue({ - success: true, - subscription: { - planName: SubscriptionName.ProPlan, - }, - }) - - authenticateToken = {} as jest.Mocked - authenticateToken.execute = jest.fn().mockReturnValue({ - success: true, - email: 'test@test.com', - subscriptions: [{} as jest.Mocked], - }) - - logger = {} as jest.Mocked - logger.debug = jest.fn() - - tokenEncoder = {} as jest.Mocked> - tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar') - - request = { - headers: {}, - body: {}, - params: {}, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - }) - - it('should get offline user features', async () => { - response.locals.offlineUserEmail = 'test@test.com' - - getUserFeatures.execute = jest.fn().mockReturnValue({ success: true }) - - const httpResponse = await createController().getOfflineFeatures(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserFeatures.execute).toHaveBeenCalledWith({ - email: 'test@test.com', - offline: true, - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should get offline user subscription', async () => { - response.locals.userEmail = 'test@test.com' - - const httpResponse = await createController().getSubscription(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserOfflineSubscription.execute).toHaveBeenCalledWith({ - userEmail: 'test@test.com', - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not get offline user subscription if the procedure fails', async () => { - response.locals.userEmail = 'test@test.com' - - getUserOfflineSubscription.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().getSubscription(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserOfflineSubscription.execute).toHaveBeenCalledWith({ - userEmail: 'test@test.com', - }) - - expect(result.statusCode).toEqual(400) - }) - - it('should not get offline user features if the procedure fails', async () => { - response.locals.offlineUserEmail = 'test@test.com' - - getUserFeatures.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().getOfflineFeatures(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserFeatures.execute).toHaveBeenCalledWith({ - email: 'test@test.com', - offline: true, - }) - - expect(result.statusCode).toEqual(400) - }) - - it('should create a offline subscription token for authenticated user', async () => { - request.body.email = 'test@test.com' - - const httpResponse = await createController().createToken(request) - const result = await httpResponse.executeAsync() - - expect(createOfflineSubscriptionToken.execute).toHaveBeenCalledWith({ - userEmail: 'test@test.com', - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not create a offline subscription token for missing email in request', async () => { - const httpResponse = await createController().createToken(request) - const result = await httpResponse.executeAsync() - - expect(createOfflineSubscriptionToken.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(400) - }) - - it('should not create a offline subscription token if the workflow fails with no subscription', async () => { - request.body.email = 'test@test.com' - - createOfflineSubscriptionToken.execute = jest.fn().mockReturnValue({ - success: false, - error: 'no-subscription', - }) - - const httpResponse = await createController().createToken(request) - const result = await httpResponse.executeAsync() - - expect(createOfflineSubscriptionToken.execute).toHaveBeenCalledWith({ - userEmail: 'test@test.com', - }) - - expect(httpResponse.json).toEqual({ success: false, error: { tag: 'no-subscription' } }) - expect(result.statusCode).toEqual(200) - }) - - it('should validate a offline subscription token for user', async () => { - request.params.token = 'test' - request.body.email = 'test@test.com' - - const httpResponse = await createController().validate(request) - const result = await httpResponse.executeAsync() - - expect(authenticateToken.execute).toHaveBeenCalledWith({ - token: 'test', - userEmail: 'test@test.com', - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not validate a offline subscription token for user if it is invalid', async () => { - request.body.email = 'test@test.com' - request.params.token = 'test' - - authenticateToken.execute = jest.fn().mockReturnValue({ - success: false, - }) - - const httpResponse = await createController().validate(request) - const result = await httpResponse.executeAsync() - - expect(authenticateToken.execute).toHaveBeenCalledWith({ - token: 'test', - userEmail: 'test@test.com', - }) - - expect(result.statusCode).toEqual(401) - }) - - it('should not validate a offline subscription token for user if email is missing', async () => { - request.params.token = 'test' - - const httpResponse = await createController().validate(request) - const result = await httpResponse.executeAsync() - - expect(authenticateToken.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(400) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSessionController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSessionController.spec.ts deleted file mode 100644 index 132a88731..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSessionController.spec.ts +++ /dev/null @@ -1,240 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' - -import { AnnotatedSessionController } from './AnnotatedSessionController' -import { results } from 'inversify-express-utils' -import { DeleteOtherSessionsForUser } from '../../Domain/UseCase/DeleteOtherSessionsForUser' -import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser' -import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken' - -describe('AnnotatedSessionController', () => { - let deleteSessionForUser: DeleteSessionForUser - let deleteOtherSessionsForUser: DeleteOtherSessionsForUser - let refreshSessionToken: RefreshSessionToken - let request: express.Request - let response: express.Response - - const createController = () => - new AnnotatedSessionController(deleteSessionForUser, deleteOtherSessionsForUser, refreshSessionToken) - - beforeEach(() => { - deleteSessionForUser = {} as jest.Mocked - deleteSessionForUser.execute = jest.fn().mockReturnValue({ success: true }) - - deleteOtherSessionsForUser = {} as jest.Mocked - deleteOtherSessionsForUser.execute = jest.fn() - - refreshSessionToken = {} as jest.Mocked - refreshSessionToken.execute = jest.fn() - - request = { - body: {}, - headers: {}, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - response.status = jest.fn().mockReturnThis() - response.setHeader = jest.fn() - response.send = jest.fn() - }) - - it('should refresh session tokens', async () => { - request.body.access_token = '123' - request.body.refresh_token = '234' - - refreshSessionToken.execute = jest.fn().mockReturnValue({ - success: true, - sessionPayload: { - access_token: '1231', - refresh_token: '2341', - access_expiration: 123123, - refresh_expiration: 123123, - }, - }) - - const httpResult = await createController().refresh(request, response) - const result = await httpResult.executeAsync() - - expect(await result.content.readAsStringAsync()).toEqual( - '{"session":{"access_token":"1231","refresh_token":"2341","access_expiration":123123,"refresh_expiration":123123}}', - ) - }) - - it('should return bad request if tokens are missing from refresh token request', async () => { - const httpResponse = await createController().refresh(request, response) - expect(httpResponse.statusCode).toEqual(400) - }) - - it('should return bad request upon failed tokens refreshing', async () => { - request.body.access_token = '123' - request.body.refresh_token = '234' - request.headers['user-agent'] = 'Google Chrome' - - refreshSessionToken.execute = jest.fn().mockReturnValue({ - success: false, - errorTag: 'test', - errorMessage: 'something bad happened', - }) - - const httpResponse = await createController().refresh(request, response) - - expect(httpResponse.json).toEqual({ - error: { - tag: 'test', - message: 'something bad happened', - }, - }) - expect(httpResponse.statusCode).toEqual(400) - }) - - it('should delete a specific session for current user', async () => { - response.locals = { - user: { - uuid: '123', - }, - session: { - uuid: '234', - }, - } - request.body.uuid = '123' - - const httpResult = await createController().deleteSession(request, response) - const result = await httpResult.executeAsync() - - expect(deleteSessionForUser.execute).toBeCalledWith({ - userUuid: '123', - sessionUuid: '123', - }) - - expect(result.statusCode).toEqual(204) - }) - - it('should not delete a specific session is current session has read only access', async () => { - response.locals = { - user: { - uuid: '123', - }, - session: { - uuid: '234', - }, - } - request.body.uuid = '123' - response.locals.readOnlyAccess = true - - const httpResponse = await createController().deleteSession(request, response) - const result = await httpResponse.executeAsync() - - expect(deleteSessionForUser.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should not delete a specific session if request is missing params', async () => { - response.locals = { - user: { - uuid: '123', - }, - session: { - uuid: '234', - }, - } - - const httpResponse = await createController().deleteSession(request, response) - - expect(deleteSessionForUser.execute).not.toHaveBeenCalled() - - expect(httpResponse.statusCode).toEqual(400) - }) - - it('should not delete a specific session if it is the current session', async () => { - response.locals = { - user: { - uuid: '123', - }, - session: { - uuid: '234', - }, - } - request.body.uuid = '234' - - const httpResponse = await createController().deleteSession(request, response) - - expect(deleteSessionForUser.execute).not.toHaveBeenCalled() - - expect(httpResponse.statusCode).toEqual(400) - }) - - it('should respond with failure if deleting a specific session fails', async () => { - response.locals = { - user: { - uuid: '123', - }, - session: { - uuid: '234', - }, - } - request.body.uuid = '123' - - deleteSessionForUser.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().deleteSession(request, response) - - expect(httpResponse.statusCode).toEqual(400) - }) - - it('should delete all sessions except current for current user', async () => { - response.locals = { - user: { - uuid: '123', - }, - session: { - uuid: '234', - }, - } - - const httpResult = await createController().deleteAllSessions(request, response) - const result = await httpResult.executeAsync() - - expect(deleteOtherSessionsForUser.execute).toHaveBeenCalledWith({ - userUuid: '123', - currentSessionUuid: '234', - markAsRevoked: true, - }) - - expect(result.statusCode).toEqual(204) - }) - - it('should not delete all sessions if current sessions has read only access', async () => { - response.locals = { - user: { - uuid: '123', - }, - session: { - uuid: '234', - }, - } - response.locals.readOnlyAccess = true - - const httpResponse = await createController().deleteAllSessions(request, response) - const result = await httpResponse.executeAsync() - - expect(deleteOtherSessionsForUser.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should return unauthorized if current user is missing', async () => { - response.locals = { - session: { - uuid: '234', - }, - } - const httpResponse = await createController().deleteAllSessions(request, response) - - expect(httpResponse.json).toEqual({ error: { message: 'No session exists with the provided identifier.' } }) - expect(httpResponse.statusCode).toEqual(401) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSessionsController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSessionsController.spec.ts deleted file mode 100644 index 094bc5939..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSessionsController.spec.ts +++ /dev/null @@ -1,138 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' - -import { AnnotatedSessionsController } from './AnnotatedSessionsController' -import { results } from 'inversify-express-utils' -import { User } from '@standardnotes/responses' - -import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest' -import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken' -import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser' -import { ProjectorInterface } from '../../Projection/ProjectorInterface' -import { Session } from '../../Domain/Session/Session' -import { Result } from '@standardnotes/domain-core' - -describe('AnnotatedSessionsController', () => { - let getActiveSessionsForUser: GetActiveSessionsForUser - let authenticateRequest: AuthenticateRequest - let sessionProjector: ProjectorInterface - let session: Session - let request: express.Request - let response: express.Response - let user: User - let createCrossServiceToken: CreateCrossServiceToken - - const createController = () => - new AnnotatedSessionsController( - getActiveSessionsForUser, - authenticateRequest, - sessionProjector, - createCrossServiceToken, - ) - - beforeEach(() => { - session = {} as jest.Mocked - - user = {} as jest.Mocked - - getActiveSessionsForUser = {} as jest.Mocked - getActiveSessionsForUser.execute = jest.fn().mockReturnValue({ sessions: [session] }) - - authenticateRequest = {} as jest.Mocked - authenticateRequest.execute = jest.fn() - - sessionProjector = {} as jest.Mocked> - sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' }) - - createCrossServiceToken = {} as jest.Mocked - createCrossServiceToken.execute = jest.fn().mockReturnValue(Result.ok('foobar')) - - request = { - params: {}, - headers: {}, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - }) - - it('should get all active sessions for current user', async () => { - response.locals = { - user: { - uuid: '123', - }, - session: { - uuid: '234', - }, - } - - const httpResponse = await createController().getSessions(request, response) - - expect(httpResponse).toBeInstanceOf(results.JsonResult) - - const result = await httpResponse.executeAsync() - expect(await result.content.readAsStringAsync()).toEqual('[{"foo":"bar"}]') - }) - - it('should validate a session from an incoming request', async () => { - authenticateRequest.execute = jest.fn().mockReturnValue({ - success: true, - user, - session, - }) - - request.headers.authorization = 'test' - - const httpResponse = await createController().validate(request) - - expect(httpResponse).toBeInstanceOf(results.JsonResult) - - const result = await httpResponse.executeAsync() - const httpResponseContent = await result.content.readAsStringAsync() - const httpResponseJSON = JSON.parse(httpResponseContent) - - expect(httpResponseJSON.authToken).toEqual('foobar') - }) - - it('should validate a user from an incoming request', async () => { - authenticateRequest.execute = jest.fn().mockReturnValue({ - success: true, - user, - }) - - request.headers.authorization = 'test' - - const httpResponse = await createController().validate(request) - - expect(httpResponse).toBeInstanceOf(results.JsonResult) - - const result = await httpResponse.executeAsync() - const httpResponseContent = await result.content.readAsStringAsync() - const httpResponseJSON = JSON.parse(httpResponseContent) - - expect(httpResponseJSON.authToken).toEqual('foobar') - }) - - it('should not validate a session from an incoming request', async () => { - authenticateRequest.execute = jest.fn().mockReturnValue({ - success: false, - errorTag: 'invalid-auth', - errorMessage: 'Invalid login credentials.', - responseCode: 401, - }) - - request.headers.authorization = 'test' - - const httpResponse = await createController().validate(request) - - expect(httpResponse).toBeInstanceOf(results.JsonResult) - expect(httpResponse.statusCode).toEqual(401) - - const result = await httpResponse.executeAsync() - expect(await result.content.readAsStringAsync()).toEqual( - '{"error":{"tag":"invalid-auth","message":"Invalid login credentials."}}', - ) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.spec.ts deleted file mode 100644 index 740bd510c..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.spec.ts +++ /dev/null @@ -1,330 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' - -import { AnnotatedSettingsController } from './AnnotatedSettingsController' -import { results } from 'inversify-express-utils' -import { EncryptionVersion } from '../../Domain/Encryption/EncryptionVersion' -import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting' -import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting' -import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings' -import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting' -import { User } from '../../Domain/User/User' -import { Result } from '@standardnotes/domain-core' - -describe('AnnotatedSettingsController', () => { - let deleteSetting: DeleteSetting - let getSettings: GetSettings - let getSetting: GetSetting - let updateSetting: UpdateSetting - - let request: express.Request - let response: express.Response - let user: User - - const createController = () => new AnnotatedSettingsController(getSettings, getSetting, updateSetting, deleteSetting) - - beforeEach(() => { - deleteSetting = {} as jest.Mocked - deleteSetting.execute = jest.fn().mockReturnValue({ success: true }) - - user = {} as jest.Mocked - user.uuid = '123' - - getSettings = {} as jest.Mocked - getSettings.execute = jest.fn() - - getSetting = {} as jest.Mocked - getSetting.execute = jest.fn() - - updateSetting = {} as jest.Mocked - updateSetting.execute = jest.fn() - - request = { - headers: {}, - body: {}, - params: {}, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - }) - - it('should get user settings', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - const httpResponse = await createController().getSettings(request, response) - const result = await httpResponse.executeAsync() - - expect(getSettings.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not get user settings if not allowed', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '2-3-4', - } - - const httpResponse = await createController().getSettings(request, response) - const result = await httpResponse.executeAsync() - - expect(getSettings.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should get user setting', async () => { - request.params.userUuid = '1-2-3' - request.params.settingName = 'test' - response.locals.user = { - uuid: '1-2-3', - } - - getSetting.execute = jest.fn().mockReturnValue(Result.ok()) - - const httpResponse = await createController().getSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not get user setting if not allowed', async () => { - request.params.userUuid = '1-2-3' - request.params.settingName = 'test' - response.locals.user = { - uuid: '2-3-4', - } - - getSetting.execute = jest.fn() - - const httpResponse = await createController().getSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(getSetting.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should fail if could not get user setting', async () => { - request.params.userUuid = '1-2-3' - request.params.settingName = 'test' - response.locals.user = { - uuid: '1-2-3', - } - - getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops')) - - const httpResponse = await createController().getSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' }) - - expect(result.statusCode).toEqual(400) - }) - - it('should update user setting with default encryption', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - request.body = { - name: 'foo', - value: 'bar', - } - - updateSetting.execute = jest.fn().mockReturnValue({ success: true, statusCode: 200 }) - - const httpResponse = await createController().updateSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(updateSetting.execute).toHaveBeenCalledWith({ - props: { - name: 'foo', - sensitive: false, - serverEncryptionVersion: 1, - unencryptedValue: 'bar', - }, - userUuid: '1-2-3', - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should update user setting with different encryption setting', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - request.body = { - name: 'foo', - value: 'bar', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - } - - updateSetting.execute = jest.fn().mockReturnValue({ success: true, statusCode: 200 }) - - const httpResponse = await createController().updateSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(updateSetting.execute).toHaveBeenCalledWith({ - props: { - name: 'foo', - sensitive: false, - serverEncryptionVersion: 0, - unencryptedValue: 'bar', - }, - userUuid: '1-2-3', - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not update user setting if session has read only access', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - response.locals.readOnlyAccess = true - - request.body = { - name: 'foo', - value: 'bar', - } - - const httpResponse = await createController().updateSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(updateSetting.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should not update user setting if not allowed', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '2-3-4', - } - - request.body = { - name: 'foo', - value: 'bar', - serverEncryptionVersion: EncryptionVersion.Default, - } - - updateSetting.execute = jest.fn() - - const httpResponse = await createController().updateSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(updateSetting.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should fail if could not update user setting', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - request.body = { - name: 'foo', - value: 'bar', - serverEncryptionVersion: EncryptionVersion.Default, - } - - updateSetting.execute = jest.fn().mockReturnValue({ success: false, statusCode: 404 }) - - const httpResponse = await createController().updateSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(updateSetting.execute).toHaveBeenCalledWith({ - props: { - name: 'foo', - serverEncryptionVersion: 1, - sensitive: false, - unencryptedValue: 'bar', - }, - userUuid: '1-2-3', - }) - - expect(result.statusCode).toEqual(404) - }) - - it('should delete user setting', async () => { - request.params.userUuid = '1-2-3' - request.params.settingName = 'foo' - response.locals.user = { - uuid: '1-2-3', - } - - deleteSetting.execute = jest.fn().mockReturnValue({ success: true }) - - const httpResponse = await createController().deleteSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(deleteSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'foo' }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not delete user setting if session has read only access', async () => { - request.params.userUuid = '1-2-3' - request.params.settingName = 'foo' - response.locals.user = { - uuid: '1-2-3', - } - response.locals.readOnlyAccess = true - - const httpResponse = await createController().deleteSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(deleteSetting.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should not delete user setting if user is not allowed', async () => { - request.params.userUuid = '1-2-3' - request.params.settingName = 'foo' - response.locals.user = { - uuid: '2-3-4', - } - - deleteSetting.execute = jest.fn() - - const httpResponse = await createController().deleteSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(deleteSetting.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should fail if could not delete user setting', async () => { - request.params.userUuid = '1-2-3' - request.params.settingName = 'foo' - response.locals.user = { - uuid: '1-2-3', - } - - deleteSetting.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().deleteSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(deleteSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'foo' }) - - expect(result.statusCode).toEqual(400) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.ts index 815e118fd..bca0df709 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.ts @@ -11,19 +11,34 @@ import { import TYPES from '../../Bootstrap/Types' import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting' import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting' -import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings' -import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting' import { BaseSettingsController } from './Base/BaseSettingsController' +import { SetSettingValue } from '../../Domain/UseCase/SetSettingValue/SetSettingValue' +import { MapperInterface } from '@standardnotes/domain-core' +import { Setting } from '../../Domain/Setting/Setting' +import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting' +import { SettingHttpRepresentation } from '../../Mapping/Http/SettingHttpRepresentation' +import { SubscriptionSettingHttpRepresentation } from '../../Mapping/Http/SubscriptionSettingHttpRepresentation' +import { GetAllSettingsForUser } from '../../Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser' @controller('/users/:userUuid') export class AnnotatedSettingsController extends BaseSettingsController { constructor( - @inject(TYPES.Auth_GetSettings) override doGetSettings: GetSettings, + @inject(TYPES.Auth_GetAllSettingsForUser) override doGetSettings: GetAllSettingsForUser, @inject(TYPES.Auth_GetSetting) override doGetSetting: GetSetting, - @inject(TYPES.Auth_UpdateSetting) override doUpdateSetting: UpdateSetting, + @inject(TYPES.Auth_SetSettingValue) override setSettingValue: SetSettingValue, @inject(TYPES.Auth_DeleteSetting) override doDeleteSetting: DeleteSetting, + @inject(TYPES.Auth_SettingHttpMapper) settingHttMapper: MapperInterface, + @inject(TYPES.Auth_SubscriptionSettingHttpMapper) + subscriptionSettingHttpMapper: MapperInterface, ) { - super(doGetSettings, doGetSetting, doUpdateSetting, doDeleteSetting) + super( + doGetSettings, + doGetSetting, + setSettingValue, + doDeleteSetting, + settingHttMapper, + subscriptionSettingHttpMapper, + ) } @httpGet('/settings', TYPES.Auth_RequiredCrossServiceTokenMiddleware) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionSettingsController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionSettingsController.spec.ts deleted file mode 100644 index 522ddf353..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionSettingsController.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' - -import { results } from 'inversify-express-utils' -import { AnnotatedSubscriptionSettingsController } from './AnnotatedSubscriptionSettingsController' -import { User } from '../../Domain/User/User' -import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting' -import { Result } from '@standardnotes/domain-core' - -describe('AnnotatedSubscriptionSettingsController', () => { - let getSetting: GetSetting - - let request: express.Request - let response: express.Response - let user: User - - const createController = () => new AnnotatedSubscriptionSettingsController(getSetting) - - beforeEach(() => { - user = {} as jest.Mocked - user.uuid = '123' - - getSetting = {} as jest.Mocked - getSetting.execute = jest.fn() - - request = { - headers: {}, - body: {}, - params: {}, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - }) - - it('should get subscription setting', async () => { - request.params.userUuid = '1-2-3' - request.params.subscriptionSettingName = 'test' - response.locals.user = { - uuid: '1-2-3', - } - - getSetting.execute = jest.fn().mockReturnValue(Result.ok()) - - const httpResponse = await createController().getSubscriptionSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' }) - - expect(result.statusCode).toEqual(200) - }) - - it('should fail if could not get subscription setting', async () => { - request.params.userUuid = '1-2-3' - request.params.subscriptionSettingName = 'test' - response.locals.user = { - uuid: '1-2-3', - } - - getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops')) - - const httpResponse = await createController().getSubscriptionSetting(request, response) - const result = await httpResponse.executeAsync() - - expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' }) - - expect(result.statusCode).toEqual(400) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionSettingsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionSettingsController.ts index b14fc36d9..1c9e2efe3 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionSettingsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionSettingsController.ts @@ -7,13 +7,23 @@ import { results, } from 'inversify-express-utils' import TYPES from '../../Bootstrap/Types' -import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting' import { BaseSubscriptionSettingsController } from './Base/BaseSubscriptionSettingsController' +import { GetSharedOrRegularSubscriptionForUser } from '../../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser' +import { GetSubscriptionSetting } from '../../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting' +import { MapperInterface } from '@standardnotes/domain-core' +import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting' +import { SubscriptionSettingHttpRepresentation } from '../../Mapping/Http/SubscriptionSettingHttpRepresentation' @controller('/users/:userUuid') export class AnnotatedSubscriptionSettingsController extends BaseSubscriptionSettingsController { - constructor(@inject(TYPES.Auth_GetSetting) override doGetSetting: GetSetting) { - super(doGetSetting) + constructor( + @inject(TYPES.Auth_GetSubscriptionSetting) override doGetSetting: GetSubscriptionSetting, + @inject(TYPES.Auth_GetSharedOrRegularSubscriptionForUser) + override getSharedOrRegularSubscription: GetSharedOrRegularSubscriptionForUser, + @inject(TYPES.Auth_SubscriptionSettingHttpMapper) + override subscriptionSettingMapper: MapperInterface, + ) { + super(doGetSetting, getSharedOrRegularSubscription, subscriptionSettingMapper) } @httpGet('/subscription-settings/:subscriptionSettingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionTokensController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionTokensController.spec.ts deleted file mode 100644 index 12fed39b2..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionTokensController.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' -import { results } from 'inversify-express-utils' - -import { AnnotatedSubscriptionTokensController } from './AnnotatedSubscriptionTokensController' -import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security' - -import { Setting } from '../../Domain/Setting/Setting' -import { SettingServiceInterface } from '../../Domain/Setting/SettingServiceInterface' -import { AuthenticateSubscriptionToken } from '../../Domain/UseCase/AuthenticateSubscriptionToken/AuthenticateSubscriptionToken' -import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken' -import { CreateSubscriptionTokenResponse } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionTokenResponse' -import { ProjectorInterface } from '../../Projection/ProjectorInterface' -import { User } from '../../Domain/User/User' -import { Role } from '../../Domain/Role/Role' - -describe('AnnotatedSubscriptionTokensController', () => { - let createSubscriptionToken: CreateSubscriptionToken - let authenticateToken: AuthenticateSubscriptionToken - const jwtTTL = 60 - let userProjector: ProjectorInterface - let roleProjector: ProjectorInterface - let settingService: SettingServiceInterface - let extensionKeySetting: Setting - let tokenEncoder: TokenEncoderInterface - - let request: express.Request - let response: express.Response - let user: User - let role: Role - - const createController = () => - new AnnotatedSubscriptionTokensController( - createSubscriptionToken, - authenticateToken, - settingService, - userProjector, - roleProjector, - tokenEncoder, - jwtTTL, - ) - - beforeEach(() => { - user = {} as jest.Mocked - user.uuid = '123' - user.roles = Promise.resolve([role]) - - createSubscriptionToken = {} as jest.Mocked - createSubscriptionToken.execute = jest.fn().mockReturnValue({ - subscriptionToken: { - token: 'test', - }, - } as jest.Mocked) - - authenticateToken = {} as jest.Mocked - authenticateToken.execute = jest.fn().mockReturnValue({ - success: true, - user, - }) - - userProjector = {} as jest.Mocked> - userProjector.projectSimple = jest.fn().mockReturnValue({ bar: 'baz' }) - - roleProjector = {} as jest.Mocked> - roleProjector.projectSimple = jest.fn().mockReturnValue({ name: 'role1', uuid: '1-3-4' }) - - extensionKeySetting = { - name: 'EXTENSION_KEY', - value: 'abc123', - } as jest.Mocked - - settingService = {} as jest.Mocked - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(extensionKeySetting) - - tokenEncoder = {} as jest.Mocked> - tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar') - - request = { - headers: {}, - body: {}, - params: {}, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - }) - - it('should create an subscription token for authenticated user', async () => { - response.locals.user = { - uuid: '1-2-3', - } - - const httpResponse = await createController().createToken(request, response) - const result = await httpResponse.executeAsync() - - expect(createSubscriptionToken.execute).toHaveBeenCalledWith({ - userUuid: '1-2-3', - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not create an subscription token if session has read only access', async () => { - response.locals.user = { - uuid: '1-2-3', - } - response.locals.readOnlyAccess = true - - const httpResponse = await createController().createToken(request, response) - const result = await httpResponse.executeAsync() - - expect(createSubscriptionToken.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should validate an subscription token for user', async () => { - request.params.token = 'test' - - const httpResponse = await createController().validate(request) - const result = await httpResponse.executeAsync() - - expect(authenticateToken.execute).toHaveBeenCalledWith({ - token: 'test', - }) - - const responseBody = JSON.parse(await result.content.readAsStringAsync()) - expect(responseBody.authToken).toEqual('foobar') - expect(result.statusCode).toEqual(200) - - expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith( - { - extensionKey: 'abc123', - roles: [ - { - name: 'role1', - uuid: '1-3-4', - }, - ], - user: { - bar: 'baz', - }, - }, - 60, - ) - }) - - it('should validate an subscription token for user without an extension key setting', async () => { - request.params.token = 'test' - - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) - - const httpResponse = await createController().validate(request) - const result = await httpResponse.executeAsync() - - expect(authenticateToken.execute).toHaveBeenCalledWith({ - token: 'test', - }) - - const responseBody = JSON.parse(await result.content.readAsStringAsync()) - - expect(responseBody.authToken).toEqual('foobar') - expect(result.statusCode).toEqual(200) - }) - - it('should not validate an subscription token for user if it is invalid', async () => { - request.params.token = 'test' - - authenticateToken.execute = jest.fn().mockReturnValue({ - success: false, - }) - - const httpResponse = await createController().validate(request) - const result = await httpResponse.executeAsync() - - expect(authenticateToken.execute).toHaveBeenCalledWith({ - token: 'test', - }) - - expect(result.statusCode).toEqual(401) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionTokensController.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionTokensController.ts index 4f65f07cc..76ab3bade 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionTokensController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSubscriptionTokensController.ts @@ -10,33 +10,25 @@ import { import TYPES from '../../Bootstrap/Types' import { Role } from '../../Domain/Role/Role' -import { SettingServiceInterface } from '../../Domain/Setting/SettingServiceInterface' import { AuthenticateSubscriptionToken } from '../../Domain/UseCase/AuthenticateSubscriptionToken/AuthenticateSubscriptionToken' import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken' import { User } from '../../Domain/User/User' import { ProjectorInterface } from '../../Projection/ProjectorInterface' import { BaseSubscriptionTokensController } from './Base/BaseSubscriptionTokensController' +import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting' @controller('/subscription-tokens') export class AnnotatedSubscriptionTokensController extends BaseSubscriptionTokensController { constructor( @inject(TYPES.Auth_CreateSubscriptionToken) override createSubscriptionToken: CreateSubscriptionToken, @inject(TYPES.Auth_AuthenticateSubscriptionToken) override authenticateToken: AuthenticateSubscriptionToken, - @inject(TYPES.Auth_SettingService) override settingService: SettingServiceInterface, + @inject(TYPES.Auth_GetSetting) override getSetting: GetSetting, @inject(TYPES.Auth_UserProjector) override userProjector: ProjectorInterface, @inject(TYPES.Auth_RoleProjector) override roleProjector: ProjectorInterface, @inject(TYPES.Auth_CrossServiceTokenEncoder) override tokenEncoder: TokenEncoderInterface, @inject(TYPES.Auth_AUTH_JWT_TTL) override jwtTTL: number, ) { - super( - createSubscriptionToken, - authenticateToken, - settingService, - userProjector, - roleProjector, - tokenEncoder, - jwtTTL, - ) + super(createSubscriptionToken, authenticateToken, getSetting, userProjector, roleProjector, tokenEncoder, jwtTTL) } @httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedUsersController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedUsersController.spec.ts deleted file mode 100644 index 1998395c7..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedUsersController.spec.ts +++ /dev/null @@ -1,396 +0,0 @@ -import 'reflect-metadata' - -import * as express from 'express' - -import { AnnotatedUsersController } from './AnnotatedUsersController' -import { results } from 'inversify-express-utils' -import { Result, Username } from '@standardnotes/domain-core' -import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount' -import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials' -import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts' -import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/GetUserSubscription' -import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts' -import { InviteToSharedSubscription } from '../../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription' -import { UpdateUser } from '../../Domain/UseCase/UpdateUser' -import { User } from '../../Domain/User/User' - -describe('AnnotatedUsersController', () => { - let updateUser: UpdateUser - let deleteAccount: DeleteAccount - let getUserSubscription: GetUserSubscription - let clearLoginAttempts: ClearLoginAttempts - let increaseLoginAttempts: IncreaseLoginAttempts - let changeCredentials: ChangeCredentials - let inviteToSharedSubscription: InviteToSharedSubscription - - let request: express.Request - let response: express.Response - let user: User - - const createController = () => - new AnnotatedUsersController( - updateUser, - deleteAccount, - getUserSubscription, - clearLoginAttempts, - increaseLoginAttempts, - changeCredentials, - ) - - beforeEach(() => { - updateUser = {} as jest.Mocked - updateUser.execute = jest.fn() - - deleteAccount = {} as jest.Mocked - deleteAccount.execute = jest.fn().mockReturnValue(Result.ok('success')) - - user = {} as jest.Mocked - user.uuid = '123' - user.email = 'test@test.te' - - getUserSubscription = {} as jest.Mocked - getUserSubscription.execute = jest.fn() - - changeCredentials = {} as jest.Mocked - changeCredentials.execute = jest.fn() - - clearLoginAttempts = {} as jest.Mocked - clearLoginAttempts.execute = jest.fn() - - increaseLoginAttempts = {} as jest.Mocked - increaseLoginAttempts.execute = jest.fn() - - inviteToSharedSubscription = {} as jest.Mocked - inviteToSharedSubscription.execute = jest.fn() - - request = { - headers: {}, - body: {}, - params: {}, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - - response.status = jest.fn().mockReturnThis() - response.setHeader = jest.fn() - response.send = jest.fn() - }) - - it('should update user', async () => { - request.body.version = '002' - request.body.api = '20190520' - request.body.origination = 'test' - request.params.userId = '123' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - - updateUser.execute = jest.fn().mockReturnValue({ success: true, authResponse: { foo: 'bar' } }) - - const httpResponse = await createController().update(request, response) - const result = await httpResponse.executeAsync() - - expect(updateUser.execute).toHaveBeenCalledWith({ - apiVersion: '20190520', - updatedWithUserAgent: 'Google Chrome', - user: { - uuid: '123', - email: 'test@test.te', - }, - }) - - expect(await result.content.readAsStringAsync()).toEqual('{"foo":"bar"}') - }) - - it('should not update user if session has read only access', async () => { - request.body.version = '002' - request.body.api = '20190520' - request.body.origination = 'test' - request.params.userId = '123' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - response.locals.readOnlyAccess = true - - const httpResponse = await createController().update(request, response) - const result = await httpResponse.executeAsync() - - expect(updateUser.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should not update a user if the procedure fails', async () => { - request.body.version = '002' - request.body.api = '20190520' - request.body.origination = 'test' - request.params.userId = '123' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - - updateUser.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().update(request, response) - const result = await httpResponse.executeAsync() - - expect(updateUser.execute).toHaveBeenCalledWith({ - apiVersion: '20190520', - updatedWithUserAgent: 'Google Chrome', - user: { - uuid: '123', - email: 'test@test.te', - }, - }) - - expect(result.statusCode).toEqual(400) - expect(await result.content.readAsStringAsync()).toEqual('{"error":{"message":"Could not update user."}}') - }) - - it('should not update a user if it is not the same as logged in user', async () => { - request.body.version = '002' - request.body.api = '20190520' - request.body.origination = 'test' - request.params.userId = '234' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - - const httpResponse = await createController().update(request, response) - const result = await httpResponse.executeAsync() - - expect(updateUser.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - expect(await result.content.readAsStringAsync()).toEqual('{"error":{"message":"Operation not allowed."}}') - }) - - it('should delete user', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - const httpResponse = await createController().deleteAccount(request, response) - const result = await httpResponse.executeAsync() - - expect(deleteAccount.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' }) - - expect(result.statusCode).toEqual(200) - }) - - it('should indicate failure when deleting user', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - deleteAccount.execute = jest.fn().mockReturnValue(Result.fail('Something bad happened')) - - const httpResponse = await createController().deleteAccount(request, response) - const result = await httpResponse.executeAsync() - - expect(deleteAccount.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' }) - - expect(result.statusCode).toEqual(400) - }) - - it('should not delete user if user uuid is different than the one in the session', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '2-3-4', - } - - const httpResponse = await createController().deleteAccount(request, response) - const result = await httpResponse.executeAsync() - - expect(deleteAccount.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should get user subscription', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - getUserSubscription.execute = jest.fn().mockReturnValue({ - success: true, - }) - - const httpResponse = await createController().getSubscription(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserSubscription.execute).toHaveBeenCalledWith({ - userUuid: '1-2-3', - }) - - expect(result.statusCode).toEqual(200) - }) - - it('should not get user subscription if the user with provided uuid does not exist', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '1-2-3', - } - - getUserSubscription.execute = jest.fn().mockReturnValue({ - success: false, - }) - - const httpResponse = await createController().getSubscription(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserSubscription.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' }) - - expect(result.statusCode).toEqual(400) - }) - - it('should not get user subscription if not allowed', async () => { - request.params.userUuid = '1-2-3' - response.locals.user = { - uuid: '2-3-4', - } - - getUserSubscription.execute = jest.fn() - - const httpResponse = await createController().getSubscription(request, response) - const result = await httpResponse.executeAsync() - - expect(getUserSubscription.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should change a password', async () => { - request.body.version = '004' - request.body.api = '20190520' - request.body.current_password = 'test123' - request.body.new_password = 'test234' - request.body.pw_nonce = 'asdzxc' - request.body.origination = 'change-password' - request.body.created = '123' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - - changeCredentials.execute = jest.fn().mockReturnValue(Result.ok({ foo: 'bar' })) - - const httpResponse = await createController().changeCredentials(request, response) - const result = await httpResponse.executeAsync() - - expect(changeCredentials.execute).toHaveBeenCalledWith({ - apiVersion: '20190520', - updatedWithUserAgent: 'Google Chrome', - currentPassword: 'test123', - newPassword: 'test234', - kpCreated: '123', - kpOrigination: 'change-password', - pwNonce: 'asdzxc', - protocolVersion: '004', - newEmail: undefined, - username: Username.create('test@test.te').getValue(), - }) - - expect(clearLoginAttempts.execute).toHaveBeenCalled() - - expect(await result.content.readAsStringAsync()).toEqual('{"foo":"bar"}') - }) - - it('should not change a password if session has read only access', async () => { - request.body.version = '004' - request.body.api = '20190520' - request.body.current_password = 'test123' - request.body.new_password = 'test234' - request.body.pw_nonce = 'asdzxc' - request.body.origination = 'change-password' - request.body.created = '123' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - response.locals.readOnlyAccess = true - - const httpResponse = await createController().changeCredentials(request, response) - const result = await httpResponse.executeAsync() - - expect(changeCredentials.execute).not.toHaveBeenCalled() - - expect(clearLoginAttempts.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should indicate if changing a password fails', async () => { - request.body.version = '004' - request.body.api = '20190520' - request.body.current_password = 'test123' - request.body.new_password = 'test234' - request.body.pw_nonce = 'asdzxc' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - - changeCredentials.execute = jest.fn().mockReturnValue(Result.fail('Something bad happened')) - - const httpResponse = await createController().changeCredentials(request, response) - const result = await httpResponse.executeAsync() - - expect(increaseLoginAttempts.execute).toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - expect(await result.content.readAsStringAsync()).toEqual('{"error":{"message":"Something bad happened"}}') - }) - - it('should not change a password if current password is missing', async () => { - request.body.version = '004' - request.body.api = '20190520' - request.body.new_password = 'test234' - request.body.pw_nonce = 'asdzxc' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - - const httpResponse = await createController().changeCredentials(request, response) - const result = await httpResponse.executeAsync() - - expect(changeCredentials.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(400) - expect(await result.content.readAsStringAsync()).toEqual( - '{"error":{"message":"Your current password is required to change your password. Please update your application if you do not see this option."}}', - ) - }) - - it('should not change a password if new password is missing', async () => { - request.body.version = '004' - request.body.api = '20190520' - request.body.current_password = 'test123' - request.body.pw_nonce = 'asdzxc' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - - const httpResponse = await createController().changeCredentials(request, response) - const result = await httpResponse.executeAsync() - - expect(changeCredentials.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(400) - expect(await result.content.readAsStringAsync()).toEqual( - '{"error":{"message":"Your new password is required to change your password. Please try again."}}', - ) - }) - - it('should not change a password if password nonce is missing', async () => { - request.body.version = '004' - request.body.api = '20190520' - request.body.current_password = 'test123' - request.body.new_password = 'test234' - request.headers['user-agent'] = 'Google Chrome' - response.locals.user = user - - const httpResponse = await createController().changeCredentials(request, response) - const result = await httpResponse.executeAsync() - - expect(changeCredentials.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(400) - expect(await result.content.readAsStringAsync()).toEqual( - '{"error":{"message":"The change password request is missing new auth parameters. Please try again."}}', - ) - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedValetTokenController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedValetTokenController.spec.ts deleted file mode 100644 index c90983673..000000000 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedValetTokenController.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import 'reflect-metadata' - -import { Request, Response } from 'express' -import { results } from 'inversify-express-utils' -import { AnnotatedValetTokenController } from './AnnotatedValetTokenController' -import { CreateValetToken } from '../../Domain/UseCase/CreateValetToken/CreateValetToken' - -describe('AnnotatedValetTokenController', () => { - let createValetToken: CreateValetToken - let request: Request - let response: Response - - const createController = () => new AnnotatedValetTokenController(createValetToken) - - beforeEach(() => { - createValetToken = {} as jest.Mocked - createValetToken.execute = jest.fn().mockReturnValue({ success: true, valetToken: 'foobar' }) - - request = { - body: { - operation: 'write', - resources: [{ remoteIdentifier: '00000000-0000-0000-0000-000000000000' }], - }, - } as jest.Mocked - - response = { - locals: {}, - } as jest.Mocked - - response.locals.user = { uuid: '1-2-3' } - }) - - it('should create a valet token', async () => { - const httpResponse = await createController().create(request, response) - const result = await httpResponse.executeAsync() - - expect(createValetToken.execute).toHaveBeenCalledWith({ - operation: 'write', - userUuid: '1-2-3', - resources: [{ remoteIdentifier: '00000000-0000-0000-0000-000000000000' }], - }) - expect(await result.content.readAsStringAsync()).toEqual('{"success":true,"valetToken":"foobar"}') - }) - - it('should not create a valet token if the remote resource identifier is not a valid uuid', async () => { - request.body.resources = ['00000000-0000-0000-0000-000000000000', 'invalid-uuid'] - - const httpResponse = await createController().create(request, response) - const result = await httpResponse.executeAsync() - - expect(createValetToken.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(400) - }) - - it('should create a read valet token for read only access session', async () => { - response.locals.readOnlyAccess = true - request.body.operation = 'read' - - const httpResponse = await createController().create(request, response) - const result = await httpResponse.executeAsync() - - expect(createValetToken.execute).toHaveBeenCalledWith({ - operation: 'read', - userUuid: '1-2-3', - resources: [{ remoteIdentifier: '00000000-0000-0000-0000-000000000000' }], - }) - expect(await result.content.readAsStringAsync()).toEqual('{"success":true,"valetToken":"foobar"}') - }) - - it('should not create a write valet token if session has read only access', async () => { - response.locals.readOnlyAccess = true - request.body.operation = 'write' - - const httpResponse = await createController().create(request, response) - const result = await httpResponse.executeAsync() - - expect(createValetToken.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should not create a delete valet token if session has read only access', async () => { - response.locals.readOnlyAccess = true - request.body.operation = 'delete' - - const httpResponse = await createController().create(request, response) - const result = await httpResponse.executeAsync() - - expect(createValetToken.execute).not.toHaveBeenCalled() - - expect(result.statusCode).toEqual(401) - }) - - it('should not create a valet token if use case fails', async () => { - createValetToken.execute = jest.fn().mockReturnValue({ success: false }) - - const httpResponse = await createController().create(request, response) - const result = await httpResponse.executeAsync() - - expect(createValetToken.execute).toHaveBeenCalledWith({ - operation: 'write', - userUuid: '1-2-3', - resources: [{ remoteIdentifier: '00000000-0000-0000-0000-000000000000' }], - }) - - expect(await result.content.readAsStringAsync()).toEqual('{"success":false}') - }) -}) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts index 015e3ab5f..3f658090a 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts @@ -1,20 +1,28 @@ -import { ControllerContainerInterface } from '@standardnotes/domain-core' +import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core' +import { BaseHttpController, results } from 'inversify-express-utils' import { ErrorTag } from '@standardnotes/responses' import { Request, Response } from 'express' import { DeleteSetting } from '../../../Domain/UseCase/DeleteSetting/DeleteSetting' import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting' -import { GetSettings } from '../../../Domain/UseCase/GetSettings/GetSettings' -import { UpdateSetting } from '../../../Domain/UseCase/UpdateSetting/UpdateSetting' -import { BaseHttpController, results } from 'inversify-express-utils' -import { EncryptionVersion } from '../../../Domain/Encryption/EncryptionVersion' +import { GetAllSettingsForUser } from '../../../Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser' +import { SetSettingValue } from '../../../Domain/UseCase/SetSettingValue/SetSettingValue' +import { Setting } from '../../../Domain/Setting/Setting' +import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting' +import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation' +import { SettingHttpRepresentation } from '../../../Mapping/Http/SettingHttpRepresentation' export class BaseSettingsController extends BaseHttpController { constructor( - protected doGetSettings: GetSettings, + protected doGetSettings: GetAllSettingsForUser, protected doGetSetting: GetSetting, - protected doUpdateSetting: UpdateSetting, + protected setSettingValue: SetSettingValue, protected doDeleteSetting: DeleteSetting, + protected settingHttMapper: MapperInterface, + protected subscriptionSettingHttpMapper: MapperInterface< + SubscriptionSetting, + SubscriptionSettingHttpRepresentation + >, private controllerContainer?: ControllerContainerInterface, ) { super() @@ -41,8 +49,36 @@ export class BaseSettingsController extends BaseHttpController { const { userUuid } = request.params const result = await this.doGetSettings.execute({ userUuid }) + if (result.isFailed()) { + return this.json( + { + error: { + message: result.getError(), + }, + }, + 400, + ) + } + const settingsAndSubscriptionSettings = result.getValue() - return this.json(result) + const settingsHttpRepresentation = settingsAndSubscriptionSettings.settings.map((settingAndValue) => ({ + ...this.settingHttMapper.toProjection(settingAndValue.setting), + value: settingAndValue.decryptedValue, + })) + + const subscriptionSettingsHttpRepresentation = settingsAndSubscriptionSettings.subscriptionSettings.map( + (settingAndValue) => ({ + ...this.subscriptionSettingHttpMapper.toProjection(settingAndValue.setting), + value: settingAndValue.decryptedValue, + }), + ) + + const httpRepresentation = settingsHttpRepresentation.concat(subscriptionSettingsHttpRepresentation) + + return this.json({ + success: true, + settings: httpRepresentation, + }) } async getSetting(request: Request, response: Response): Promise { @@ -58,7 +94,12 @@ export class BaseSettingsController extends BaseHttpController { } const { userUuid, settingName } = request.params - const resultOrError = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() }) + const resultOrError = await this.doGetSetting.execute({ + allowSensitiveRetrieval: true, + userUuid, + decrypted: true, + settingName: settingName.toUpperCase(), + }) if (resultOrError.isFailed()) { return this.json( { @@ -70,9 +111,22 @@ export class BaseSettingsController extends BaseHttpController { ) } + const settingAndValue = resultOrError.getValue() + + if (settingAndValue.setting.props.sensitive) { + return this.json({ + success: true, + }) + } + + const settingHttpReprepesentation = { + ...this.settingHttMapper.toProjection(settingAndValue.setting), + value: settingAndValue.decryptedValue, + } + return this.json({ success: true, - ...resultOrError.getValue(), + setting: settingHttpReprepesentation, }) } @@ -100,26 +154,30 @@ export class BaseSettingsController extends BaseHttpController { ) } - const { name, value, serverEncryptionVersion = EncryptionVersion.Default, sensitive = false } = request.body + const { name, value } = request.body - const props = { - name, - unencryptedValue: value, - serverEncryptionVersion, - sensitive, - } - - const { userUuid } = request.params - const result = await this.doUpdateSetting.execute({ - userUuid, - props, + const result = await this.setSettingValue.execute({ + settingName: name, + value, + userUuid: response.locals.user.uuid, }) - if (result.success) { - return this.json({ setting: result.setting }, result.statusCode) + if (result.isFailed()) { + return this.json( + { + error: { + message: result.getError(), + }, + }, + 400, + ) } + const setting = result.getValue() - return this.json(result, result.statusCode) + return this.json({ + success: true, + setting: setting.props.sensitive ? undefined : this.settingHttMapper.toProjection(setting), + }) } async deleteSetting(request: Request, response: Response): Promise { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController.ts index d770c6f96..e740a1bdb 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController.ts @@ -1,12 +1,17 @@ -import { ControllerContainerInterface } from '@standardnotes/domain-core' +import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core' import { BaseHttpController, results } from 'inversify-express-utils' import { Request, Response } from 'express' -import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting' +import { GetSubscriptionSetting } from '../../../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting' +import { GetSharedOrRegularSubscriptionForUser } from '../../../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser' +import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting' +import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation' export class BaseSubscriptionSettingsController extends BaseHttpController { constructor( - protected doGetSetting: GetSetting, + protected doGetSetting: GetSubscriptionSetting, + protected getSharedOrRegularSubscription: GetSharedOrRegularSubscriptionForUser, + protected subscriptionSettingMapper: MapperInterface, private controllerContainer?: ControllerContainerInterface, ) { super() @@ -17,8 +22,24 @@ export class BaseSubscriptionSettingsController extends BaseHttpController { } async getSubscriptionSetting(request: Request, response: Response): Promise { - const resultOrError = await this.doGetSetting.execute({ + const subscriptionOrError = await this.getSharedOrRegularSubscription.execute({ userUuid: response.locals.user.uuid, + }) + if (subscriptionOrError.isFailed()) { + return this.json( + { + error: { + message: subscriptionOrError.getError(), + }, + }, + 400, + ) + } + const subscription = subscriptionOrError.getValue() + + const resultOrError = await this.doGetSetting.execute({ + userSubscriptionUuid: subscription.uuid, + allowSensitiveRetrieval: false, settingName: request.params.subscriptionSettingName.toUpperCase(), }) @@ -33,9 +54,11 @@ export class BaseSubscriptionSettingsController extends BaseHttpController { ) } + const settingAndValue = resultOrError.getValue() + return this.json({ success: true, - ...resultOrError.getValue(), + setting: this.subscriptionSettingMapper.toProjection(settingAndValue.setting), }) } } diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController.ts index 84fa186eb..cdcfc2180 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController.ts @@ -4,18 +4,18 @@ import { Role, TokenEncoderInterface, CrossServiceTokenData } from '@standardnot import { BaseHttpController, results } from 'inversify-express-utils' import { Request, Response } from 'express' -import { SettingServiceInterface } from '../../../Domain/Setting/SettingServiceInterface' import { AuthenticateSubscriptionToken } from '../../../Domain/UseCase/AuthenticateSubscriptionToken/AuthenticateSubscriptionToken' import { CreateSubscriptionToken } from '../../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken' import { ProjectorInterface } from '../../../Projection/ProjectorInterface' import { SettingName } from '@standardnotes/settings' import { User } from '../../../Domain/User/User' +import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting' export class BaseSubscriptionTokensController extends BaseHttpController { constructor( protected createSubscriptionToken: CreateSubscriptionToken, protected authenticateToken: AuthenticateSubscriptionToken, - protected settingService: SettingServiceInterface, + protected getSetting: GetSetting, protected userProjector: ProjectorInterface, protected roleProjector: ProjectorInterface, protected tokenEncoder: TokenEncoderInterface, @@ -70,12 +70,15 @@ export class BaseSubscriptionTokensController extends BaseHttpController { const user = authenticateTokenResponse.user as User let extensionKey = undefined - const extensionKeySetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.create(SettingName.NAMES.ExtensionKey).getValue(), + const extensionKeySettingOrError = await this.getSetting.execute({ + settingName: SettingName.NAMES.ExtensionKey, userUuid: user.uuid, + allowSensitiveRetrieval: true, + decrypted: true, }) - if (extensionKeySetting !== null) { - extensionKey = extensionKeySetting.value as string + if (!extensionKeySettingOrError.isFailed()) { + const extensionKeySetting = extensionKeySettingOrError.getValue() + extensionKey = extensionKeySetting.decryptedValue as string } const roles = await user.roles diff --git a/packages/auth/src/Infra/TypeORM/TypeORMSetting.ts b/packages/auth/src/Infra/TypeORM/TypeORMSetting.ts new file mode 100644 index 000000000..660b22901 --- /dev/null +++ b/packages/auth/src/Infra/TypeORM/TypeORMSetting.ts @@ -0,0 +1,53 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm' + +@Entity({ name: 'settings' }) +@Index('index_settings_on_name_and_user_uuid', ['name', 'userUuid']) +export class TypeORMSetting { + @PrimaryGeneratedColumn('uuid') + declare uuid: string + + @Column({ + length: 255, + }) + declare name: string + + @Column({ + type: 'text', + nullable: true, + }) + declare value: string | null + + @Column({ + name: 'server_encryption_version', + type: 'tinyint', + default: 0, + }) + declare serverEncryptionVersion: number + + @Column({ + name: 'created_at', + type: 'bigint', + }) + declare createdAt: number + + @Column({ + name: 'updated_at', + type: 'bigint', + }) + @Index('index_settings_on_updated_at') + declare updatedAt: number + + @Column({ + name: 'user_uuid', + length: 36, + }) + declare userUuid: string + + @Column({ + type: 'tinyint', + width: 1, + nullable: false, + default: 0, + }) + declare sensitive: boolean +} diff --git a/packages/auth/src/Infra/TypeORM/TypeORMSettingRepository.ts b/packages/auth/src/Infra/TypeORM/TypeORMSettingRepository.ts index 97253b83b..d5975788e 100644 --- a/packages/auth/src/Infra/TypeORM/TypeORMSettingRepository.ts +++ b/packages/auth/src/Infra/TypeORM/TypeORMSettingRepository.ts @@ -1,32 +1,46 @@ import { SettingName } from '@standardnotes/settings' import { ReadStream } from 'fs' -import { inject, injectable } from 'inversify' import { Repository } from 'typeorm' -import TYPES from '../../Bootstrap/Types' + import { Setting } from '../../Domain/Setting/Setting' import { SettingRepositoryInterface } from '../../Domain/Setting/SettingRepositoryInterface' import { DeleteSettingDto } from '../../Domain/UseCase/DeleteSetting/DeleteSettingDto' +import { TypeORMSetting } from './TypeORMSetting' +import { MapperInterface } from '@standardnotes/domain-core' -@injectable() export class TypeORMSettingRepository implements SettingRepositoryInterface { constructor( - @inject(TYPES.Auth_ORMSettingRepository) - private ormRepository: Repository, + private ormRepository: Repository, + private mapper: MapperInterface, ) {} - async save(setting: Setting): Promise { - return this.ormRepository.save(setting) + async insert(setting: Setting): Promise { + const persistence = this.mapper.toProjection(setting) + + await this.ormRepository.insert(persistence) + } + + async update(setting: Setting): Promise { + const persistence = this.mapper.toProjection(setting) + + await this.ormRepository.update(persistence.uuid, persistence) } async findOneByUuidAndNames(uuid: string, names: SettingName[]): Promise { const nameValues = names.map((name) => name.value) - return this.ormRepository + const persistence = await this.ormRepository .createQueryBuilder('setting') .where('setting.uuid = :uuid AND setting.name IN (:...names)', { names: nameValues, uuid, }) .getOne() + + if (persistence === null) { + return null + } + + return this.mapper.toDomain(persistence) } async streamAllByName(name: SettingName): Promise { @@ -51,22 +65,34 @@ export class TypeORMSettingRepository implements SettingRepositoryInterface { } async findOneByUuid(uuid: string): Promise { - return this.ormRepository + const persistence = await this.ormRepository .createQueryBuilder('setting') .where('setting.uuid = :uuid', { uuid, }) .getOne() + + if (persistence === null) { + return null + } + + return this.mapper.toDomain(persistence) } async findOneByNameAndUserUuid(name: string, userUuid: string): Promise { - return this.ormRepository + const persistence = await this.ormRepository .createQueryBuilder('setting') .where('setting.name = :name AND setting.user_uuid = :user_uuid', { name, user_uuid: userUuid, }) .getOne() + + if (persistence === null) { + return null + } + + return this.mapper.toDomain(persistence) } async findLastByNameAndUserUuid(name: string, userUuid: string): Promise { @@ -84,16 +110,18 @@ export class TypeORMSettingRepository implements SettingRepositoryInterface { return null } - return settings.pop() as Setting + return this.mapper.toDomain(settings.pop() as TypeORMSetting) } async findAllByUserUuid(userUuid: string): Promise { - return this.ormRepository + const persistence = await this.ormRepository .createQueryBuilder('setting') .where('setting.user_uuid = :user_uuid', { user_uuid: userUuid, }) .getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) } async deleteByUserUuid({ settingName, userUuid }: DeleteSettingDto): Promise { diff --git a/packages/auth/src/Infra/TypeORM/TypeORMSubscriptionSetting.ts b/packages/auth/src/Infra/TypeORM/TypeORMSubscriptionSetting.ts new file mode 100644 index 000000000..8ff2f324f --- /dev/null +++ b/packages/auth/src/Infra/TypeORM/TypeORMSubscriptionSetting.ts @@ -0,0 +1,53 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm' + +@Entity({ name: 'subscription_settings' }) +@Index('index_settings_on_name_and_user_subscription_uuid', ['name', 'userSubscriptionUuid']) +export class TypeORMSubscriptionSetting { + @PrimaryGeneratedColumn('uuid') + declare uuid: string + + @Column({ + length: 255, + }) + declare name: string + + @Column({ + type: 'text', + nullable: true, + }) + declare value: string | null + + @Column({ + name: 'server_encryption_version', + type: 'tinyint', + default: 0, + }) + declare serverEncryptionVersion: number + + @Column({ + name: 'created_at', + type: 'bigint', + }) + declare createdAt: number + + @Column({ + name: 'updated_at', + type: 'bigint', + }) + @Index('index_subcsription_settings_on_updated_at') + declare updatedAt: number + + @Column({ + name: 'user_subscription_uuid', + length: 36, + }) + declare userSubscriptionUuid: string + + @Column({ + type: 'tinyint', + width: 1, + nullable: false, + default: 0, + }) + declare sensitive: boolean +} diff --git a/packages/auth/src/Infra/TypeORM/TypeORMSubscriptionSettingRepository.ts b/packages/auth/src/Infra/TypeORM/TypeORMSubscriptionSettingRepository.ts index 4c5f4b2a5..d3f0c7a15 100644 --- a/packages/auth/src/Infra/TypeORM/TypeORMSubscriptionSettingRepository.ts +++ b/packages/auth/src/Infra/TypeORM/TypeORMSubscriptionSettingRepository.ts @@ -1,50 +1,72 @@ -import { inject, injectable } from 'inversify' import { Repository } from 'typeorm' -import TYPES from '../../Bootstrap/Types' +import { MapperInterface, Uuid } from '@standardnotes/domain-core' -import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting' import { SubscriptionSettingRepositoryInterface } from '../../Domain/Setting/SubscriptionSettingRepositoryInterface' +import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting' + +import { TypeORMSubscriptionSetting } from './TypeORMSubscriptionSetting' -@injectable() export class TypeORMSubscriptionSettingRepository implements SubscriptionSettingRepositoryInterface { constructor( - @inject(TYPES.Auth_ORMSubscriptionSettingRepository) - private ormRepository: Repository, + private ormRepository: Repository, + private mapper: MapperInterface, ) {} - async findAllBySubscriptionUuid(userSubscriptionUuid: string): Promise { - return this.ormRepository + async findAllBySubscriptionUuid(userSubscriptionUuid: Uuid): Promise { + const persistence = await this.ormRepository .createQueryBuilder('setting') .where('setting.user_subscription_uuid = :userSubscriptionUuid', { - userSubscriptionUuid, + userSubscriptionUuid: userSubscriptionUuid.value, }) .getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) } - async save(subscriptionSetting: SubscriptionSetting): Promise { - return this.ormRepository.save(subscriptionSetting) + async insert(subscriptionSetting: SubscriptionSetting): Promise { + const persistence = this.mapper.toProjection(subscriptionSetting) + + await this.ormRepository.insert(persistence) } - async findOneByUuid(uuid: string): Promise { - return this.ormRepository + async update(subscriptionSetting: SubscriptionSetting): Promise { + const persistence = this.mapper.toProjection(subscriptionSetting) + + await this.ormRepository.update(persistence.uuid, persistence) + } + + async findOneByUuid(uuid: Uuid): Promise { + const persistence = await this.ormRepository .createQueryBuilder('setting') .where('setting.uuid = :uuid', { - uuid, + uuid: uuid.value, }) .getOne() + + if (!persistence) { + return null + } + + return this.mapper.toDomain(persistence) } async findLastByNameAndUserSubscriptionUuid( name: string, - userSubscriptionUuid: string, + userSubscriptionUuid: Uuid, ): Promise { - return this.ormRepository + const persistence = await this.ormRepository .createQueryBuilder('setting') .where('setting.name = :name AND setting.user_subscription_uuid = :userSubscriptionUuid', { name, - userSubscriptionUuid, + userSubscriptionUuid: userSubscriptionUuid.value, }) .orderBy('updated_at', 'DESC') .getOne() + + if (!persistence) { + return null + } + + return this.mapper.toDomain(persistence) } } diff --git a/packages/auth/src/Infra/TypeORM/TypeORMUserSubscriptionRepository.ts b/packages/auth/src/Infra/TypeORM/TypeORMUserSubscriptionRepository.ts index 511475416..9194c9acb 100644 --- a/packages/auth/src/Infra/TypeORM/TypeORMUserSubscriptionRepository.ts +++ b/packages/auth/src/Infra/TypeORM/TypeORMUserSubscriptionRepository.ts @@ -95,16 +95,22 @@ export class TypeORMUserSubscriptionRepository implements UserSubscriptionReposi .orderBy('ends_at', 'DESC') .getMany() - const uncanceled = subscriptions.find((subscription) => !subscription.cancelled) - if (uncanceled !== undefined) { - return uncanceled - } + return this.firstUncancelled(subscriptions) + } - if (subscriptions.length !== 0) { - return subscriptions[0] - } + async findOneByUserUuidAndType(userUuid: string, type: UserSubscriptionType): Promise { + const subscriptions = await this.ormRepository + .createQueryBuilder() + .where('user_uuid = :user_uuid', { + user_uuid: userUuid, + }) + .andWhere('subscription_type = :type', { + type, + }) + .orderBy('ends_at', 'DESC') + .getMany() - return null + return this.firstUncancelled(subscriptions) } async updateEndsAt(subscriptionId: number, endsAt: number, timestamp: number): Promise { @@ -135,4 +141,17 @@ export class TypeORMUserSubscriptionRepository implements UserSubscriptionReposi }) .execute() } + + private firstUncancelled(subscriptions: UserSubscription[]): UserSubscription | null { + const uncanceled = subscriptions.find((subscription) => !subscription.cancelled) + if (uncanceled !== undefined) { + return uncanceled + } + + if (subscriptions.length !== 0) { + return subscriptions[0] + } + + return null + } } diff --git a/packages/auth/src/Mapping/Http/SettingHttpMapper.ts b/packages/auth/src/Mapping/Http/SettingHttpMapper.ts new file mode 100644 index 000000000..945e87586 --- /dev/null +++ b/packages/auth/src/Mapping/Http/SettingHttpMapper.ts @@ -0,0 +1,21 @@ +import { MapperInterface } from '@standardnotes/domain-core' + +import { Setting } from '../../Domain/Setting/Setting' +import { SettingHttpRepresentation } from './SettingHttpRepresentation' + +export class SettingHttpMapper implements MapperInterface { + toDomain(_projection: SettingHttpRepresentation): Setting { + throw new Error('Mapping from http representation to domain is not implemented.') + } + + toProjection(domain: Setting): SettingHttpRepresentation { + return { + uuid: domain.id.toString(), + name: domain.props.name, + value: domain.props.value, + createdAt: domain.props.timestamps.createdAt, + updatedAt: domain.props.timestamps.updatedAt, + sensitive: domain.props.sensitive, + } + } +} diff --git a/packages/auth/src/Mapping/Http/SettingHttpRepresentation.ts b/packages/auth/src/Mapping/Http/SettingHttpRepresentation.ts new file mode 100644 index 000000000..84ac9b077 --- /dev/null +++ b/packages/auth/src/Mapping/Http/SettingHttpRepresentation.ts @@ -0,0 +1,8 @@ +export interface SettingHttpRepresentation { + uuid: string + name: string + value: string | null + createdAt: number + updatedAt: number + sensitive: boolean +} diff --git a/packages/auth/src/Mapping/Http/SubscriptionSettingHttpMapper.ts b/packages/auth/src/Mapping/Http/SubscriptionSettingHttpMapper.ts new file mode 100644 index 000000000..03dcd3854 --- /dev/null +++ b/packages/auth/src/Mapping/Http/SubscriptionSettingHttpMapper.ts @@ -0,0 +1,22 @@ +import { MapperInterface } from '@standardnotes/domain-core' +import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting' +import { SubscriptionSettingHttpRepresentation } from './SubscriptionSettingHttpRepresentation' + +export class SubscriptionSettingHttpMapper + implements MapperInterface +{ + toDomain(_projection: SubscriptionSettingHttpRepresentation): SubscriptionSetting { + throw new Error('Mapping from http representation to domain is not implemented.') + } + + toProjection(domain: SubscriptionSetting): SubscriptionSettingHttpRepresentation { + return { + uuid: domain.id.toString(), + name: domain.props.name, + value: domain.props.value, + createdAt: domain.props.timestamps.createdAt, + updatedAt: domain.props.timestamps.updatedAt, + sensitive: domain.props.sensitive, + } + } +} diff --git a/packages/auth/src/Mapping/Http/SubscriptionSettingHttpRepresentation.ts b/packages/auth/src/Mapping/Http/SubscriptionSettingHttpRepresentation.ts new file mode 100644 index 000000000..304b05045 --- /dev/null +++ b/packages/auth/src/Mapping/Http/SubscriptionSettingHttpRepresentation.ts @@ -0,0 +1,8 @@ +export interface SubscriptionSettingHttpRepresentation { + uuid: string + name: string + value: string | null + createdAt: number + updatedAt: number + sensitive: boolean +} diff --git a/packages/auth/src/Mapping/Persistence/SettingPersistenceMapper.ts b/packages/auth/src/Mapping/Persistence/SettingPersistenceMapper.ts new file mode 100644 index 000000000..ace7d1eb8 --- /dev/null +++ b/packages/auth/src/Mapping/Persistence/SettingPersistenceMapper.ts @@ -0,0 +1,52 @@ +import { MapperInterface, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core' +import { Setting } from '../../Domain/Setting/Setting' +import { TypeORMSetting } from '../../Infra/TypeORM/TypeORMSetting' + +export class SettingPersistenceMapper implements MapperInterface { + toDomain(projection: TypeORMSetting): Setting { + const timestampsOrError = Timestamps.create(projection.createdAt, projection.updatedAt) + if (timestampsOrError.isFailed()) { + throw new Error(`Failed to create setting from projection: ${timestampsOrError.getError()}`) + } + const timestamps = timestampsOrError.getValue() + + const userUuidOrError = Uuid.create(projection.userUuid) + if (userUuidOrError.isFailed()) { + throw new Error(`Failed to create setting from projection: ${userUuidOrError.getError()}`) + } + const userUuid = userUuidOrError.getValue() + + const settingOrError = Setting.create( + { + name: projection.name, + value: projection.value, + serverEncryptionVersion: projection.serverEncryptionVersion, + sensitive: !!projection.sensitive, + userUuid, + timestamps, + }, + new UniqueEntityId(projection.uuid), + ) + if (settingOrError.isFailed()) { + throw new Error(`Failed to create setting from projection: ${settingOrError.getError()}`) + } + const setting = settingOrError.getValue() + + return setting + } + + toProjection(domain: Setting): TypeORMSetting { + const projection = new TypeORMSetting() + + projection.uuid = domain.id.toString() + projection.name = domain.props.name + projection.value = domain.props.value + projection.serverEncryptionVersion = domain.props.serverEncryptionVersion + projection.createdAt = domain.props.timestamps.createdAt + projection.updatedAt = domain.props.timestamps.updatedAt + projection.userUuid = domain.props.userUuid.value + projection.sensitive = !!domain.props.sensitive + + return projection + } +} diff --git a/packages/auth/src/Mapping/Persistence/SubscriptionSettingPersistenceMapper.ts b/packages/auth/src/Mapping/Persistence/SubscriptionSettingPersistenceMapper.ts new file mode 100644 index 000000000..9ad2352af --- /dev/null +++ b/packages/auth/src/Mapping/Persistence/SubscriptionSettingPersistenceMapper.ts @@ -0,0 +1,55 @@ +import { MapperInterface, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core' +import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting' +import { TypeORMSubscriptionSetting } from '../../Infra/TypeORM/TypeORMSubscriptionSetting' + +export class SubscriptionSettingPersistenceMapper + implements MapperInterface +{ + toDomain(projection: TypeORMSubscriptionSetting): SubscriptionSetting { + const timestampsOrError = Timestamps.create(projection.createdAt, projection.updatedAt) + if (timestampsOrError.isFailed()) { + throw new Error(`Failed to create subscription setting from projection: ${timestampsOrError.getError()}`) + } + const timestamps = timestampsOrError.getValue() + + const userSubscriptionUuidOrError = Uuid.create(projection.userSubscriptionUuid) + if (userSubscriptionUuidOrError.isFailed()) { + throw new Error( + `Failed to create subscription setting from projection: ${userSubscriptionUuidOrError.getError()}`, + ) + } + const userSubscriptionUuid = userSubscriptionUuidOrError.getValue() + + const subscriptionSettingOrError = SubscriptionSetting.create( + { + name: projection.name, + value: projection.value, + serverEncryptionVersion: projection.serverEncryptionVersion, + sensitive: projection.sensitive, + userSubscriptionUuid, + timestamps, + }, + new UniqueEntityId(projection.uuid), + ) + if (subscriptionSettingOrError.isFailed()) { + throw new Error(`Failed to create subscription setting from projection: ${subscriptionSettingOrError.getError()}`) + } + const subscriptionSetting = subscriptionSettingOrError.getValue() + + return subscriptionSetting + } + + toProjection(domain: SubscriptionSetting): TypeORMSubscriptionSetting { + const projection = new TypeORMSubscriptionSetting() + + projection.uuid = domain.id.toString() + projection.name = domain.props.name + projection.value = domain.props.value + projection.serverEncryptionVersion = domain.props.serverEncryptionVersion + projection.createdAt = domain.props.timestamps.createdAt + projection.updatedAt = domain.props.timestamps.updatedAt + projection.userSubscriptionUuid = domain.props.userSubscriptionUuid.value + + return projection + } +} diff --git a/packages/auth/src/Projection/SettingProjector.ts b/packages/auth/src/Projection/SettingProjector.ts deleted file mode 100644 index 7c07ad0c0..000000000 --- a/packages/auth/src/Projection/SettingProjector.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { injectable } from 'inversify' - -import { Setting } from '../Domain/Setting/Setting' -import { SimpleSetting } from '../Domain/Setting/SimpleSetting' - -@injectable() -export class SettingProjector { - async projectSimple(setting: Setting): Promise { - return { - uuid: setting.uuid, - name: setting.name, - value: setting.value, - createdAt: setting.createdAt, - updatedAt: setting.updatedAt, - sensitive: setting.sensitive, - } - } - - async projectManySimple(settings: Setting[]): Promise { - return Promise.all( - settings.map(async (setting) => { - return this.projectSimple(setting) - }), - ) - } -} diff --git a/packages/auth/src/Projection/SubscriptionSettingProjector.ts b/packages/auth/src/Projection/SubscriptionSettingProjector.ts deleted file mode 100644 index 00b6682a4..000000000 --- a/packages/auth/src/Projection/SubscriptionSettingProjector.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { injectable } from 'inversify' - -import { SimpleSubscriptionSetting } from '../Domain/Setting/SimpleSubscriptionSetting' -import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting' - -@injectable() -export class SubscriptionSettingProjector { - async projectSimple(setting: SubscriptionSetting): Promise { - return { - uuid: setting.uuid, - name: setting.name, - value: setting.value, - createdAt: setting.createdAt, - updatedAt: setting.updatedAt, - sensitive: setting.sensitive, - } - } - - async projectManySimple(settings: SubscriptionSetting[]): Promise { - return Promise.all( - settings.map(async (setting) => { - return this.projectSimple(setting) - }), - ) - } -} diff --git a/packages/domain-core/src/Domain/Common/Username.spec.ts b/packages/domain-core/src/Domain/Common/Username.spec.ts index 0dc9d3e01..bfe3fdeeb 100644 --- a/packages/domain-core/src/Domain/Common/Username.spec.ts +++ b/packages/domain-core/src/Domain/Common/Username.spec.ts @@ -19,4 +19,16 @@ describe('Username', () => { expect(valueOrError.isFailed()).toBeTruthy() }) + + it('should indicate if the username is potentially a vault account', () => { + const value = Username.create('a75a31ce95365904ef0e0a8e6cefc1f5e99adfef81bbdb6d4499eeb10ae0ff67').getValue() + + expect(value.isPotentiallyAPrivateUsernameAccount()).toBeTruthy() + }) + + it('should indicate if the user is not a vault account', () => { + const value = Username.create('test@test.te').getValue() + + expect(value.isPotentiallyAPrivateUsernameAccount()).toBeFalsy() + }) }) diff --git a/packages/domain-core/src/Domain/Common/Username.ts b/packages/domain-core/src/Domain/Common/Username.ts index 48b9ced27..0b371800f 100644 --- a/packages/domain-core/src/Domain/Common/Username.ts +++ b/packages/domain-core/src/Domain/Common/Username.ts @@ -25,4 +25,8 @@ export class Username extends ValueObject { return Result.ok(new Username({ value: trimmedAndLowerCasedUsername })) } + + isPotentiallyAPrivateUsernameAccount(): boolean { + return this.value.length === 64 && !this.value.includes('@') + } }