Browse Source

feat: refactor settings (#890)

* feat: refactor settings

* fix verify mfa specs and data source metadata

* fix: compound index field names

* fix metadata binding for typeorm repository

* fix responses to preserve e2e

* fixes for e2e

* fix recovery codes e2e

* add missing specs
Karol Sójko 1 year ago
parent
commit
1b5078eb96
100 changed files with 2104 additions and 4262 deletions
  1. 3 3
      packages/auth/bin/backup.ts
  2. 3 3
      packages/auth/bin/user_email_backup.ts
  3. 13 10
      packages/auth/migrations/mysql/1627638504691-move_mfa_items_to_user_settings.ts
  4. 395 114
      packages/auth/src/Bootstrap/Container.ts
  5. 4 4
      packages/auth/src/Bootstrap/DataSource.ts
  6. 15 9
      packages/auth/src/Bootstrap/Types.ts
  7. 0 5
      packages/auth/src/Domain/Feature/FeatureService.spec.ts
  8. 0 123
      packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.spec.ts
  9. 15 19
      packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.ts
  10. 0 89
      packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.spec.ts
  11. 21 18
      packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.ts
  12. 0 100
      packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.spec.ts
  13. 23 19
      packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.ts
  14. 0 48
      packages/auth/src/Domain/Handler/PaymentsAccountDeletedEventHandler.spec.ts
  15. 0 131
      packages/auth/src/Domain/Handler/PredicateVerificationRequestedEventHandler.spec.ts
  16. 0 45
      packages/auth/src/Domain/Handler/SharedSubscriptionInvitationCreatedEventHandler.spec.ts
  17. 0 63
      packages/auth/src/Domain/Handler/SubscriptionCancelledEventHandler.spec.ts
  18. 0 123
      packages/auth/src/Domain/Handler/SubscriptionExpiredEventHandler.spec.ts
  19. 0 177
      packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts
  20. 15 13
      packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts
  21. 0 161
      packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.spec.ts
  22. 24 22
      packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts
  23. 0 124
      packages/auth/src/Domain/Handler/SubscriptionRefundedEventHandler.spec.ts
  24. 0 149
      packages/auth/src/Domain/Handler/SubscriptionRenewedEventHandler.spec.ts
  25. 0 258
      packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.spec.ts
  26. 30 28
      packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts
  27. 0 37
      packages/auth/src/Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler.spec.ts
  28. 11 8
      packages/auth/src/Domain/Session/SessionService.spec.ts
  29. 20 23
      packages/auth/src/Domain/Session/SessionService.ts
  30. 0 7
      packages/auth/src/Domain/Setting/CreateOrReplaceSettingDto.ts
  31. 0 6
      packages/auth/src/Domain/Setting/CreateOrReplaceSettingResponse.ts
  32. 0 9
      packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingDTO.ts
  33. 0 6
      packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingResponse.ts
  34. 0 7
      packages/auth/src/Domain/Setting/FindSettingDTO.ts
  35. 0 8
      packages/auth/src/Domain/Setting/FindSubscriptionSettingDTO.ts
  36. 9 56
      packages/auth/src/Domain/Setting/Setting.ts
  37. 240 0
      packages/auth/src/Domain/Setting/SettingCrypter.spec.ts
  38. 60 0
      packages/auth/src/Domain/Setting/SettingCrypter.ts
  39. 10 0
      packages/auth/src/Domain/Setting/SettingCrypterInterface.ts
  40. 0 90
      packages/auth/src/Domain/Setting/SettingDecrypter.spec.ts
  41. 0 37
      packages/auth/src/Domain/Setting/SettingDecrypter.ts
  42. 0 6
      packages/auth/src/Domain/Setting/SettingDecrypterInterface.ts
  43. 0 4
      packages/auth/src/Domain/Setting/SettingDescription.ts
  44. 0 185
      packages/auth/src/Domain/Setting/SettingFactory.spec.ts
  45. 0 114
      packages/auth/src/Domain/Setting/SettingFactory.ts
  46. 0 19
      packages/auth/src/Domain/Setting/SettingFactoryInterface.ts
  47. 24 10
      packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts
  48. 2 2
      packages/auth/src/Domain/Setting/SettingInterpreter.ts
  49. 8 10
      packages/auth/src/Domain/Setting/SettingProps.ts
  50. 2 1
      packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts
  51. 0 202
      packages/auth/src/Domain/Setting/SettingService.spec.ts
  52. 0 110
      packages/auth/src/Domain/Setting/SettingService.ts
  53. 0 11
      packages/auth/src/Domain/Setting/SettingServiceInterface.ts
  54. 1 6
      packages/auth/src/Domain/Setting/SettingsAssociationService.ts
  55. 0 3
      packages/auth/src/Domain/Setting/SimpleSetting.ts
  56. 0 3
      packages/auth/src/Domain/Setting/SimpleSubscriptionSetting.ts
  57. 9 56
      packages/auth/src/Domain/Setting/SubscriptionSetting.ts
  58. 8 10
      packages/auth/src/Domain/Setting/SubscriptionSettingProps.ts
  59. 7 4
      packages/auth/src/Domain/Setting/SubscriptionSettingRepositoryInterface.ts
  60. 0 404
      packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts
  61. 0 192
      packages/auth/src/Domain/Setting/SubscriptionSettingService.ts
  62. 0 15
      packages/auth/src/Domain/Setting/SubscriptionSettingServiceInterface.ts
  63. 3 15
      packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.spec.ts
  64. 2 15
      packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.ts
  65. 1 12
      packages/auth/src/Domain/Subscription/UserSubscription.ts
  66. 1 0
      packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts
  67. 0 114
      packages/auth/src/Domain/Subscription/UserSubscriptionService.spec.ts
  68. 0 63
      packages/auth/src/Domain/Subscription/UserSubscriptionService.ts
  69. 0 6
      packages/auth/src/Domain/Subscription/UserSubscriptionServiceInterface.ts
  70. 19 17
      packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.spec.ts
  71. 16 12
      packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.ts
  72. 6 5
      packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.spec.ts
  73. 8 6
      packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts
  74. 68 0
      packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings.spec.ts
  75. 45 0
      packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings.ts
  76. 4 0
      packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettingsDTO.ts
  77. 195 0
      packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings.spec.ts
  78. 119 0
      packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings.ts
  79. 6 0
      packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettingsDTO.ts
  80. 35 10
      packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts
  81. 24 20
      packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts
  82. 37 27
      packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.spec.ts
  83. 34 31
      packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts
  84. 8 22
      packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts
  85. 12 12
      packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts
  86. 13 10
      packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.spec.ts
  87. 7 3
      packages/auth/src/Domain/UseCase/DeleteSetting/DeleteSetting.ts
  88. 51 32
      packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.spec.ts
  89. 29 27
      packages/auth/src/Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription.ts
  90. 18 6
      packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.spec.ts
  91. 10 11
      packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.ts
  92. 75 0
      packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser.spec.ts
  93. 72 0
      packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser.ts
  94. 3 0
      packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUserDTO.ts
  95. 46 0
      packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser.spec.ts
  96. 28 0
      packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser.ts
  97. 3 0
      packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUserDTO.ts
  98. 106 200
      packages/auth/src/Domain/UseCase/GetSetting/GetSetting.spec.ts
  99. 26 66
      packages/auth/src/Domain/UseCase/GetSetting/GetSetting.ts
  100. 2 1
      packages/auth/src/Domain/UseCase/GetSetting/GetSettingDto.ts

+ 3 - 3
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,
               ),

+ 3 - 3
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,
     ),

+ 13 - 10
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<void> {
@@ -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()

+ 395 - 114
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<MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>>(TYPES.Auth_SharedVaultUserPersistenceMapper)
       .toConstantValue(new SharedVaultUserPersistenceMapper())
+    container
+      .bind<MapperInterface<Setting, SettingHttpRepresentation>>(TYPES.Auth_SettingHttpMapper)
+      .toConstantValue(new SettingHttpMapper())
+    container
+      .bind<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
+        TYPES.Auth_SubscriptionSettingHttpMapper,
+      )
+      .toConstantValue(new SubscriptionSettingHttpMapper())
+    container
+      .bind<MapperInterface<Setting, TypeORMSetting>>(TYPES.Auth_SettingPersistenceMapper)
+      .toConstantValue(new SettingPersistenceMapper())
+    container
+      .bind<MapperInterface<SubscriptionSetting, TypeORMSubscriptionSetting>>(
+        TYPES.Auth_SubscriptionSettingPersistenceMapper,
+      )
+      .toConstantValue(new SubscriptionSettingPersistenceMapper())
 
     // ORM
     container
@@ -412,14 +436,14 @@ export class ContainerConfigLoader {
       .bind<Repository<Session>>(TYPES.Auth_ORMSessionRepository)
       .toConstantValue(appDataSource.getRepository(Session))
     container
-      .bind<Repository<Setting>>(TYPES.Auth_ORMSettingRepository)
-      .toConstantValue(appDataSource.getRepository(Setting))
+      .bind<Repository<TypeORMSetting>>(TYPES.Auth_ORMSettingRepository)
+      .toConstantValue(appDataSource.getRepository(TypeORMSetting))
     container
       .bind<Repository<SharedSubscriptionInvitation>>(TYPES.Auth_ORMSharedSubscriptionInvitationRepository)
       .toConstantValue(appDataSource.getRepository(SharedSubscriptionInvitation))
     container
-      .bind<Repository<SubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository)
-      .toConstantValue(appDataSource.getRepository(SubscriptionSetting))
+      .bind<Repository<TypeORMSubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository)
+      .toConstantValue(appDataSource.getRepository(TypeORMSubscriptionSetting))
     container.bind<Repository<User>>(TYPES.Auth_ORMUserRepository).toConstantValue(appDataSource.getRepository(User))
     container
       .bind<Repository<UserSubscription>>(TYPES.Auth_ORMUserSubscriptionRepository)
@@ -446,10 +470,24 @@ export class ContainerConfigLoader {
       .bind<RevokedSessionRepositoryInterface>(TYPES.Auth_RevokedSessionRepository)
       .to(TypeORMRevokedSessionRepository)
     container.bind<UserRepositoryInterface>(TYPES.Auth_UserRepository).to(TypeORMUserRepository)
-    container.bind<SettingRepositoryInterface>(TYPES.Auth_SettingRepository).to(TypeORMSettingRepository)
+    container
+      .bind<SettingRepositoryInterface>(TYPES.Auth_SettingRepository)
+      .toConstantValue(
+        new TypeORMSettingRepository(
+          container.get<Repository<TypeORMSetting>>(TYPES.Auth_ORMSettingRepository),
+          container.get<MapperInterface<Setting, TypeORMSetting>>(TYPES.Auth_SettingPersistenceMapper),
+        ),
+      )
     container
       .bind<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository)
-      .to(TypeORMSubscriptionSettingRepository)
+      .toConstantValue(
+        new TypeORMSubscriptionSettingRepository(
+          container.get<Repository<TypeORMSubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository),
+          container.get<MapperInterface<SubscriptionSetting, TypeORMSubscriptionSetting>>(
+            TYPES.Auth_SubscriptionSettingPersistenceMapper,
+          ),
+        ),
+      )
     container
       .bind<OfflineSettingRepositoryInterface>(TYPES.Auth_OfflineSettingRepository)
       .to(TypeORMOfflineSettingRepository)
@@ -512,13 +550,6 @@ export class ContainerConfigLoader {
     container.bind<UserProjector>(TYPES.Auth_UserProjector).to(UserProjector)
     container.bind<RoleProjector>(TYPES.Auth_RoleProjector).to(RoleProjector)
     container.bind<PermissionProjector>(TYPES.Auth_PermissionProjector).to(PermissionProjector)
-    container.bind<SettingProjector>(TYPES.Auth_SettingProjector).to(SettingProjector)
-    container
-      .bind<SubscriptionSettingProjector>(TYPES.Auth_SubscriptionSettingProjector)
-      .to(SubscriptionSettingProjector)
-
-    // Factories
-    container.bind<SettingFactoryInterface>(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<TraceSession>(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<SelectorInterface<ProtocolVersion>>(TYPES.Auth_ProtocolVersionSelector)
       .toConstantValue(new DeterministicSelector<ProtocolVersion>())
-    container.bind<UAParser>(TYPES.Auth_DeviceDetector).toConstantValue(new UAParser())
-    container.bind<SessionService>(TYPES.Auth_SessionService).to(SessionService)
+    container.bind<UAParserInstance>(TYPES.Auth_DeviceDetector).toConstantValue(new UAParser())
+    container.bind<CrypterInterface>(TYPES.Auth_Crypter).to(CrypterNode)
+    container
+      .bind<SettingCrypterInterface>(TYPES.Auth_SettingCrypter)
+      .toConstantValue(
+        new SettingCrypter(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<CrypterInterface>(TYPES.Auth_Crypter),
+        ),
+      )
+    container
+      .bind<GetSetting>(TYPES.Auth_GetSetting)
+      .toConstantValue(
+        new GetSetting(
+          container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
+          container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
+        ),
+      )
+    container
+      .bind<SessionService>(TYPES.Auth_SessionService)
+      .toConstantValue(
+        new SessionService(
+          container.get<SessionRepositoryInterface>(TYPES.Auth_SessionRepository),
+          container.get<EphemeralSessionRepositoryInterface>(TYPES.Auth_EphemeralSessionRepository),
+          container.get<RevokedSessionRepositoryInterface>(TYPES.Auth_RevokedSessionRepository),
+          container.get<UAParserInstance>(TYPES.Auth_DeviceDetector),
+          container.get<TimerInterface>(TYPES.Auth_Timer),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+          container.get<number>(TYPES.Auth_ACCESS_TOKEN_AGE),
+          container.get<number>(TYPES.Auth_REFRESH_TOKEN_AGE),
+          container.get<CryptoNode>(TYPES.Auth_CryptoNode),
+          container.get<TraceSession>(TYPES.Auth_TraceSession),
+          container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
+          container.get<string[]>(TYPES.Auth_READONLY_USERS),
+          container.get<GetSetting>(TYPES.Auth_GetSetting),
+        ),
+      )
     container.bind<AuthResponseFactory20161215>(TYPES.Auth_AuthResponseFactory20161215).to(AuthResponseFactory20161215)
     container.bind<AuthResponseFactory20190520>(TYPES.Auth_AuthResponseFactory20190520).to(AuthResponseFactory20190520)
     container.bind<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115).to(AuthResponseFactory20200115)
@@ -684,11 +758,9 @@ export class ContainerConfigLoader {
       .bind<AuthenticationMethodResolver>(TYPES.Auth_AuthenticationMethodResolver)
       .to(AuthenticationMethodResolver)
     container.bind<DomainEventFactory>(TYPES.Auth_DomainEventFactory).to(DomainEventFactory)
-    container.bind<CrypterInterface>(TYPES.Auth_Crypter).to(CrypterNode)
     container
       .bind<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService)
       .to(SettingsAssociationService)
-    container.bind<SettingDecrypterInterface>(TYPES.Auth_SettingDecrypter).to(SettingDecrypter)
 
     container
       .bind<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams)
@@ -711,21 +783,6 @@ export class ContainerConfigLoader {
         ),
       )
 
-    container
-      .bind<SettingServiceInterface>(TYPES.Auth_SettingService)
-      .toConstantValue(
-        new SettingService(
-          container.get<SettingFactoryInterface>(TYPES.Auth_SettingFactory),
-          container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
-          container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
-          container.get<SettingInterpreterInterface>(TYPES.Auth_SettingInterpreter),
-          container.get<SettingDecrypterInterface>(TYPES.Auth_SettingDecrypter),
-          container.get<winston.Logger>(TYPES.Auth_Logger),
-        ),
-      )
-    container
-      .bind<SubscriptionSettingServiceInterface>(TYPES.Auth_SubscriptionSettingService)
-      .to(SubscriptionSettingService)
     container.bind<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService).to(OfflineSettingService)
     container.bind<ContentDecoderInterface>(TYPES.Auth_ContenDecoder).toConstantValue(new ContentDecoder())
     container.bind<ClientServiceInterface>(TYPES.Auth_WebSocketsClientService).to(WebSocketsClientService)
@@ -738,7 +795,6 @@ export class ContainerConfigLoader {
     container
       .bind<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector)
       .toConstantValue(new DeterministicSelector<boolean>())
-    container.bind<UserSubscriptionServiceInterface>(TYPES.Auth_UserSubscriptionService).to(UserSubscriptionService)
 
     // Middleware
     container.bind<SessionMiddleware>(TYPES.Auth_SessionMiddleware).to(SessionMiddleware)
@@ -765,15 +821,6 @@ export class ContainerConfigLoader {
     container.bind<OfflineUserAuthMiddleware>(TYPES.Auth_OfflineUserAuthMiddleware).to(OfflineUserAuthMiddleware)
 
     // use cases
-    container
-      .bind<TraceSession>(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<PersistStatistics>(TYPES.Auth_PersistStatistics)
       .toConstantValue(
@@ -848,24 +895,65 @@ export class ContainerConfigLoader {
           container.get(TYPES.Auth_FeatureService),
         ),
       )
+    container
+      .bind<SetSettingValue>(TYPES.Auth_SetSettingValue)
+      .toConstantValue(
+        new SetSettingValue(
+          container.get<GetSetting>(TYPES.Auth_GetSetting),
+          container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
+          container.get<TimerInterface>(TYPES.Auth_Timer),
+          container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
+          container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
+          container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
+        ),
+      )
     container
       .bind<GenerateRecoveryCodes>(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<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting)
+      .toConstantValue(
+        new GetSubscriptionSetting(
+          container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
+          container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
+        ),
+      )
+    container
+      .bind<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue)
+      .toConstantValue(
+        new SetSubscriptionSettingValue(
+          container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
+          container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
+          container.get<TimerInterface>(TYPES.Auth_Timer),
+        ),
+      )
+    container
+      .bind<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings)
+      .toConstantValue(
+        new ApplyDefaultSubscriptionSettings(
+          container.get<SubscriptionSettingsAssociationServiceInterface>(
+            TYPES.Auth_SubscriptionSettingsAssociationService,
+          ),
+          container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
+          container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
+          container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
+        ),
+      )
     container
       .bind<ActivatePremiumFeatures>(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<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
+          container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
+          container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
+          container.get<TimerInterface>(TYPES.Auth_Timer),
         ),
       )
 
@@ -879,47 +967,136 @@ export class ContainerConfigLoader {
     container.bind<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest)
     container.bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken).to(RefreshSessionToken)
     container.bind<SignIn>(TYPES.Auth_SignIn).to(SignIn)
-    container.bind<VerifyMFA>(TYPES.Auth_VerifyMFA).to(VerifyMFA)
+    container
+      .bind<VerifyMFA>(TYPES.Auth_VerifyMFA)
+      .toConstantValue(
+        new VerifyMFA(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector),
+          container.get<LockRepositoryInterface>(TYPES.Auth_LockRepository),
+          container.get<string>(TYPES.Auth_PSEUDO_KEY_PARAMS_KEY),
+          container.get<AuthenticatorRepositoryInterface>(TYPES.Auth_AuthenticatorRepository),
+          container.get<VerifyAuthenticatorAuthenticationResponse>(
+            TYPES.Auth_VerifyAuthenticatorAuthenticationResponse,
+          ),
+          container.get<GetSetting>(TYPES.Auth_GetSetting),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
     container.bind<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts).to(ClearLoginAttempts)
     container.bind<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts).to(IncreaseLoginAttempts)
     container
       .bind<GetUserKeyParamsRecovery>(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<KeyParamsFactoryInterface>(TYPES.Auth_KeyParamsFactory),
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<PKCERepositoryInterface>(TYPES.Auth_PKCERepository),
+          container.get<GetSetting>(TYPES.Auth_GetSetting),
         ),
       )
     container.bind<UpdateUser>(TYPES.Auth_UpdateUser).to(UpdateUser)
-    container.bind<Register>(TYPES.Auth_Register).to(Register)
+    container
+      .bind<ApplyDefaultSettings>(TYPES.Auth_ApplyDefaultSettings)
+      .toConstantValue(
+        new ApplyDefaultSettings(
+          container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
+          container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
+        ),
+      )
+    container
+      .bind<Register>(TYPES.Auth_Register)
+      .toConstantValue(
+        new Register(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<RoleRepositoryInterface>(TYPES.Auth_RoleRepository),
+          container.get<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115),
+          container.get<CrypterInterface>(TYPES.Auth_Crypter),
+          container.get<boolean>(TYPES.Auth_DISABLE_USER_REGISTRATION),
+          container.get<TimerInterface>(TYPES.Auth_Timer),
+          container.get<ApplyDefaultSettings>(TYPES.Auth_ApplyDefaultSettings),
+        ),
+      )
     container.bind<GetActiveSessionsForUser>(TYPES.Auth_GetActiveSessionsForUser).to(GetActiveSessionsForUser)
     container.bind<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser).to(DeleteOtherSessionsForUser)
     container.bind<DeleteSessionForUser>(TYPES.Auth_DeleteSessionForUser).to(DeleteSessionForUser)
     container.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials).to(ChangeCredentials)
-    container.bind<GetSettings>(TYPES.Auth_GetSettings).to(GetSettings)
-    container.bind<GetSetting>(TYPES.Auth_GetSetting).to(GetSetting)
+    container
+      .bind<GetSettings>(TYPES.Auth_GetSettings)
+      .toConstantValue(
+        new GetSettings(
+          container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
+          container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
+        ),
+      )
+    container
+      .bind<GetSubscriptionSettings>(TYPES.Auth_GetSubscriptionSettings)
+      .toConstantValue(
+        new GetSubscriptionSettings(
+          container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
+          container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
+        ),
+      )
+    container
+      .bind<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser)
+      .toConstantValue(
+        new GetRegularSubscriptionForUser(
+          container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
+        ),
+      )
+    container
+      .bind<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser)
+      .toConstantValue(
+        new GetSharedSubscriptionForUser(
+          container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
+        ),
+      )
+    container
+      .bind<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser)
+      .toConstantValue(
+        new GetSharedOrRegularSubscriptionForUser(
+          container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
+          container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
+        ),
+      )
+    container
+      .bind<GetAllSettingsForUser>(TYPES.Auth_GetAllSettingsForUser)
+      .toConstantValue(
+        new GetAllSettingsForUser(
+          container.get<GetSettings>(TYPES.Auth_GetSettings),
+          container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
+          container.get<GetSubscriptionSettings>(TYPES.Auth_GetSubscriptionSettings),
+        ),
+      )
     container.bind<GetUserFeatures>(TYPES.Auth_GetUserFeatures).to(GetUserFeatures)
-    container.bind<UpdateSetting>(TYPES.Auth_UpdateSetting).to(UpdateSetting)
     container.bind<DeleteSetting>(TYPES.Auth_DeleteSetting).to(DeleteSetting)
     container
       .bind<SignInWithRecoveryCodes>(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<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115),
+          container.get<PKCERepositoryInterface>(TYPES.Auth_PKCERepository),
+          container.get<CrypterInterface>(TYPES.Auth_Crypter),
+          container.get<GetSetting>(TYPES.Auth_GetSetting),
+          container.get<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes),
+          container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
+          container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
+          container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
+          container.get<AuthenticatorRepositoryInterface>(TYPES.Auth_AuthenticatorRepository),
+        ),
+      )
+    container
+      .bind<DeleteAccount>(TYPES.Auth_DeleteAccount)
+      .toConstantValue(
+        new DeleteAccount(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
+          container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
+          container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
+          container.get<TimerInterface>(TYPES.Auth_Timer),
         ),
       )
-    container.bind<DeleteAccount>(TYPES.Auth_DeleteAccount).to(DeleteAccount)
     container.bind<GetUserSubscription>(TYPES.Auth_GetUserSubscription).to(GetUserSubscription)
     container.bind<GetUserOfflineSubscription>(TYPES.Auth_GetUserOfflineSubscription).to(GetUserOfflineSubscription)
     container.bind<CreateSubscriptionToken>(TYPES.Auth_CreateSubscriptionToken).to(CreateSubscriptionToken)
@@ -932,12 +1109,38 @@ export class ContainerConfigLoader {
     container
       .bind<CreateOfflineSubscriptionToken>(TYPES.Auth_CreateOfflineSubscriptionToken)
       .to(CreateOfflineSubscriptionToken)
-    container.bind<CreateValetToken>(TYPES.Auth_CreateValetToken).to(CreateValetToken)
+    container
+      .bind<CreateValetToken>(TYPES.Auth_CreateValetToken)
+      .toConstantValue(
+        new CreateValetToken(
+          container.get<TokenEncoderInterface<ValetTokenData>>(TYPES.Auth_ValetTokenEncoder),
+          container.get<SubscriptionSettingsAssociationServiceInterface>(
+            TYPES.Auth_SubscriptionSettingsAssociationService,
+          ),
+          container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
+          container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
+          container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
+          container.get<TimerInterface>(TYPES.Auth_Timer),
+          container.get<number>(TYPES.Auth_VALET_TOKEN_TTL),
+        ),
+      )
     container.bind<CreateListedAccount>(TYPES.Auth_CreateListedAccount).to(CreateListedAccount)
     container.bind<InviteToSharedSubscription>(TYPES.Auth_InviteToSharedSubscription).to(InviteToSharedSubscription)
     container
       .bind<AcceptSharedSubscriptionInvitation>(TYPES.Auth_AcceptSharedSubscriptionInvitation)
-      .to(AcceptSharedSubscriptionInvitation)
+      .toConstantValue(
+        new AcceptSharedSubscriptionInvitation(
+          container.get<SharedSubscriptionInvitationRepositoryInterface>(
+            TYPES.Auth_SharedSubscriptionInvitationRepository,
+          ),
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
+          container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
+          container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
+          container.get<TimerInterface>(TYPES.Auth_Timer),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
     container
       .bind<DeclineSharedSubscriptionInvitation>(TYPES.Auth_DeclineSharedSubscriptionInvitation)
       .to(DeclineSharedSubscriptionInvitation)
@@ -948,15 +1151,32 @@ export class ContainerConfigLoader {
       .bind<ListSharedSubscriptionInvitations>(TYPES.Auth_ListSharedSubscriptionInvitations)
       .to(ListSharedSubscriptionInvitations)
     container.bind<VerifyPredicate>(TYPES.Auth_VerifyPredicate).to(VerifyPredicate)
-    container.bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken).to(CreateCrossServiceToken)
+    container
+      .bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken)
+      .toConstantValue(
+        new CreateCrossServiceToken(
+          container.get<ProjectorInterface<User>>(TYPES.Auth_UserProjector),
+          container.get<ProjectorInterface<Session>>(TYPES.Auth_SessionProjector),
+          container.get<ProjectorInterface<Role>>(TYPES.Auth_RoleProjector),
+          container.get<TokenEncoderInterface<CrossServiceTokenData>>(TYPES.Auth_CrossServiceTokenEncoder),
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<number>(TYPES.Auth_AUTH_JWT_TTL),
+          container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
+          container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
+          container.get<SharedVaultUserRepositoryInterface>(TYPES.Auth_SharedVaultUserRepository),
+        ),
+      )
     container.bind<ProcessUserRequest>(TYPES.Auth_ProcessUserRequest).to(ProcessUserRequest)
     container
       .bind<UpdateStorageQuotaUsedForUser>(TYPES.Auth_UpdateStorageQuotaUsedForUser)
       .toConstantValue(
         new UpdateStorageQuotaUsedForUser(
-          container.get(TYPES.Auth_UserRepository),
-          container.get(TYPES.Auth_UserSubscriptionService),
-          container.get(TYPES.Auth_SubscriptionSettingService),
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
+          container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
+          container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
+          container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
         ),
       )
     container
@@ -984,8 +1204,9 @@ export class ContainerConfigLoader {
       .toConstantValue(
         new DisableEmailSettingBasedOnEmailSubscription(
           container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
-          container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
-          container.get<SettingFactoryInterface>(TYPES.Auth_SettingFactory),
+          container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
+          container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
+          container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
         ),
       )
 
@@ -1039,7 +1260,16 @@ export class ContainerConfigLoader {
       )
     container
       .bind<SubscriptionPurchasedEventHandler>(TYPES.Auth_SubscriptionPurchasedEventHandler)
-      .to(SubscriptionPurchasedEventHandler)
+      .toConstantValue(
+        new SubscriptionPurchasedEventHandler(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
+          container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
+          container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
+          container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
     container
       .bind<SubscriptionCancelledEventHandler>(TYPES.Auth_SubscriptionCancelledEventHandler)
       .to(SubscriptionCancelledEventHandler)
@@ -1054,13 +1284,42 @@ export class ContainerConfigLoader {
       .to(SubscriptionExpiredEventHandler)
     container
       .bind<SubscriptionSyncRequestedEventHandler>(TYPES.Auth_SubscriptionSyncRequestedEventHandler)
-      .to(SubscriptionSyncRequestedEventHandler)
+      .toConstantValue(
+        new SubscriptionSyncRequestedEventHandler(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
+          container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
+          container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
+          container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
+          container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
+          container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
+          container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
     container
       .bind<ExtensionKeyGrantedEventHandler>(TYPES.Auth_ExtensionKeyGrantedEventHandler)
-      .to(ExtensionKeyGrantedEventHandler)
+      .toConstantValue(
+        new ExtensionKeyGrantedEventHandler(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
+          container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
+          container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
     container
       .bind<SubscriptionReassignedEventHandler>(TYPES.Auth_SubscriptionReassignedEventHandler)
-      .to(SubscriptionReassignedEventHandler)
+      .toConstantValue(
+        new SubscriptionReassignedEventHandler(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
+          container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+          container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
+          container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
+        ),
+      )
     container
       .bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler)
       .toConstantValue(
@@ -1103,10 +1362,24 @@ export class ContainerConfigLoader {
       )
     container
       .bind<ListedAccountCreatedEventHandler>(TYPES.Auth_ListedAccountCreatedEventHandler)
-      .to(ListedAccountCreatedEventHandler)
+      .toConstantValue(
+        new ListedAccountCreatedEventHandler(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<GetSetting>(TYPES.Auth_GetSetting),
+          container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
     container
       .bind<ListedAccountDeletedEventHandler>(TYPES.Auth_ListedAccountDeletedEventHandler)
-      .to(ListedAccountDeletedEventHandler)
+      .toConstantValue(
+        new ListedAccountDeletedEventHandler(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<GetSetting>(TYPES.Auth_GetSetting),
+          container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
     container
       .bind<UserDisabledSessionUserAgentLoggingEventHandler>(TYPES.Auth_UserDisabledSessionUserAgentLoggingEventHandler)
       .to(UserDisabledSessionUserAgentLoggingEventHandler)
@@ -1322,33 +1595,41 @@ export class ContainerConfigLoader {
         .bind<BaseSubscriptionTokensController>(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<CreateSubscriptionToken>(TYPES.Auth_CreateSubscriptionToken),
+            container.get<AuthenticateSubscriptionToken>(TYPES.Auth_AuthenticateSubscriptionToken),
+            container.get<GetSetting>(TYPES.Auth_GetSetting),
+            container.get<ProjectorInterface<User>>(TYPES.Auth_UserProjector),
+            container.get<ProjectorInterface<Role>>(TYPES.Auth_RoleProjector),
+            container.get<TokenEncoderInterface<CrossServiceTokenData>>(TYPES.Auth_CrossServiceTokenEncoder),
+            container.get<number>(TYPES.Auth_AUTH_JWT_TTL),
+            container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
           ),
         )
       container
         .bind<BaseSubscriptionSettingsController>(TYPES.Auth_BaseSubscriptionSettingsController)
         .toConstantValue(
           new BaseSubscriptionSettingsController(
-            container.get(TYPES.Auth_GetSetting),
-            container.get(TYPES.Auth_ControllerContainer),
+            container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
+            container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
+            container.get<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
+              TYPES.Auth_SubscriptionSettingHttpMapper,
+            ),
+            container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
           ),
         )
       container
         .bind<BaseSettingsController>(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<GetAllSettingsForUser>(TYPES.Auth_GetAllSettingsForUser),
+            container.get<GetSetting>(TYPES.Auth_GetSetting),
+            container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
+            container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
+            container.get<MapperInterface<Setting, SettingHttpRepresentation>>(TYPES.Auth_SettingHttpMapper),
+            container.get<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
+              TYPES.Auth_SubscriptionSettingHttpMapper,
+            ),
+            container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
           ),
         )
       container

+ 4 - 4
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,

+ 15 - 9
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'),

+ 0 - 5
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 = {

+ 0 - 123
packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.spec.ts

@@ -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<User>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
-
-    settingService = {} as jest.Mocked<SettingServiceInterface>
-    settingService.createOrReplace = jest.fn()
-
-    offlineSettingService = {} as jest.Mocked<OfflineSettingServiceInterface>
-    offlineSettingService.createOrUpdate = jest.fn()
-
-    timestamp = dayjs.utc().valueOf()
-
-    event = {} as jest.Mocked<ExtensionKeyGrantedEvent>
-    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<ContentDecoderInterface>
-    contentDecoder.decode = jest.fn().mockReturnValue({
-      featuresUrl: 'http://features-url',
-      extensionKey: 'key',
-    })
-
-    logger = {} as jest.Mocked<Logger>
-    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()
-  })
-})

+ 15 - 19
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<void> {
@@ -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}`)
+    }
   }
 }

+ 0 - 89
packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.spec.ts

@@ -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<User>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
-
-    settingService = {} as jest.Mocked<SettingServiceInterface>
-    settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
-    settingService.createOrReplace = jest.fn()
-
-    event = {} as jest.Mocked<ListedAccountCreatedEvent>
-    event.payload = {
-      userEmail: 'test@test.com',
-      userId: 1,
-      userName: 'testuser',
-      secret: 'new-secret',
-      hostUrl: 'https://dev.listed.to',
-    }
-
-    logger = {} as jest.Mocked<Logger>
-    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<Setting>)
-
-    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"}]',
-      },
-    })
-  })
-})

+ 21 - 18
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<void> {
@@ -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}`)
+    }
   }
 }

+ 0 - 100
packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.spec.ts

@@ -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<User>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
-
-    settingService = {} as jest.Mocked<SettingServiceInterface>
-    settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
-      value: '[{"authorId":1,"secret":"my-secret","hostUrl":"https://dev.listed.to"}]',
-    } as jest.Mocked<Setting>)
-    settingService.createOrReplace = jest.fn()
-
-    event = {} as jest.Mocked<ListedAccountDeletedEvent>
-    event.payload = {
-      userEmail: 'test@test.com',
-      userId: 1,
-      userName: 'testuser',
-      secret: 'my-secret',
-      hostUrl: 'https://dev.listed.to',
-    }
-
-    logger = {} as jest.Mocked<Logger>
-    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<Setting>)
-
-    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"}]',
-      },
-    })
-  })
-})

+ 23 - 19
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<void> {
@@ -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}`)
+    }
   }
 }

+ 0 - 48
packages/auth/src/Domain/Handler/PaymentsAccountDeletedEventHandler.spec.ts

@@ -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<DeleteAccount>
-    deleteAccountUseCase.execute = jest.fn().mockResolvedValue(Result.ok('success'))
-
-    logger = {} as jest.Mocked<Logger>
-    logger.error = jest.fn()
-
-    event = {
-      payload: {
-        username: 'username',
-      },
-    } as jest.Mocked<PaymentsAccountDeletedEvent>
-  })
-
-  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')
-  })
-})

+ 0 - 131
packages/auth/src/Domain/Handler/PredicateVerificationRequestedEventHandler.spec.ts

@@ -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>
-    verifyPredicate.execute = jest
-      .fn()
-      .mockReturnValue({ predicateVerificationResult: PredicateVerificationResult.Affirmed })
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue({ uuid: '1-2-3' } as jest.Mocked<User>)
-
-    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
-    domainEventFactory.createPredicateVerifiedEvent = jest
-      .fn()
-      .mockReturnValue({} as jest.Mocked<PredicateVerifiedEvent>)
-
-    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
-    domainEventPublisher.publish = jest.fn()
-
-    logger = {} as jest.Mocked<Logger>
-    logger.warn = jest.fn()
-    logger.info = jest.fn()
-    logger.debug = jest.fn()
-
-    event = {} as jest.Mocked<PredicateVerificationRequestedEvent>
-    event.meta = {
-      correlation: {
-        userIdentifier: '2-3-4',
-        userIdentifierType: 'uuid',
-      },
-      origin: DomainEventService.Auth,
-    }
-    event.payload = {
-      predicate: {} as jest.Mocked<Predicate>,
-    } as jest.Mocked<PredicateVerificationRequestedEventPayload>
-  })
-
-  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()
-  })
-})

+ 0 - 45
packages/auth/src/Domain/Handler/SharedSubscriptionInvitationCreatedEventHandler.spec.ts

@@ -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>
-    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<SharedSubscriptionInvitationCreatedEvent>
-
-    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<SharedSubscriptionInvitationCreatedEvent>
-
-    await createHandler().handle(event)
-
-    expect(acceptSharedSubscriptionInvitation.execute).not.toHaveBeenCalled()
-  })
-})

+ 0 - 63
packages/auth/src/Domain/Handler/SubscriptionCancelledEventHandler.spec.ts

@@ -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<UserSubscriptionRepositoryInterface>
-    userSubscriptionRepository.updateCancelled = jest.fn()
-
-    offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
-    offlineUserSubscriptionRepository.updateCancelled = jest.fn()
-
-    timestamp = dayjs.utc().valueOf()
-
-    event = {} as jest.Mocked<SubscriptionCancelledEvent>
-    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)
-  })
-})

+ 0 - 123
packages/auth/src/Domain/Handler/SubscriptionExpiredEventHandler.spec.ts

@@ -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<User>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
-    userRepository.save = jest.fn().mockReturnValue(user)
-
-    userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
-    userSubscriptionRepository.updateEndsAt = jest.fn()
-    userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13)
-    userSubscriptionRepository.findBySubscriptionId = jest
-      .fn()
-      .mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked<UserSubscription>])
-
-    offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
-    offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
-
-    roleService = {} as jest.Mocked<RoleServiceInterface>
-    roleService.removeUserRoleBasedOnSubscription = jest.fn()
-
-    timestamp = dayjs.utc().valueOf()
-
-    event = {} as jest.Mocked<SubscriptionExpiredEvent>
-    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>
-    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()
-  })
-})

+ 0 - 177
packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts

@@ -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<User>
-    subscription = {
-      subscriptionType: UserSubscriptionType.Regular,
-    } as jest.Mocked<UserSubscription>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
-    userRepository.save = jest.fn().mockReturnValue(user)
-
-    userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
-    userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(0)
-    userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13)
-    userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
-
-    offlineUserSubscription = {} as jest.Mocked<OfflineUserSubscription>
-
-    offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
-    offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(offlineUserSubscription)
-    offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
-
-    roleService = {} as jest.Mocked<RoleServiceInterface>
-    roleService.addUserRoleBasedOnSubscription = jest.fn()
-    roleService.setOfflineUserRole = jest.fn()
-
-    subscriptionExpiresAt = timestamp + 365 * 1000
-
-    event = {} as jest.Mocked<SubscriptionPurchasedEvent>
-    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<SubscriptionSettingServiceInterface>
-    subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
-
-    logger = {} as jest.Mocked<Logger>
-    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()
-  })
-})

+ 15 - 13
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<void> {
@@ -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<void> {

+ 0 - 161
packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.spec.ts

@@ -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<User>
-    subscription = {
-      subscriptionType: UserSubscriptionType.Regular,
-    } as jest.Mocked<UserSubscription>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
-    userRepository.save = jest.fn().mockReturnValue(user)
-
-    userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
-    userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
-
-    roleService = {} as jest.Mocked<RoleServiceInterface>
-    roleService.addUserRoleBasedOnSubscription = jest.fn()
-
-    subscriptionExpiresAt = timestamp + 365 * 1000
-
-    event = {} as jest.Mocked<SubscriptionReassignedEvent>
-    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<SettingServiceInterface>
-    settingService.createOrReplace = jest.fn()
-
-    subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
-    subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
-
-    logger = {} as jest.Mocked<Logger>
-    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()
-  })
-})

+ 24 - 22
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<void> {
@@ -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<void> {

+ 0 - 124
packages/auth/src/Domain/Handler/SubscriptionRefundedEventHandler.spec.ts

@@ -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<User>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
-    userRepository.save = jest.fn().mockReturnValue(user)
-
-    userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
-    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<UserSubscription>])
-
-    offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
-    offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
-
-    roleService = {} as jest.Mocked<RoleServiceInterface>
-    roleService.removeUserRoleBasedOnSubscription = jest.fn()
-
-    timestamp = dayjs.utc().valueOf()
-
-    event = {} as jest.Mocked<SubscriptionRefundedEvent>
-    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>
-    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()
-  })
-})

+ 0 - 149
packages/auth/src/Domain/Handler/SubscriptionRenewedEventHandler.spec.ts

@@ -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<User>
-    subscription = {} as jest.Mocked<UserSubscription>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
-    userRepository.save = jest.fn().mockReturnValue(user)
-
-    userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
-    userSubscriptionRepository.updateEndsAt = jest.fn()
-    userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
-    userSubscriptionRepository.findBySubscriptionId = jest
-      .fn()
-      .mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked<UserSubscription>])
-
-    offlineUserSubscription = {} as jest.Mocked<OfflineUserSubscription>
-
-    offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
-    offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(offlineUserSubscription)
-    offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
-
-    roleService = {} as jest.Mocked<RoleServiceInterface>
-    roleService.addUserRoleBasedOnSubscription = jest.fn()
-    roleService.setOfflineUserRole = jest.fn()
-
-    timestamp = dayjs.utc().valueOf()
-    subscriptionExpiresAt = dayjs.utc().valueOf() + 365 * 1000
-
-    event = {} as jest.Mocked<SubscriptionRenewedEvent>
-    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>
-    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()
-  })
-})

+ 0 - 258
packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.spec.ts

@@ -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<User>
-    subscription = {
-      subscriptionType: UserSubscriptionType.Regular,
-    } as jest.Mocked<UserSubscription>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
-    userRepository.save = jest.fn().mockReturnValue(user)
-
-    userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
-    userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
-    userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([])
-
-    offlineUserSubscription = {} as jest.Mocked<OfflineUserSubscription>
-
-    offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
-    offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(null)
-    offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
-
-    offlineSettingService = {} as jest.Mocked<OfflineSettingServiceInterface>
-    offlineSettingService.createOrUpdate = jest.fn()
-
-    contentDecoder = {} as jest.Mocked<ContentDecoderInterface>
-    contentDecoder.decode = jest.fn().mockReturnValue({
-      featuresUrl: 'http://features-url',
-      extensionKey: 'key',
-    })
-
-    roleService = {} as jest.Mocked<RoleServiceInterface>
-    roleService.addUserRoleBasedOnSubscription = jest.fn()
-    roleService.setOfflineUserRole = jest.fn()
-
-    subscriptionExpiresAt = timestamp + 365 * 1000
-
-    event = {} as jest.Mocked<SubscriptionSyncRequestedEvent>
-    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<SettingServiceInterface>
-    settingService.createOrReplace = jest.fn()
-
-    subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
-    subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
-
-    logger = {} as jest.Mocked<Logger>
-    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<UserSubscription>])
-    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<OfflineUserSubscription>)
-    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()
-  })
-})

+ 30 - 28
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<void> {
@@ -95,17 +88,26 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
 
     await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
 
-    await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
+    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()}`,
+      )
+    }
 
-    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.subscriptionName,
     })
+
+    if (result.isFailed()) {
+      this.logger.error(`Could not set extension key for user ${user.uuid}`)
+    }
   }
 
   private async createOrUpdateSubscription(

+ 0 - 37
packages/auth/src/Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler.spec.ts

@@ -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<SessionRepositoryInterface>
-    sessionRepository.clearUserAgentByUserUuid = jest.fn()
-
-    revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
-    revokedSessionRepository.clearUserAgentByUserUuid = jest.fn()
-
-    event = {} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>
-    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')
-  })
-})

+ 11 - 8
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<SettingServiceInterface>
-    settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
+    getSetting = {} as jest.Mocked<GetSetting>
+    getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found'))
 
     ephemeralSessionRepository = {} as jest.Mocked<EphemeralSessionRepositoryInterface>
     ephemeralSessionRepository.insert = jest.fn()
@@ -240,9 +240,12 @@ describe('SessionService', () => {
     const user = {} as jest.Mocked<User>
     user.uuid = '123'
 
-    settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
-      value: LogSessionUserAgentOption.Disabled,
-    } as jest.Mocked<Setting>)
+    getSetting.execute = jest.fn().mockReturnValue(
+      Result.ok({
+        setting: {} as jest.Mocked<Setting>,
+        decryptedValue: LogSessionUserAgentOption.Disabled,
+      }),
+    )
 
     const result = await createService().createNewSessionForUser({
       user,

+ 20 - 23
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<boolean> {
-    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
   }
 }

+ 0 - 7
packages/auth/src/Domain/Setting/CreateOrReplaceSettingDto.ts

@@ -1,7 +0,0 @@
-import { User } from '../User/User'
-import { SettingProps } from './SettingProps'
-
-export type CreateOrReplaceSettingDto = {
-  user: User
-  props: SettingProps
-}

+ 0 - 6
packages/auth/src/Domain/Setting/CreateOrReplaceSettingResponse.ts

@@ -1,6 +0,0 @@
-import { Setting } from './Setting'
-
-export type CreateOrReplaceSettingResponse = {
-  status: 'created' | 'replaced'
-  setting: Setting
-}

+ 0 - 9
packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingDTO.ts

@@ -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
-}

+ 0 - 6
packages/auth/src/Domain/Setting/CreateOrReplaceSubscriptionSettingResponse.ts

@@ -1,6 +0,0 @@
-import { SubscriptionSetting } from './SubscriptionSetting'
-
-export type CreateOrReplaceSubscriptionSettingResponse = {
-  status: 'created' | 'replaced'
-  subscriptionSetting: SubscriptionSetting
-}

+ 0 - 7
packages/auth/src/Domain/Setting/FindSettingDTO.ts

@@ -1,7 +0,0 @@
-import { SettingName } from '@standardnotes/settings'
-
-export type FindSettingDTO = {
-  userUuid: string
-  settingName: SettingName
-  settingUuid?: string
-}

+ 0 - 8
packages/auth/src/Domain/Setting/FindSubscriptionSettingDTO.ts

@@ -1,8 +0,0 @@
-import { SettingName } from '@standardnotes/settings'
-
-export type FindSubscriptionSettingDTO = {
-  userUuid: string
-  userSubscriptionUuid: string
-  subscriptionSettingName: SettingName
-  settingUuid?: string
-}

+ 9 - 56
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<SettingProps> {
+  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<User>
-
-  @Column({
-    type: 'tinyint',
-    width: 1,
-    nullable: false,
-    default: 0,
-  })
-  declare sensitive: boolean
+  static create(props: SettingProps, id?: UniqueEntityId): Result<Setting> {
+    return Result.ok<Setting>(new Setting(props, id))
+  }
 }

+ 240 - 0
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<CrypterInterface>
+    crypter.decryptForUser = jest.fn().mockReturnValue('decrypted')
+
+    user = {
+      uuid: '4-5-6',
+    } as jest.Mocked<User>
+
+    userRepository = {} as jest.Mocked<UserRepositoryInterface>
+    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()
+    })
+  })
+})

+ 60 - 0
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<string | null> {
+    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<string | null> {
+    return this.decrypt(setting.props.value, setting.props.serverEncryptionVersion, userUuidString)
+  }
+
+  async decryptSubscriptionSettingValue(setting: SubscriptionSetting, userUuidString: string): Promise<string | null> {
+    return this.decrypt(setting.props.value, setting.props.serverEncryptionVersion, userUuidString)
+  }
+
+  private async decrypt(
+    value: string | null,
+    serverEncryptionVersion: number,
+    userUuidString: string,
+  ): Promise<string | null> {
+    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
+  }
+}

+ 10 - 0
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<string | null>
+  decryptSettingValue(value: Setting, userUuid: string): Promise<string | null>
+  decryptSubscriptionSettingValue(setting: SubscriptionSetting, userUuid: string): Promise<string | null>
+}

+ 0 - 90
packages/auth/src/Domain/Setting/SettingDecrypter.spec.ts

@@ -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<CrypterInterface>
-    crypter.decryptForUser = jest.fn().mockReturnValue('decrypted')
-
-    user = {
-      uuid: '4-5-6',
-    } as jest.Mocked<User>
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    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<Setting>
-
-    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<Setting>
-
-    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<Setting>
-
-    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<Setting>
-    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<Setting>
-
-    let caughtError = null
-    try {
-      await createDecrypter().decryptSettingValue(setting, 'invalid')
-    } catch (error) {
-      caughtError = error
-    }
-
-    expect(caughtError).not.toBeNull()
-  })
-})

+ 0 - 37
packages/auth/src/Domain/Setting/SettingDecrypter.ts

@@ -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<string | null> {
-    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
-  }
-}

+ 0 - 6
packages/auth/src/Domain/Setting/SettingDecrypterInterface.ts

@@ -1,6 +0,0 @@
-import { Setting } from './Setting'
-import { SubscriptionSetting } from './SubscriptionSetting'
-
-export interface SettingDecrypterInterface {
-  decryptSettingValue(setting: Setting | SubscriptionSetting, userUuid: string): Promise<string | null>
-}

+ 0 - 4
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
 }

+ 0 - 185
packages/auth/src/Domain/Setting/SettingFactory.spec.ts

@@ -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<CrypterInterface>
-    crypter.encryptForUser = jest.fn().mockReturnValue('encrypted')
-
-    timer = {} as jest.Mocked<TimerInterface>
-    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
-
-    user = {} as jest.Mocked<User>
-
-    userSubscription = {
-      user: Promise.resolve(user),
-    } as jest.Mocked<UserSubscription>
-  })
-
-  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<SubscriptionSetting>
-    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<Setting>
-    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()
-  })
-})

+ 0 - 114
packages/auth/src/Domain/Setting/SettingFactory.ts

@@ -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<SubscriptionSetting> {
-    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<SubscriptionSetting> {
-    const { uuid, userSubscription } = original
-
-    return Object.assign(await this.createSubscriptionSetting(props, await userSubscription), {
-      uuid,
-    })
-  }
-
-  async create(props: SettingProps, user: User): Promise<Setting> {
-    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<Setting> {
-    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<string | null> {
-    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}!`)
-    }
-  }
-}

+ 0 - 19
packages/auth/src/Domain/Setting/SettingFactoryInterface.ts

@@ -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<Setting>
-  createSubscriptionSetting(
-    props: SubscriptionSettingProps,
-    userSubscription: UserSubscription,
-  ): Promise<SubscriptionSetting>
-  createReplacement(original: Setting, props: SettingProps): Promise<Setting>
-  createSubscriptionSettingReplacement(
-    original: SubscriptionSetting,
-    props: SubscriptionSettingProps,
-  ): Promise<SubscriptionSetting>
-}

+ 24 - 10
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<SettingDecrypterInterface>
-    settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
+    settingCrypter = {} as jest.Mocked<SettingCrypterInterface>
+    settingCrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
 
     domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
     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<Setting>)
+    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 () => {

+ 2 - 2
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({

+ 8 - 10
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
 }

+ 2 - 1
packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts

@@ -13,5 +13,6 @@ export interface SettingRepositoryInterface {
   streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream>
   streamAllByName(name: SettingName): Promise<ReadStream>
   deleteByUserUuid(dto: DeleteSettingDto): Promise<void>
-  save(setting: Setting): Promise<Setting>
+  insert(setting: Setting): Promise<void>
+  update(setting: Setting): Promise<void>
 }

+ 0 - 202
packages/auth/src/Domain/Setting/SettingService.spec.ts

@@ -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>
-    user.isPotentiallyAPrivateUsernameAccount = jest.fn().mockReturnValue(false)
-
-    setting = {
-      name: SettingName.NAMES.DropboxBackupToken,
-    } as jest.Mocked<Setting>
-
-    factory = {} as jest.Mocked<SettingFactoryInterface>
-    factory.create = jest.fn().mockReturnValue(setting)
-    factory.createReplacement = jest.fn().mockReturnValue(setting)
-
-    settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
-    settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null)
-    settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
-    settingRepository.save = jest.fn().mockImplementation((setting) => setting)
-
-    settingsAssociationService = {} as jest.Mocked<SettingsAssociationServiceInterface>
-    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<SettingInterpreterInterface>
-    settingInterpreter.interpretSettingUpdated = jest.fn()
-
-    settingDecrypter = {} as jest.Mocked<SettingDecrypterInterface>
-    settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
-
-    logger = {} as jest.Mocked<Logger>
-    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<Setting>
-
-    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',
-    })
-  })
-})

+ 0 - 110
packages/auth/src/Domain/Setting/SettingService.ts

@@ -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<void> {
-    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<Setting | null> {
-    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<CreateOrReplaceSettingResponse> {
-    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,
-    }
-  }
-}

+ 0 - 11
packages/auth/src/Domain/Setting/SettingServiceInterface.ts

@@ -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<void>
-  createOrReplace(dto: CreateOrReplaceSettingDto): Promise<CreateOrReplaceSettingResponse>
-  findSettingWithDecryptedValue(dto: FindSettingDTO): Promise<Setting | null>
-}

+ 1 - 6
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,
       },

+ 0 - 3
packages/auth/src/Domain/Setting/SimpleSetting.ts

@@ -1,3 +0,0 @@
-import { Setting } from './Setting'
-
-export type SimpleSetting = Omit<Setting, 'user' | 'serverEncryptionVersion'>

+ 0 - 3
packages/auth/src/Domain/Setting/SimpleSubscriptionSetting.ts

@@ -1,3 +0,0 @@
-import { SubscriptionSetting } from './SubscriptionSetting'
-
-export type SimpleSubscriptionSetting = Omit<SubscriptionSetting, 'userSubscription' | 'serverEncryptionVersion'>

+ 9 - 56
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<SubscriptionSettingProps> {
+  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<UserSubscription>
-
-  @Column({
-    type: 'tinyint',
-    width: 1,
-    nullable: false,
-    default: 0,
-  })
-  declare sensitive: boolean
+  static create(props: SubscriptionSettingProps, id?: UniqueEntityId): Result<SubscriptionSetting> {
+    return Result.ok<SubscriptionSetting>(new SubscriptionSetting(props, id))
+  }
 }

+ 8 - 10
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
 }

+ 7 - 4
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<SubscriptionSetting | null>
-  findLastByNameAndUserSubscriptionUuid(name: string, userSubscriptionUuid: string): Promise<SubscriptionSetting | null>
-  findAllBySubscriptionUuid(userSubscriptionUuid: string): Promise<SubscriptionSetting[]>
-  save(subscriptionSetting: SubscriptionSetting): Promise<SubscriptionSetting>
+  findOneByUuid(uuid: Uuid): Promise<SubscriptionSetting | null>
+  findLastByNameAndUserSubscriptionUuid(name: string, userSubscriptionUuid: Uuid): Promise<SubscriptionSetting | null>
+  findAllBySubscriptionUuid(userSubscriptionUuid: Uuid): Promise<SubscriptionSetting[]>
+  insert(subscriptionSetting: SubscriptionSetting): Promise<void>
+  update(subscriptionSetting: SubscriptionSetting): Promise<void>
 }

+ 0 - 404
packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts

@@ -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<User>
-
-    userSubscription = {
-      uuid: '1-2-3',
-      user: Promise.resolve(user),
-      planName: SubscriptionName.PlusPlan,
-    } as jest.Mocked<UserSubscription>
-
-    setting = {
-      name: SettingName.NAMES.FileUploadBytesUsed,
-    } as jest.Mocked<SubscriptionSetting>
-
-    factory = {} as jest.Mocked<SettingFactoryInterface>
-    factory.createSubscriptionSetting = jest.fn().mockReturnValue(setting)
-    factory.createSubscriptionSettingReplacement = jest.fn().mockReturnValue(setting)
-
-    subscriptionSettingRepository = {} as jest.Mocked<SubscriptionSettingRepositoryInterface>
-    subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null)
-    subscriptionSettingRepository.save = jest.fn().mockImplementation((setting) => setting)
-
-    userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
-    userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([
-      {
-        uuid: 's-1-2-3',
-      } as jest.Mocked<UserSubscription>,
-      {
-        uuid: 's-2-3-4',
-      } as jest.Mocked<UserSubscription>,
-    ])
-
-    subscriptionSettingsAssociationService = {} as jest.Mocked<SubscriptionSettingsAssociationServiceInterface>
-    subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
-      new Map([
-        [
-          SettingName.NAMES.FileUploadBytesUsed,
-          {
-            value: '0',
-            sensitive: 0,
-            serverEncryptionVersion: EncryptionVersion.Unencrypted,
-            replaceable: true,
-          },
-        ],
-      ]),
-    )
-
-    settingInterpreter = {} as jest.Mocked<SettingInterpreterInterface>
-    settingInterpreter.interpretSettingUpdated = jest.fn()
-
-    settingDecrypter = {} as jest.Mocked<SettingDecrypterInterface>
-    settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
-
-    logger = {} as jest.Mocked<Logger>
-    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<UserSubscription>,
-    ])
-
-    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<SubscriptionSetting>
-
-    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()
-  })
-})

+ 0 - 192
packages/auth/src/Domain/Setting/SubscriptionSettingService.ts

@@ -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<string, string>,
-  ): Promise<void> {
-    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<SubscriptionSetting | null> {
-    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<CreateOrReplaceSubscriptionSettingResponse> {
-    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<SubscriptionSetting | null> {
-    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,
-    )
-  }
-}

+ 0 - 15
packages/auth/src/Domain/Setting/SubscriptionSettingServiceInterface.ts

@@ -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<string, string>,
-  ): Promise<void>
-  createOrReplace(dto: CreateOrReplaceSubscriptionSettingDTO): Promise<CreateOrReplaceSubscriptionSettingResponse>
-  findSubscriptionSettingWithDecryptedValue(dto: FindSubscriptionSettingDTO): Promise<SubscriptionSetting | null>
-}

+ 3 - 15
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<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
-      ).keys(),
-    ]
+    const flatSettings = [...(settings as Map<string, SettingDescription>).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<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
-      ).keys(),
-    ]
+    const flatSettings = [...(settings as Map<string, SettingDescription>).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,
     })

+ 2 - 15
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,
     })

+ 1 - 12
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<User>
-
-  @OneToMany(
-    /* istanbul ignore next */
-    () => SubscriptionSetting,
-    /* istanbul ignore next */
-    (subscriptionSetting) => subscriptionSetting.userSubscription,
-    /* istanbul ignore next */
-    { lazy: true, eager: false },
-  )
-  declare subscriptionSettings: Promise<SubscriptionSetting[]>
 }

+ 1 - 0
packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts

@@ -5,6 +5,7 @@ export interface UserSubscriptionRepositoryInterface {
   findOneByUuid(uuid: string): Promise<UserSubscription | null>
   countByUserUuid(userUuid: string): Promise<number>
   findOneByUserUuid(userUuid: string): Promise<UserSubscription | null>
+  findOneByUserUuidAndType(userUuid: string, type: UserSubscriptionType): Promise<UserSubscription | null>
   findByUserUuid(userUuid: string): Promise<UserSubscription[]>
   findOneByUserUuidAndSubscriptionId(userUuid: string, subscriptionId: number): Promise<UserSubscription | null>
   findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise<UserSubscription[]>

+ 0 - 114
packages/auth/src/Domain/Subscription/UserSubscriptionService.spec.ts

@@ -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<User>
-
-    regularSubscription = {
-      uuid: '1-2-3',
-      subscriptionType: UserSubscriptionType.Regular,
-      user: Promise.resolve(user),
-    } as jest.Mocked<UserSubscription>
-    sharedSubscription = {
-      uuid: '2-3-4',
-      subscriptionType: UserSubscriptionType.Shared,
-      user: Promise.resolve(user),
-    } as jest.Mocked<UserSubscription>
-
-    userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
-    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,
-      })
-    })
-  })
-})

+ 0 - 63
packages/auth/src/Domain/Subscription/UserSubscriptionService.ts

@@ -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<FindRegularSubscriptionResponse> {
-    const userSubscription = await this.userSubscriptionRepository.findOneByUserUuid(userUuid)
-
-    return this.findRegularSubscription(userSubscription)
-  }
-
-  async findRegularSubscriptionForUuid(uuid: string): Promise<FindRegularSubscriptionResponse> {
-    const userSubscription = await this.userSubscriptionRepository.findOneByUuid(uuid)
-
-    return this.findRegularSubscription(userSubscription)
-  }
-
-  private async findRegularSubscription(
-    userSubscription: UserSubscription | null,
-  ): Promise<FindRegularSubscriptionResponse> {
-    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,
-    }
-  }
-}

+ 0 - 6
packages/auth/src/Domain/Subscription/UserSubscriptionServiceInterface.ts

@@ -1,6 +0,0 @@
-import { FindRegularSubscriptionResponse } from './FindRegularSubscriptionResponse'
-
-export interface UserSubscriptionServiceInterface {
-  findRegularSubscriptionForUuid(uuid: string): Promise<FindRegularSubscriptionResponse>
-  findRegularSubscriptionForUserUuid(userUuid: string): Promise<FindRegularSubscriptionResponse>
-}

+ 19 - 17
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<RoleServiceInterface>
     roleService.addUserRoleBasedOnSubscription = jest.fn()
 
-    subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
-    subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
+    applyDefaultSubscriptionSettings = {} as jest.Mocked<ApplyDefaultSubscriptionSettings>
+    applyDefaultSubscriptionSettings.execute = jest.fn().mockReturnValue(Result.ok())
 
     timer = {} as jest.Mocked<TimerInterface>
     timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
+
+    logger = {} as jest.Mocked<Logger>
+    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()
   })
 })

+ 16 - 12
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<AcceptSharedSubscriptionInvitationResponse> {
@@ -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,

+ 6 - 5
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<SubscriptionSettingServiceInterface>
-    subscriptionSettingsService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
+    applyDefaultSubscriptionSettings = {} as jest.Mocked<ApplyDefaultSubscriptionSettings>
+    applyDefaultSubscriptionSettings.execute = jest.fn().mockReturnValue(Result.ok())
   })
 
   it('should return error when username is invalid', async () => {

+ 8 - 6
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<string> {
   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<string> {
 
     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.')
   }

+ 68 - 0
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<SettingsAssociationServiceInterface>
+    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>
+    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()
+  })
+})

+ 45 - 0
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<void> {
+  constructor(
+    private settingsAssociationService: SettingsAssociationServiceInterface,
+    private setSettingValue: SetSettingValue,
+  ) {}
+
+  async execute(dto: ApplyDefaultSettingsDTO): Promise<Result<void>> {
+    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()
+  }
+}

+ 4 - 0
packages/auth/src/Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettingsDTO.ts

@@ -0,0 +1,4 @@
+export interface ApplyDefaultSettingsDTO {
+  userUuid: string
+  userName: string
+}

+ 195 - 0
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<SubscriptionSettingsAssociationServiceInterface>
+    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<UserSubscriptionRepositoryInterface>
+    userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([
+      {
+        uuid: '1-2-3',
+      } as jest.Mocked<UserSubscription>,
+    ])
+
+    getSubscriptionSetting = {} as jest.Mocked<GetSubscriptionSetting>
+    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>
+    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)
+  })
+})

+ 119 - 0
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<void> {
+  constructor(
+    private subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface,
+    private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
+    private getSubscriptionSetting: GetSubscriptionSetting,
+    private setSubscriptionSettingValue: SetSubscriptionSettingValue,
+  ) {}
+
+  async execute(dto: ApplyDefaultSubscriptionSettingsDTO): Promise<Result<void>> {
+    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<Result<{ setting: SubscriptionSetting; decryptedValue?: string | null }>> {
+    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,
+      },
+    })
+  }
+}

+ 6 - 0
packages/auth/src/Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettingsDTO.ts

@@ -0,0 +1,6 @@
+export interface ApplyDefaultSubscriptionSettingsDTO {
+  userUuid: string
+  userSubscriptionUuid: string
+  subscriptionPlanName: string
+  overrides?: Map<string, string>
+}

+ 35 - 10
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<User>
@@ -18,7 +23,8 @@ describe('CreateCrossServiceToken', () => {
   let roleProjector: ProjectorInterface<Role>
   let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
   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<UserRepositoryInterface>
     userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
 
-    getSettingUseCase = {} as jest.Mocked<GetSetting>
-    getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } }))
+    getSubscriptionSetting = {} as jest.Mocked<GetSubscriptionSetting>
+    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<GetRegularSubscriptionForUser>
+    getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
 
     sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
     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<UserSubscription>
+      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<UserSubscription>
+      getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription))
+
+      getSubscriptionSetting.execute = jest.fn().mockReturnValue(Result.fail('error'))
 
       const result = await createUseCase().execute({
         user,

+ 24 - 20
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<string> {
   constructor(
-    @inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>,
-    @inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface<Session>,
-    @inject(TYPES.Auth_RoleProjector) private roleProjector: ProjectorInterface<Role>,
-    @inject(TYPES.Auth_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
-    @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
-    @inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
-    @inject(TYPES.Auth_GetSetting)
-    private getSettingUseCase: GetSetting,
-    @inject(TYPES.Auth_SharedVaultUserRepository) private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
+    private userProjector: ProjectorInterface<User>,
+    private sessionProjector: ProjectorInterface<Session>,
+    private roleProjector: ProjectorInterface<Role>,
+    private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
+    private userRepository: UserRepositoryInterface,
+    private jwtTTL: number,
+    private getRegularSubscription: GetRegularSubscriptionForUser,
+    private getSubscriptionSettingUseCase: GetSubscriptionSetting,
+    private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
   ) {}
 
   async execute(dto: CreateCrossServiceTokenDTO): Promise<Result<string>> {
@@ -61,18 +59,24 @@ export class CreateCrossServiceToken implements UseCaseInterface<string> {
     }
 
     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,

+ 37 - 27
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<ValetTokenData>
-  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<TokenEncoderInterface<ValetTokenData>>
     tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
 
-    subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
-    subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
-      value: '123',
-    })
+    getSubscriptionSetting = {} as jest.Mocked<GetSubscriptionSetting>
+    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<SubscriptionSettingsAssociationServiceInterface>
     subscriptionSettingsAssociationService.getFileUploadLimit = jest.fn().mockReturnValue(5_368_709_120)
@@ -60,10 +76,11 @@ describe('CreateValetToken', () => {
       user: Promise.resolve(user),
     } as jest.Mocked<UserSubscription>
 
-    userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
-    userSubscriptionService.findRegularSubscriptionForUserUuid = jest
-      .fn()
-      .mockReturnValue({ regularSubscription, sharedSubscription: null })
+    getRegularSubscription = {} as jest.Mocked<GetRegularSubscriptionForUser>
+    getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription))
+
+    getSharedSubscription = {} as jest.Mocked<GetSharedSubscriptionForUser>
+    getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
 
     timer = {} as jest.Mocked<TimerInterface>
     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,

+ 34 - 31
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<ValetTokenData>,
-    @inject(TYPES.Auth_SubscriptionSettingService)
-    private subscriptionSettingService: SubscriptionSettingServiceInterface,
-    @inject(TYPES.Auth_SubscriptionSettingsAssociationService)
+    private tokenEncoder: TokenEncoderInterface<ValetTokenData>,
     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<CreateValetTokenResponseData> {
     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
     }
 

+ 8 - 22
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<UserSubscriptionServiceInterface>
-    userSubscriptionService.findRegularSubscriptionForUserUuid = jest
-      .fn()
-      .mockReturnValue({ regularSubscription, sharedSubscription: null })
+    getRegularSubscription = {} as jest.Mocked<GetRegularSubscriptionForUser>
+    getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription))
 
     domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
     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()

+ 12 - 12
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<string> {
   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<Result<string>> {
@@ -49,9 +46,12 @@ export class DeleteAccount implements UseCaseInterface<string> {
 
     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
     }
 

+ 13 - 10
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 = 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<SettingRepositoryInterface>
     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<TimerInterface>
     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()
   })
 })

+ 7 - 3
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,

+ 51 - 32
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<User>
@@ -22,52 +31,66 @@ describe('DisableEmailSettingBasedOnEmailSubscription', () => {
     userRepository = {} as jest.Mocked<UserRepositoryInterface>
     userRepository.findOneByUsernameOrEmail = jest.fn().mockResolvedValue(user)
 
-    settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
-    settingRepository.findLastByNameAndUserUuid = jest.fn().mockResolvedValue({} as jest.Mocked<Setting>)
-    settingRepository.save = jest.fn()
+    setSettingValue = {} as jest.Mocked<SetSettingValue>
+    setSettingValue.execute = jest.fn().mockReturnValue(Result.ok())
+
+    setSubscriptionSetting = {} as jest.Mocked<SetSubscriptionSettingValue>
+    setSubscriptionSetting.execute = jest.fn().mockResolvedValue(Result.ok())
+
+    regularSubscription = {} as jest.Mocked<UserSubscription>
 
-    factory = {} as jest.Mocked<SettingFactoryInterface>
-    factory.create = jest.fn().mockResolvedValue({} as jest.Mocked<Setting>)
-    factory.createReplacement = jest.fn().mockResolvedValue({} as jest.Mocked<Setting>)
+    getSharedOrRegularSubscriptionForUser = {} as jest.Mocked<GetSharedOrRegularSubscriptionForUser>
+    getSharedOrRegularSubscriptionForUser.execute = jest.fn().mockResolvedValue(Result.ok(regularSubscription))
   })
 
-  it('should fail if the username is empty', async () => {
+  it('should set the setting value when muting non subscription setting value', async () => {
     const useCase = createUseCase()
 
     const result = await useCase.execute({
-      userEmail: '',
+      userEmail: 'test@test.te',
       level: EmailLevel.LEVELS.Marketing,
     })
 
-    expect(result.isFailed()).toBeTruthy()
+    expect(result.isFailed()).toBeFalsy()
   })
 
-  it('should fail if the user is not found', async () => {
-    userRepository.findOneByUsernameOrEmail = jest.fn().mockResolvedValue(null)
+  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.Marketing,
+      level: EmailLevel.LEVELS.SignIn,
     })
 
     expect(result.isFailed()).toBeTruthy()
   })
 
-  it('should fail if the setting name cannot be determined', async () => {
+  it('should fail if the username is empty', async () => {
     const useCase = createUseCase()
 
     const result = await useCase.execute({
-      userEmail: 'test@test.te',
-      level: 'invalid',
+      userEmail: '',
+      level: EmailLevel.LEVELS.Marketing,
     })
 
     expect(result.isFailed()).toBeTruthy()
   })
 
-  it('should create a new setting if it does not exist', async () => {
-    settingRepository.findLastByNameAndUserUuid = jest.fn().mockResolvedValue(null)
+  it('should fail if the user is not found', async () => {
+    userRepository.findOneByUsernameOrEmail = jest.fn().mockResolvedValue(null)
 
     const useCase = createUseCase()
 
@@ -76,21 +99,17 @@ describe('DisableEmailSettingBasedOnEmailSubscription', () => {
       level: EmailLevel.LEVELS.Marketing,
     })
 
-    expect(result.isFailed()).toBeFalsy()
-    expect(factory.create).toHaveBeenCalled()
-    expect(factory.createReplacement).not.toHaveBeenCalled()
+    expect(result.isFailed()).toBeTruthy()
   })
 
-  it('should replace the setting if it exists', async () => {
+  it('should fail if the setting name cannot be determined', async () => {
     const useCase = createUseCase()
 
     const result = await useCase.execute({
       userEmail: 'test@test.te',
-      level: EmailLevel.LEVELS.Marketing,
+      level: 'invalid',
     })
 
-    expect(result.isFailed()).toBeFalsy()
-    expect(factory.create).not.toHaveBeenCalled()
-    expect(factory.createReplacement).toHaveBeenCalled()
+    expect(result.isFailed()).toBeTruthy()
   })
 })

+ 29 - 27
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<void> {
   constructor(
     private userRepository: UserRepositoryInterface,
-    private settingRepository: SettingRepositoryInterface,
-    private factory: SettingFactoryInterface,
+    private setSettingValue: SetSettingValue,
+    private setSubscriptionSetting: SetSubscriptionSettingValue,
+    private getSharedOrRegularSubscriptionForUser: GetSharedOrRegularSubscriptionForUser,
   ) {}
 
   async execute(dto: DisableEmailSettingBasedOnEmailSubscriptionDTO): Promise<Result<void>> {
@@ -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<string> {
+  private getSettingNameFromLevel(level: string): Result<SettingName> {
     /* 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}`)
     }

+ 18 - 6
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<UserRepositoryInterface>
     userRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked<User>)
 
-    settingService = {} as jest.Mocked<SettingServiceInterface>
-    settingService.createOrReplace = jest.fn()
+    setSettingValue = {} as jest.Mocked<SetSettingValue>
+    setSettingValue.execute = jest.fn().mockReturnValue(Result.ok())
 
     cryptoNode = {} as jest.Mocked<CryptoNode>
     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('')
 

+ 10 - 11
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<string> {
   constructor(
     private userRepository: UserRepositoryInterface,
-    private settingService: SettingServiceInterface,
+    private setSettingValue: SetSettingValue,
     private cryptoNode: CryptoNode,
   ) {}
+
   async execute(dto: GenerateRecoveryCodesDTO): Promise<Result<string>> {
     const userUuidOrError = Uuid.create(dto.userUuid)
     if (userUuidOrError.isFailed()) {
@@ -32,15 +32,14 @@ export class GenerateRecoveryCodes implements UseCaseInterface<string> {
 
     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)
   }

+ 75 - 0
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>
+    getSettings.execute = jest.fn().mockReturnValue(Result.ok([]))
+
+    getSharedOrRegularSubscription = {} as jest.Mocked<GetSharedOrRegularSubscriptionForUser>
+    getSharedOrRegularSubscription.execute = jest
+      .fn()
+      .mockReturnValue(Result.ok({ uuid: '00000000-0000-0000-0000-000000000000' }))
+
+    getSubscriptionSettings = {} as jest.Mocked<GetSubscriptionSettings>
+    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()
+  })
+})

+ 72 - 0
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,
+    })
+  }
+}

+ 3 - 0
packages/auth/src/Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUserDTO.ts

@@ -0,0 +1,3 @@
+export type GetAllSettingsForUserDTO = {
+  userUuid: string
+}

+ 46 - 0
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<UserSubscription>
+
+    userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
+    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)
+  })
+})

+ 28 - 0
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<UserSubscription> {
+  constructor(private userSubscriptionRepository: UserSubscriptionRepositoryInterface) {}
+
+  async execute(dto: GetRegularSubscriptionForUserDTO): Promise<Result<UserSubscription>> {
+    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)
+  }
+}

+ 3 - 0
packages/auth/src/Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUserDTO.ts

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

+ 106 - 200
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
-
-  const createUseCase = () =>
-    new GetSetting(
-      settingProjector,
-      subscriptionSettingProjector,
-      settingService,
-      subscriptionSettingService,
-      userSubscriptionService,
-    )
+  let settingRepository: SettingRepositoryInterface
+  let settingCrypter: SettingCrypterInterface
 
-  beforeEach(() => {
-    setting = {} as jest.Mocked<Setting>
+  const createUseCase = () => new GetSetting(settingRepository, settingCrypter)
 
-    subscriptionSetting = {
+  beforeEach(() => {
+    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<SubscriptionSetting>
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+    settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
+    settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting)
 
-    settingService = {} as jest.Mocked<SettingServiceInterface>
-    settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
-
-    settingProjector = {} as jest.Mocked<SettingProjector>
-    settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' })
-
-    subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
-    subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
-      .fn()
-      .mockReturnValue(subscriptionSetting)
-
-    regularSubscription = {
-      uuid: '1-2-3',
-      subscriptionType: UserSubscriptionType.Regular,
-    } as jest.Mocked<UserSubscription>
-
-    sharedSubscription = {
-      uuid: '2-3-4',
-      subscriptionType: UserSubscriptionType.Shared,
-    } as jest.Mocked<UserSubscription>
-
-    userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
-    userSubscriptionService.findRegularSubscriptionForUserUuid = jest
-      .fn()
-      .mockReturnValue({ regularSubscription: null, sharedSubscription: null })
-
-    subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
-    subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
+    settingCrypter = {} as jest.Mocked<SettingCrypterInterface>
+    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)
+    expect(result.isFailed()).toBeFalsy()
+  })
 
-      const result = await createUseCase().execute({
-        userUuid: '1-2-3',
-        settingName: SettingName.NAMES.DropboxBackupFrequency,
-      })
-      expect(result.isFailed()).toBeTruthy()
+  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 not retrieve a sensitive setting for user', async () => {
-      setting = {
-        sensitive: true,
-        name: SettingName.NAMES.MfaSecret,
-      } as jest.Mocked<Setting>
-
-      settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
+    expect(result.isFailed()).toBeTruthy()
+  })
 
-      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 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 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()
-    })
+    expect(result.isFailed()).toBeFalsy()
+    expect(result.getValue().decryptedValue).toEqual('decrypted')
+  })
 
-    it('should retrieve a sensitive setting for user if explicitly told to', async () => {
-      setting = {
-        sensitive: true,
-        name: SettingName.NAMES.MfaSecret,
-      } as jest.Mocked<Setting>
-
-      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' },
-      })
+  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<SettingRepositoryInterface>
+    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,
     })
+
+    expect(result.isFailed()).toBeTruthy()
   })
 
-  describe('regular subscription', () => {
-    beforeEach(() => {
-      userSubscriptionService.findRegularSubscriptionForUserUuid = jest
-        .fn()
-        .mockReturnValue({ regularSubscription, sharedSubscription: null })
+  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<SettingRepositoryInterface>
+    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,
     })
 
-    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()
+  })
+
+  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,
     })
 
-    it('should not get a suscription setting for user if it does not exist', async () => {
-      subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
+    expect(result.isFailed()).toBeTruthy()
+  })
 
-      const result = await createUseCase().execute({
-        userUuid: '1-2-3',
-        settingName: SettingName.NAMES.MuteSignInEmails,
-      })
-      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,
     })
 
-    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 fail if the setting is not found', async () => {
+    settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null)
 
-    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(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({
-        subscriptionSettingName: SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue(),
-        userSubscriptionUuid: '2-3-4',
-        userUuid: '1-2-3',
-      })
+    const result = await createUseCase().execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      settingName: SettingName.NAMES.ExtensionKey,
+      allowSensitiveRetrieval: false,
+      decrypted: false,
     })
 
-    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(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({
-        subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
-        userSubscriptionUuid: '1-2-3',
-        userUuid: '1-2-3',
-      })
-    })
+    expect(result.isFailed()).toBeTruthy()
   })
 })

+ 26 - 66
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<GetSettingResponse> {
+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<Result<GetSettingResponse>> {
+  async execute(dto: GetSettingDto): Promise<Result<{ setting: Setting; decryptedValue?: string | null }>> {
+    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<GetSettingResponse> {
     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,
     })
   }
 }

+ 2 - 1
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
 }

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