浏览代码

feat(auth): add triggering post setting update actions (#905)

* feat(auth): add triggering post setting update actions

* feat(auth): refactor email backups

* fix: add extra logs for backups

* fix: specs
Karol Sójko 1 年之前
父节点
当前提交
d228a86f48
共有 21 个文件被更改,包括 550 次插入415 次删除
  1. 11 84
      packages/auth/bin/backup.ts
  2. 2 17
      packages/auth/docker/entrypoint.sh
  3. 2 5
      packages/auth/package.json
  4. 34 12
      packages/auth/src/Bootstrap/Container.ts
  5. 3 1
      packages/auth/src/Bootstrap/Types.ts
  6. 0 153
      packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts
  7. 0 113
      packages/auth/src/Domain/Setting/SettingInterpreter.ts
  8. 0 5
      packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts
  9. 2 3
      packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts
  10. 47 0
      packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.spec.ts
  11. 55 0
      packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.ts
  12. 3 0
      packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsersDTO.ts
  13. 78 0
      packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.spec.ts
  14. 66 0
      packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.ts
  15. 3 0
      packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUserDTO.ts
  16. 104 0
      packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.spec.ts
  17. 83 0
      packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.ts
  18. 6 0
      packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActionsDTO.ts
  19. 7 0
      packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.ts
  20. 14 0
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts
  21. 30 22
      packages/auth/src/Infra/TypeORM/TypeORMSettingRepository.ts

+ 11 - 84
packages/auth/bin/backup.ts

@@ -1,9 +1,5 @@
 import 'reflect-metadata'
 
-import { SettingName } from '@standardnotes/domain-core'
-
-import { Stream } from 'stream'
-
 import { Logger } from 'winston'
 import * as dayjs from 'dayjs'
 import * as utc from 'dayjs/plugin/utc'
@@ -11,78 +7,13 @@ import * as utc from 'dayjs/plugin/utc'
 import { ContainerConfigLoader } from '../src/Bootstrap/Container'
 import TYPES from '../src/Bootstrap/Types'
 import { Env } from '../src/Bootstrap/Env'
-import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
-import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
-import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
-import { MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
-import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
-import { PermissionName } from '@standardnotes/features'
-import { GetUserKeyParams } from '../src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
+import { TriggerEmailBackupForAllUsers } from '../src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers'
 
 const inputArgs = process.argv.slice(2)
-const backupProvider = inputArgs[0]
-const backupFrequency = inputArgs[1]
-
-const requestBackups = async (
-  settingRepository: SettingRepositoryInterface,
-  roleService: RoleServiceInterface,
-  domainEventFactory: DomainEventFactoryInterface,
-  domainEventPublisher: DomainEventPublisherInterface,
-  getUserKeyParamsUseCase: GetUserKeyParams,
-): Promise<void> => {
-  const settingName = SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue()
-  const permissionName = PermissionName.DailyEmailBackup
-  const muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
-  const muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted
-
-  const stream = await settingRepository.streamAllByNameAndValue(settingName, backupFrequency)
-
-  return new Promise((resolve, reject) => {
-    stream
-      .pipe(
-        new Stream.Transform({
-          objectMode: true,
-          transform: async (setting, _encoding, callback) => {
-            const userIsPermittedForEmailBackups = await roleService.userHasPermission(
-              setting.setting_user_uuid,
-              permissionName,
-            )
-            if (!userIsPermittedForEmailBackups) {
-              callback()
-
-              return
-            }
+const backupFrequency = inputArgs[0]
 
-            let userHasEmailsMuted = false
-            const emailsMutedSetting = await settingRepository.findOneByNameAndUserUuid(
-              muteEmailsSettingName,
-              setting.setting_user_uuid,
-            )
-            if (emailsMutedSetting !== null && emailsMutedSetting.props.value !== null) {
-              userHasEmailsMuted = emailsMutedSetting.props.value === muteEmailsSettingValue
-            }
-
-            const keyParamsResponse = await getUserKeyParamsUseCase.execute({
-              userUuid: setting.setting_user_uuid,
-              authenticated: false,
-            })
-
-            await domainEventPublisher.publish(
-              domainEventFactory.createEmailBackupRequestedEvent(
-                setting.setting_user_uuid,
-                emailsMutedSetting?.id.toString() as string,
-                userHasEmailsMuted,
-                keyParamsResponse.keyParams,
-              ),
-            )
-
-            callback()
-          },
-        }),
-      )
-      .on('finish', resolve)
-      .on('error', reject)
-  })
+const requestBackups = async (triggerEmailBackupForAllUsers: TriggerEmailBackupForAllUsers): Promise<void> => {
+  await triggerEmailBackupForAllUsers.execute({ backupFrequency })
 }
 
 const container = new ContainerConfigLoader('worker')
@@ -94,24 +25,20 @@ void container.load().then((container) => {
 
   const logger: Logger = container.get(TYPES.Auth_Logger)
 
-  logger.info(`Starting ${backupFrequency} ${backupProvider} backup requesting...`)
+  logger.info(`Starting ${backupFrequency} email backup requesting...`)
 
-  const settingRepository: SettingRepositoryInterface = container.get(TYPES.Auth_SettingRepository)
-  const roleService: RoleServiceInterface = container.get(TYPES.Auth_RoleService)
-  const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.Auth_DomainEventFactory)
-  const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher)
-  const getUserKeyParamsUseCase: GetUserKeyParams = container.get(TYPES.Auth_GetUserKeyParams)
-
-  Promise.resolve(
-    requestBackups(settingRepository, roleService, domainEventFactory, domainEventPublisher, getUserKeyParamsUseCase),
+  const triggerEmailBackupForAllUsers: TriggerEmailBackupForAllUsers = container.get(
+    TYPES.Auth_TriggerEmailBackupForAllUsers,
   )
+
+  Promise.resolve(requestBackups(triggerEmailBackupForAllUsers))
     .then(() => {
-      logger.info(`${backupFrequency} ${backupProvider} backup requesting complete`)
+      logger.info(`${backupFrequency} email backup requesting complete`)
 
       process.exit(0)
     })
     .catch((error) => {
-      logger.error(`Could not finish ${backupFrequency} ${backupProvider} backup requesting: ${error.message}`)
+      logger.error(`Could not finish ${backupFrequency} email backup requesting: ${error.message}`)
 
       process.exit(1)
     })

+ 2 - 17
packages/auth/docker/entrypoint.sh

@@ -26,12 +26,12 @@ case "$COMMAND" in
 
   'email-daily-backup' )
     echo "[Docker] Starting Email Daily Backup..."
-    node docker/entrypoint-backup.js email daily
+    node docker/entrypoint-backup.js daily
     ;;
 
   'email-weekly-backup' )
     echo "[Docker] Starting Email Weekly Backup..."
-    node docker/entrypoint-backup.js email weekly
+    node docker/entrypoint-backup.js weekly
     ;;
 
   'email-backup' )
@@ -40,21 +40,6 @@ case "$COMMAND" in
     node docker/entrypoint-user-email-backup.js $EMAIL
     ;;
 
-  'dropbox-daily-backup' )
-    echo "[Docker] Starting Dropbox Daily Backup..."
-    node docker/entrypoint-backup.js dropbox daily
-    ;;
-
-  'google-drive-daily-backup' )
-    echo "[Docker] Starting Google Drive Daily Backup..."
-    node docker/entrypoint-backup.js google_drive daily
-    ;;
-
-  'one-drive-daily-backup' )
-    echo "[Docker] Starting One Drive Daily Backup..."
-    node docker/entrypoint-backup.js one_drive daily
-    ;;
-
    * )
     echo "[Docker] Unknown command"
     ;;

+ 2 - 5
packages/auth/package.json

@@ -24,12 +24,9 @@
     "worker": "yarn node dist/bin/worker.js",
     "cleanup": "yarn node dist/bin/cleanup.js",
     "stats": "yarn node dist/bin/stats.js",
-    "daily-backup:email": "yarn node dist/bin/backup.js email daily",
+    "daily-backup:email": "yarn node dist/bin/backup.js daily",
     "user-email-backup": "yarn node dist/bin/user_email_backup.js",
-    "daily-backup:dropbox": "yarn node dist/bin/backup.js dropbox daily",
-    "daily-backup:google_drive": "yarn node dist/bin/backup.js google_drive daily",
-    "daily-backup:one_drive": "yarn node dist/bin/backup.js one_drive daily",
-    "weekly-backup:email": "yarn node dist/bin/backup.js email weekly",
+    "weekly-backup:email": "yarn node dist/bin/backup.js weekly",
     "content-recalculation": "yarn node dist/bin/content.js",
     "typeorm": "typeorm-ts-node-commonjs",
     "migrate": "yarn build && yarn typeorm migration:run -d dist/src/Bootstrap/DataSource.js"

+ 34 - 12
packages/auth/src/Bootstrap/Container.ts

@@ -130,8 +130,6 @@ import { ListedAccountCreatedEventHandler } from '../Domain/Handler/ListedAccoun
 import { ListedAccountDeletedEventHandler } from '../Domain/Handler/ListedAccountDeletedEventHandler'
 import { FileRemovedEventHandler } from '../Domain/Handler/FileRemovedEventHandler'
 import { UserDisabledSessionUserAgentLoggingEventHandler } from '../Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler'
-import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface'
-import { SettingInterpreter } from '../Domain/Setting/SettingInterpreter'
 import { SettingCrypterInterface } from '../Domain/Setting/SettingCrypterInterface'
 import { SettingCrypter } from '../Domain/Setting/SettingCrypter'
 import { SharedSubscriptionInvitationRepositoryInterface } from '../Domain/SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
@@ -275,6 +273,9 @@ import { SubscriptionSettingPersistenceMapper } from '../Mapping/Persistence/Sub
 import { ApplyDefaultSettings } from '../Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings'
 import { AuthResponseFactoryResolverInterface } from '../Domain/Auth/AuthResponseFactoryResolverInterface'
 import { UserInvitedToSharedVaultEventHandler } from '../Domain/Handler/UserInvitedToSharedVaultEventHandler'
+import { TriggerPostSettingUpdateActions } from '../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions'
+import { TriggerEmailBackupForUser } from '../Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser'
+import { TriggerEmailBackupForAllUsers } from '../Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers'
 
 export class ContainerConfigLoader {
   constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -772,16 +773,6 @@ export class ContainerConfigLoader {
           container.get<winston.Logger>(TYPES.Auth_Logger),
         ),
       )
-    container
-      .bind<SettingInterpreterInterface>(TYPES.Auth_SettingInterpreter)
-      .toConstantValue(
-        new SettingInterpreter(
-          container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
-          container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
-          container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
-          container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
-        ),
-      )
 
     container.bind<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService).to(OfflineSettingService)
     container.bind<ContentDecoderInterface>(TYPES.Auth_ContenDecoder).toConstantValue(new ContentDecoder())
@@ -1231,6 +1222,35 @@ export class ContainerConfigLoader {
           container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
         ),
       )
+    container
+      .bind<TriggerEmailBackupForUser>(TYPES.Auth_TriggerEmailBackupForUser)
+      .toConstantValue(
+        new TriggerEmailBackupForUser(
+          container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
+          container.get<GetSetting>(TYPES.Auth_GetSetting),
+          container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
+          container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
+          container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
+        ),
+      )
+    container
+      .bind<TriggerEmailBackupForAllUsers>(TYPES.Auth_TriggerEmailBackupForAllUsers)
+      .toConstantValue(
+        new TriggerEmailBackupForAllUsers(
+          container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
+          container.get<TriggerEmailBackupForUser>(TYPES.Auth_TriggerEmailBackupForUser),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
+    container
+      .bind<TriggerPostSettingUpdateActions>(TYPES.Auth_TriggerPostSettingUpdateActions)
+      .toConstantValue(
+        new TriggerPostSettingUpdateActions(
+          container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
+          container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
+          container.get<TriggerEmailBackupForUser>(TYPES.Auth_TriggerEmailBackupForUser),
+        ),
+      )
 
     // Controller
     container
@@ -1655,11 +1675,13 @@ export class ContainerConfigLoader {
             container.get<GetAllSettingsForUser>(TYPES.Auth_GetAllSettingsForUser),
             container.get<GetSetting>(TYPES.Auth_GetSetting),
             container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
+            container.get<TriggerPostSettingUpdateActions>(TYPES.Auth_TriggerPostSettingUpdateActions),
             container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
             container.get<MapperInterface<Setting, SettingHttpRepresentation>>(TYPES.Auth_SettingHttpMapper),
             container.get<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
               TYPES.Auth_SubscriptionSettingHttpMapper,
             ),
+            container.get<winston.Logger>(TYPES.Auth_Logger),
             container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
           ),
         )

+ 3 - 1
packages/auth/src/Bootstrap/Types.ts

@@ -164,6 +164,9 @@ const TYPES = {
   Auth_DesignateSurvivor: Symbol.for('Auth_DesignateSurvivor'),
   Auth_GetSharedOrRegularSubscriptionForUser: Symbol.for('Auth_GetSharedOrRegularSubscriptionForUser'),
   Auth_DisableEmailSettingBasedOnEmailSubscription: Symbol.for('Auth_DisableEmailSettingBasedOnEmailSubscription'),
+  Auth_TriggerPostSettingUpdateActions: Symbol.for('Auth_TriggerPostSettingUpdateActions'),
+  Auth_TriggerEmailBackupForUser: Symbol.for('Auth_TriggerEmailBackupForUser'),
+  Auth_TriggerEmailBackupForAllUsers: Symbol.for('Auth_TriggerEmailBackupForAllUsers'),
   // Handlers
   Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
   Auth_SubscriptionPurchasedEventHandler: Symbol.for('Auth_SubscriptionPurchasedEventHandler'),
@@ -230,7 +233,6 @@ const TYPES = {
   Auth_SubscriptionSettingsAssociationService: Symbol.for('Auth_SubscriptionSettingsAssociationService'),
   Auth_FeatureService: Symbol.for('Auth_FeatureService'),
   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_BaseAuthController: Symbol.for('Auth_BaseAuthController'),

+ 0 - 153
packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts

@@ -1,153 +0,0 @@
-import {
-  DomainEventPublisherInterface,
-  EmailBackupRequestedEvent,
-  MuteEmailsSettingChangedEvent,
-  UserDisabledSessionUserAgentLoggingEvent,
-} from '@standardnotes/domain-events'
-import { EmailBackupFrequency, LogSessionUserAgentOption, MuteMarketingEmailsOption } from '@standardnotes/settings'
-import 'reflect-metadata'
-import { Logger } from 'winston'
-import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
-import { User } from '../User/User'
-import { Setting } from './Setting'
-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, SettingName } from '@standardnotes/domain-core'
-
-describe('SettingInterpreter', () => {
-  let user: User
-  let domainEventPublisher: DomainEventPublisherInterface
-  let domainEventFactory: DomainEventFactoryInterface
-  let settingRepository: SettingRepositoryInterface
-  let settingCrypter: SettingCrypterInterface
-  let logger: Logger
-  let getUserKeyParams: GetUserKeyParams
-
-  const createInterpreter = () =>
-    new SettingInterpreter(domainEventPublisher, domainEventFactory, settingRepository, getUserKeyParams)
-
-  beforeEach(() => {
-    user = {
-      uuid: '4-5-6',
-      email: 'test@test.te',
-    } as jest.Mocked<User>
-
-    settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
-    settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null)
-    settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
-
-    settingCrypter = {} as jest.Mocked<SettingCrypterInterface>
-    settingCrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
-
-    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
-    domainEventPublisher.publish = jest.fn()
-
-    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
-    domainEventFactory.createEmailBackupRequestedEvent = jest
-      .fn()
-      .mockReturnValue({} as jest.Mocked<EmailBackupRequestedEvent>)
-    domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent = jest
-      .fn()
-      .mockReturnValue({} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>)
-    domainEventFactory.createMuteEmailsSettingChangedEvent = jest
-      .fn()
-      .mockReturnValue({} as jest.Mocked<MuteEmailsSettingChangedEvent>)
-
-    logger = {} as jest.Mocked<Logger>
-    logger.debug = jest.fn()
-    logger.warn = jest.fn()
-    logger.error = jest.fn()
-
-    getUserKeyParams = {} as jest.Mocked<GetUserKeyParams>
-    getUserKeyParams.execute = jest.fn().mockReturnValue({ keyParams: {} as jest.Mocked<KeyParamsData> })
-  })
-
-  it('should trigger session cleanup if user is disabling session user agent logging', async () => {
-    await createInterpreter().interpretSettingUpdated(
-      SettingName.NAMES.LogSessionUserAgent,
-      user,
-      LogSessionUserAgentOption.Disabled,
-    )
-
-    expect(domainEventPublisher.publish).toHaveBeenCalled()
-    expect(domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent).toHaveBeenCalledWith({
-      userUuid: '4-5-6',
-      email: 'test@test.te',
-    })
-  })
-
-  it('should trigger backup if email backup setting is created - emails not muted', async () => {
-    await createInterpreter().interpretSettingUpdated(
-      SettingName.NAMES.EmailBackupFrequency,
-      user,
-      EmailBackupFrequency.Daily,
-    )
-
-    expect(domainEventPublisher.publish).toHaveBeenCalled()
-    expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '', false, {})
-  })
-
-  it('should trigger backup if email backup setting is created - emails muted', async () => {
-    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,
-      user,
-      EmailBackupFrequency.Daily,
-    )
-
-    expect(domainEventPublisher.publish).toHaveBeenCalled()
-    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 () => {
-    settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
-
-    await createInterpreter().interpretSettingUpdated(
-      SettingName.NAMES.EmailBackupFrequency,
-      user,
-      EmailBackupFrequency.Disabled,
-    )
-
-    expect(domainEventPublisher.publish).not.toHaveBeenCalled()
-    expect(domainEventFactory.createEmailBackupRequestedEvent).not.toHaveBeenCalled()
-  })
-
-  it('should trigger mute subscription emails rejection if mute setting changed', async () => {
-    settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
-
-    await createInterpreter().interpretSettingUpdated(
-      SettingName.NAMES.MuteMarketingEmails,
-      user,
-      MuteMarketingEmailsOption.Muted,
-    )
-
-    expect(domainEventPublisher.publish).toHaveBeenCalled()
-    expect(domainEventFactory.createMuteEmailsSettingChangedEvent).toHaveBeenCalledWith({
-      emailSubscriptionRejectionLevel: 'MARKETING',
-      mute: true,
-      username: 'test@test.te',
-    })
-  })
-})

+ 0 - 113
packages/auth/src/Domain/Setting/SettingInterpreter.ts

@@ -1,113 +0,0 @@
-import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
-import { EmailLevel, SettingName } from '@standardnotes/domain-core'
-import { EmailBackupFrequency, LogSessionUserAgentOption, MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
-
-import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
-import { User } from '../User/User'
-import { SettingInterpreterInterface } from './SettingInterpreterInterface'
-import { SettingRepositoryInterface } from './SettingRepositoryInterface'
-import { GetUserKeyParams } from '../UseCase/GetUserKeyParams/GetUserKeyParams'
-
-export class SettingInterpreter implements SettingInterpreterInterface {
-  private readonly emailSettingToSubscriptionRejectionLevelMap: Map<string, string> = new Map([
-    [SettingName.NAMES.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
-    [SettingName.NAMES.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup],
-    [SettingName.NAMES.MuteMarketingEmails, EmailLevel.LEVELS.Marketing],
-    [SettingName.NAMES.MuteSignInEmails, EmailLevel.LEVELS.SignIn],
-  ])
-
-  constructor(
-    private domainEventPublisher: DomainEventPublisherInterface,
-    private domainEventFactory: DomainEventFactoryInterface,
-    private settingRepository: SettingRepositoryInterface,
-    private getUserKeyParams: GetUserKeyParams,
-  ) {}
-
-  async interpretSettingUpdated(
-    updatedSettingName: string,
-    user: User,
-    unencryptedValue: string | null,
-  ): Promise<void> {
-    if (this.isChangingMuteEmailsSetting(updatedSettingName)) {
-      await this.triggerEmailSubscriptionChange(user, updatedSettingName, unencryptedValue)
-    }
-
-    if (this.isEnablingEmailBackupSetting(updatedSettingName, unencryptedValue)) {
-      await this.triggerEmailBackup(user.uuid)
-    }
-
-    if (this.isDisablingSessionUserAgentLogging(updatedSettingName, unencryptedValue)) {
-      await this.triggerSessionUserAgentCleanup(user)
-    }
-  }
-
-  private async triggerEmailBackup(userUuid: string): Promise<void> {
-    let userHasEmailsMuted = false
-    let muteEmailsSettingUuid = ''
-    const muteFailedEmailsBackupSetting = await this.settingRepository.findOneByNameAndUserUuid(
-      SettingName.NAMES.MuteFailedBackupsEmails,
-      userUuid,
-    )
-    if (muteFailedEmailsBackupSetting !== null) {
-      userHasEmailsMuted = muteFailedEmailsBackupSetting.props.value === MuteFailedBackupsEmailsOption.Muted
-      muteEmailsSettingUuid = muteFailedEmailsBackupSetting.id.toString()
-    }
-
-    const keyParamsResponse = await this.getUserKeyParams.execute({
-      authenticated: false,
-      userUuid,
-    })
-
-    await this.domainEventPublisher.publish(
-      this.domainEventFactory.createEmailBackupRequestedEvent(
-        userUuid,
-        muteEmailsSettingUuid,
-        userHasEmailsMuted,
-        keyParamsResponse.keyParams,
-      ),
-    )
-  }
-
-  private isChangingMuteEmailsSetting(settingName: string): boolean {
-    return [
-      SettingName.NAMES.MuteFailedBackupsEmails,
-      SettingName.NAMES.MuteFailedCloudBackupsEmails,
-      SettingName.NAMES.MuteMarketingEmails,
-      SettingName.NAMES.MuteSignInEmails,
-    ].includes(settingName)
-  }
-
-  private isEnablingEmailBackupSetting(settingName: string, newValue: string | null): boolean {
-    return (
-      settingName === SettingName.NAMES.EmailBackupFrequency &&
-      [EmailBackupFrequency.Daily, EmailBackupFrequency.Weekly].includes(newValue as EmailBackupFrequency)
-    )
-  }
-
-  private isDisablingSessionUserAgentLogging(settingName: string, newValue: string | null): boolean {
-    return SettingName.NAMES.LogSessionUserAgent === settingName && LogSessionUserAgentOption.Disabled === newValue
-  }
-
-  private async triggerEmailSubscriptionChange(
-    user: User,
-    settingName: string,
-    unencryptedValue: string | null,
-  ): Promise<void> {
-    await this.domainEventPublisher.publish(
-      this.domainEventFactory.createMuteEmailsSettingChangedEvent({
-        username: user.email,
-        mute: unencryptedValue === 'muted',
-        emailSubscriptionRejectionLevel: this.emailSettingToSubscriptionRejectionLevelMap.get(settingName) as string,
-      }),
-    )
-  }
-
-  private async triggerSessionUserAgentCleanup(user: User) {
-    await this.domainEventPublisher.publish(
-      this.domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent({
-        userUuid: user.uuid,
-        email: user.email,
-      }),
-    )
-  }
-}

+ 0 - 5
packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts

@@ -1,5 +0,0 @@
-import { User } from '../User/User'
-
-export interface SettingInterpreterInterface {
-  interpretSettingUpdated(updatedSettingName: string, user: User, newUnencryptedValue: string | null): Promise<void>
-}

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

@@ -1,4 +1,3 @@
-import { ReadStream } from 'fs'
 import { SettingName } from '@standardnotes/domain-core'
 
 import { DeleteSettingDto } from '../UseCase/DeleteSetting/DeleteSettingDto'
@@ -10,8 +9,8 @@ export interface SettingRepositoryInterface {
   findOneByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null>
   findLastByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null>
   findAllByUserUuid(userUuid: string): Promise<Setting[]>
-  streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream>
-  streamAllByName(name: SettingName): Promise<ReadStream>
+  countAllByNameAndValue(dto: { name: SettingName; value: string }): Promise<number>
+  findAllByNameAndValue(dto: { name: SettingName; value: string; offset: number; limit: number }): Promise<Setting[]>
   deleteByUserUuid(dto: DeleteSettingDto): Promise<void>
   insert(setting: Setting): Promise<void>
   update(setting: Setting): Promise<void>

+ 47 - 0
packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.spec.ts

@@ -0,0 +1,47 @@
+import { Logger } from 'winston'
+import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
+import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
+import { TriggerEmailBackupForAllUsers } from './TriggerEmailBackupForAllUsers'
+import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
+
+import { Setting } from '../../Setting/Setting'
+import { Result, SettingName, Timestamps, Uuid } from '@standardnotes/domain-core'
+
+describe('TriggerEmailBackupForAllUsers', () => {
+  let settingRepository: SettingRepositoryInterface
+  let triggerEmailBackupForUserUseCase: TriggerEmailBackupForUser
+  let logger: Logger
+
+  const createUseCase = () =>
+    new TriggerEmailBackupForAllUsers(settingRepository, triggerEmailBackupForUserUseCase, logger)
+
+  beforeEach(() => {
+    const setting = Setting.create({
+      name: SettingName.NAMES.EmailBackupFrequency,
+      value: null,
+      serverEncryptionVersion: EncryptionVersion.Default,
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      sensitive: false,
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+
+    settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
+    settingRepository.countAllByNameAndValue = jest.fn().mockResolvedValue(1)
+    settingRepository.findAllByNameAndValue = jest.fn().mockResolvedValue([setting])
+
+    triggerEmailBackupForUserUseCase = {} as jest.Mocked<TriggerEmailBackupForUser>
+    triggerEmailBackupForUserUseCase.execute = jest.fn().mockResolvedValue(Result.ok())
+
+    logger = {} as jest.Mocked<Logger>
+    logger.error = jest.fn()
+    logger.info = jest.fn()
+  })
+
+  it('triggers email backup for all users', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({ backupFrequency: 'daily' })
+
+    expect(result.isFailed()).toBeFalsy()
+  })
+})

+ 55 - 0
packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.ts

@@ -0,0 +1,55 @@
+import { Result, SettingName, UseCaseInterface } from '@standardnotes/domain-core'
+import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
+import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
+import { TriggerEmailBackupForAllUsersDTO } from './TriggerEmailBackupForAllUsersDTO'
+import { Logger } from 'winston'
+
+export class TriggerEmailBackupForAllUsers implements UseCaseInterface<void> {
+  private PAGING_LIMIT = 100
+
+  constructor(
+    private settingRepository: SettingRepositoryInterface,
+    private triggerEmailBackupForUserUseCase: TriggerEmailBackupForUser,
+    private logger: Logger,
+  ) {}
+
+  async execute(dto: TriggerEmailBackupForAllUsersDTO): Promise<Result<void>> {
+    const emailBackupFrequencySettingName = SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue()
+
+    const allSettingsCount = await this.settingRepository.countAllByNameAndValue({
+      name: emailBackupFrequencySettingName,
+      value: dto.backupFrequency,
+    })
+
+    this.logger.info(`Found ${allSettingsCount} users with email backup frequency set to ${dto.backupFrequency}`)
+
+    let failedUsers = 0
+    const numberOfPages = Math.ceil(allSettingsCount / this.PAGING_LIMIT)
+    for (let i = 0; i < numberOfPages; i++) {
+      const settings = await this.settingRepository.findAllByNameAndValue({
+        name: emailBackupFrequencySettingName,
+        value: dto.backupFrequency,
+        offset: i * this.PAGING_LIMIT,
+        limit: this.PAGING_LIMIT,
+      })
+
+      for (const setting of settings) {
+        const result = await this.triggerEmailBackupForUserUseCase.execute({
+          userUuid: setting.props.userUuid.value,
+        })
+        /* istanbul ignore next */
+        if (result.isFailed()) {
+          this.logger.error(`Failed to trigger email backup for user ${setting.props.userUuid.value}`)
+          failedUsers++
+        }
+      }
+    }
+
+    /* istanbul ignore next */
+    if (failedUsers > 0) {
+      this.logger.error(`Failed to trigger email backup for ${failedUsers} users`)
+    }
+
+    return Result.ok()
+  }
+}

+ 3 - 0
packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsersDTO.ts

@@ -0,0 +1,3 @@
+export interface TriggerEmailBackupForAllUsersDTO {
+  backupFrequency: string
+}

+ 78 - 0
packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.spec.ts

@@ -0,0 +1,78 @@
+import { DomainEventPublisherInterface, EmailBackupRequestedEvent } from '@standardnotes/domain-events'
+import { Result, SettingName, Timestamps, Uuid } from '@standardnotes/domain-core'
+
+import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
+import { GetSetting } from '../GetSetting/GetSetting'
+import { GetUserKeyParams } from '../GetUserKeyParams/GetUserKeyParams'
+import { TriggerEmailBackupForUser } from './TriggerEmailBackupForUser'
+import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
+import { Setting } from '../../Setting/Setting'
+import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
+
+describe('TriggerEmailBackupForUser', () => {
+  let roleService: RoleServiceInterface
+  let getSetting: GetSetting
+  let getUserKeyParamsUseCase: GetUserKeyParams
+  let domainEventPublisher: DomainEventPublisherInterface
+  let domainEventFactory: DomainEventFactoryInterface
+
+  const createUseCase = () =>
+    new TriggerEmailBackupForUser(
+      roleService,
+      getSetting,
+      getUserKeyParamsUseCase,
+      domainEventPublisher,
+      domainEventFactory,
+    )
+
+  beforeEach(() => {
+    roleService = {} as jest.Mocked<RoleServiceInterface>
+    roleService.userHasPermission = jest.fn().mockResolvedValue(true)
+
+    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()
+
+    getSetting = {} as jest.Mocked<GetSetting>
+    getSetting.execute = jest.fn().mockResolvedValue(Result.ok({ setting, decryptedValue: 'not_muted' }))
+
+    getUserKeyParamsUseCase = {} as jest.Mocked<GetUserKeyParams>
+    getUserKeyParamsUseCase.execute = jest.fn().mockResolvedValue({ keyParams: {} })
+
+    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
+    domainEventPublisher.publish = jest.fn()
+
+    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
+    domainEventFactory.createEmailBackupRequestedEvent = jest.fn().mockReturnValue({} as EmailBackupRequestedEvent)
+  })
+
+  it('publishes EmailBackupRequestedEvent', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
+
+    expect(result.isFailed()).toBeFalsy()
+  })
+
+  it('returns error if user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({ userUuid: 'invalid-uuid' })
+
+    expect(result.isFailed()).toBe(true)
+  })
+
+  it('returns error if user is not permitted for email backups', async () => {
+    roleService.userHasPermission = jest.fn().mockResolvedValue(false)
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
+
+    expect(result.isFailed()).toBe(true)
+  })
+})

+ 66 - 0
packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.ts

@@ -0,0 +1,66 @@
+import { Result, SettingName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { PermissionName } from '@standardnotes/features'
+
+import { TriggerEmailBackupForUserDTO } from './TriggerEmailBackupForUserDTO'
+import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
+import { GetSetting } from '../GetSetting/GetSetting'
+import { MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
+import { GetUserKeyParams } from '../GetUserKeyParams/GetUserKeyParams'
+import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
+import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
+
+export class TriggerEmailBackupForUser implements UseCaseInterface<void> {
+  constructor(
+    private roleService: RoleServiceInterface,
+    private getSetting: GetSetting,
+    private getUserKeyParamsUseCase: GetUserKeyParams,
+    private domainEventPublisher: DomainEventPublisherInterface,
+    private domainEventFactory: DomainEventFactoryInterface,
+  ) {}
+
+  async execute(dto: TriggerEmailBackupForUserDTO): Promise<Result<void>> {
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const userIsPermittedForEmailBackups = await this.roleService.userHasPermission(
+      userUuid.value,
+      PermissionName.DailyEmailBackup,
+    )
+
+    if (!userIsPermittedForEmailBackups) {
+      return Result.fail(`User ${userUuid.value} is not permitted for email backups`)
+    }
+
+    let userHasEmailsMuted = false
+    const emailsMutedSettingOrError = await this.getSetting.execute({
+      allowSensitiveRetrieval: true,
+      decrypted: true,
+      settingName: SettingName.NAMES.MuteFailedBackupsEmails,
+      userUuid: userUuid.value,
+    })
+    let emailsMutedSetting = null
+    if (!emailsMutedSettingOrError.isFailed()) {
+      emailsMutedSetting = emailsMutedSettingOrError.getValue()
+      userHasEmailsMuted = emailsMutedSetting.decryptedValue === MuteFailedBackupsEmailsOption.Muted
+    }
+
+    const keyParamsResponse = await this.getUserKeyParamsUseCase.execute({
+      userUuid: userUuid.value,
+      authenticated: false,
+    })
+
+    await this.domainEventPublisher.publish(
+      this.domainEventFactory.createEmailBackupRequestedEvent(
+        userUuid.value,
+        emailsMutedSetting?.setting.id.toString() as string,
+        userHasEmailsMuted,
+        keyParamsResponse.keyParams,
+      ),
+    )
+
+    return Result.ok()
+  }
+}

+ 3 - 0
packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUserDTO.ts

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

+ 104 - 0
packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.spec.ts

@@ -0,0 +1,104 @@
+import {
+  DomainEventPublisherInterface,
+  EmailBackupRequestedEvent,
+  MuteEmailsSettingChangedEvent,
+  UserDisabledSessionUserAgentLoggingEvent,
+} from '@standardnotes/domain-events'
+import { EmailBackupFrequency, LogSessionUserAgentOption, MuteMarketingEmailsOption } from '@standardnotes/settings'
+import { SettingName, Result } from '@standardnotes/domain-core'
+
+import { TriggerPostSettingUpdateActions } from './TriggerPostSettingUpdateActions'
+import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
+import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
+
+describe('TriggerPostSettingUpdateActions', () => {
+  let domainEventPublisher: DomainEventPublisherInterface
+  let domainEventFactory: DomainEventFactoryInterface
+  let triggerEmailBackupForUser: TriggerEmailBackupForUser
+
+  const createUseCase = () =>
+    new TriggerPostSettingUpdateActions(domainEventPublisher, domainEventFactory, triggerEmailBackupForUser)
+
+  beforeEach(() => {
+    triggerEmailBackupForUser = {} as jest.Mocked<TriggerEmailBackupForUser>
+    triggerEmailBackupForUser.execute = jest.fn().mockReturnValue(Result.ok())
+
+    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
+    domainEventPublisher.publish = jest.fn()
+
+    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
+    domainEventFactory.createEmailBackupRequestedEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<EmailBackupRequestedEvent>)
+    domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>)
+    domainEventFactory.createMuteEmailsSettingChangedEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<MuteEmailsSettingChangedEvent>)
+  })
+
+  it('should trigger session cleanup if user is disabling session user agent logging', async () => {
+    await createUseCase().execute({
+      updatedSettingName: SettingName.NAMES.LogSessionUserAgent,
+      userUuid: '4-5-6',
+      userEmail: 'test@test.te',
+      unencryptedValue: LogSessionUserAgentOption.Disabled,
+    })
+
+    expect(domainEventPublisher.publish).toHaveBeenCalled()
+    expect(domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent).toHaveBeenCalledWith({
+      userUuid: '4-5-6',
+      email: 'test@test.te',
+    })
+  })
+
+  it('should trigger backup if email backup setting is created - emails not muted', async () => {
+    await createUseCase().execute({
+      updatedSettingName: SettingName.NAMES.EmailBackupFrequency,
+      userUuid: '4-5-6',
+      userEmail: 'test@test.te',
+      unencryptedValue: EmailBackupFrequency.Daily,
+    })
+
+    expect(triggerEmailBackupForUser.execute).toHaveBeenCalled()
+  })
+
+  it('should trigger backup if email backup setting is created - emails muted', async () => {
+    await createUseCase().execute({
+      updatedSettingName: SettingName.NAMES.EmailBackupFrequency,
+      userUuid: '4-5-6',
+      userEmail: 'test@test.te',
+      unencryptedValue: EmailBackupFrequency.Daily,
+    })
+
+    expect(triggerEmailBackupForUser.execute).toHaveBeenCalled()
+  })
+
+  it('should not trigger backup if email backup setting is disabled', async () => {
+    await createUseCase().execute({
+      updatedSettingName: SettingName.NAMES.EmailBackupFrequency,
+      userUuid: '4-5-6',
+      userEmail: 'test@test.te',
+      unencryptedValue: EmailBackupFrequency.Disabled,
+    })
+
+    expect(triggerEmailBackupForUser.execute).not.toHaveBeenCalled()
+  })
+
+  it('should trigger mute subscription emails rejection if mute setting changed', async () => {
+    await createUseCase().execute({
+      updatedSettingName: SettingName.NAMES.MuteMarketingEmails,
+      userUuid: '4-5-6',
+      userEmail: 'test@test.te',
+      unencryptedValue: MuteMarketingEmailsOption.Muted,
+    })
+
+    expect(domainEventPublisher.publish).toHaveBeenCalled()
+    expect(domainEventFactory.createMuteEmailsSettingChangedEvent).toHaveBeenCalledWith({
+      emailSubscriptionRejectionLevel: 'MARKETING',
+      mute: true,
+      username: 'test@test.te',
+    })
+  })
+})

+ 83 - 0
packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.ts

@@ -0,0 +1,83 @@
+import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
+import { EmailLevel, Result, SettingName, UseCaseInterface } from '@standardnotes/domain-core'
+import { EmailBackupFrequency, LogSessionUserAgentOption } from '@standardnotes/settings'
+
+import { TriggerPostSettingUpdateActionsDTO } from './TriggerPostSettingUpdateActionsDTO'
+import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
+import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
+
+export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
+  private readonly emailSettingToSubscriptionRejectionLevelMap: Map<string, string> = new Map([
+    [SettingName.NAMES.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
+    [SettingName.NAMES.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup],
+    [SettingName.NAMES.MuteMarketingEmails, EmailLevel.LEVELS.Marketing],
+    [SettingName.NAMES.MuteSignInEmails, EmailLevel.LEVELS.SignIn],
+  ])
+
+  constructor(
+    private domainEventPublisher: DomainEventPublisherInterface,
+    private domainEventFactory: DomainEventFactoryInterface,
+    private triggerEmailBackupForUser: TriggerEmailBackupForUser,
+  ) {}
+
+  async execute(dto: TriggerPostSettingUpdateActionsDTO): Promise<Result<void>> {
+    if (this.isChangingMuteEmailsSetting(dto.updatedSettingName)) {
+      await this.triggerEmailSubscriptionChange(dto.userEmail, dto.updatedSettingName, dto.unencryptedValue)
+    }
+
+    if (this.isEnablingEmailBackupSetting(dto.updatedSettingName, dto.unencryptedValue)) {
+      await this.triggerEmailBackupForUser.execute({
+        userUuid: dto.userUuid,
+      })
+    }
+
+    if (this.isDisablingSessionUserAgentLogging(dto.updatedSettingName, dto.unencryptedValue)) {
+      await this.triggerSessionUserAgentCleanup(dto.userEmail, dto.userUuid)
+    }
+
+    return Result.ok()
+  }
+
+  private isChangingMuteEmailsSetting(settingName: string): boolean {
+    return [
+      SettingName.NAMES.MuteFailedBackupsEmails,
+      SettingName.NAMES.MuteFailedCloudBackupsEmails,
+      SettingName.NAMES.MuteMarketingEmails,
+      SettingName.NAMES.MuteSignInEmails,
+    ].includes(settingName)
+  }
+
+  private isEnablingEmailBackupSetting(settingName: string, newValue: string | null): boolean {
+    return (
+      settingName === SettingName.NAMES.EmailBackupFrequency &&
+      [EmailBackupFrequency.Daily, EmailBackupFrequency.Weekly].includes(newValue as EmailBackupFrequency)
+    )
+  }
+
+  private isDisablingSessionUserAgentLogging(settingName: string, newValue: string | null): boolean {
+    return SettingName.NAMES.LogSessionUserAgent === settingName && LogSessionUserAgentOption.Disabled === newValue
+  }
+
+  private async triggerEmailSubscriptionChange(
+    userEmail: string,
+    settingName: string,
+    unencryptedValue: string | null,
+  ): Promise<void> {
+    await this.domainEventPublisher.publish(
+      this.domainEventFactory.createMuteEmailsSettingChangedEvent({
+        username: userEmail,
+        mute: unencryptedValue === 'muted',
+        emailSubscriptionRejectionLevel: this.emailSettingToSubscriptionRejectionLevelMap.get(settingName) as string,
+      }),
+    )
+  }
+
+  private async triggerSessionUserAgentCleanup(userEmail: string, userUuid: string) {
+    await this.domainEventPublisher.publish(
+      this.domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent({
+        userUuid,
+        email: userEmail,
+      }),
+    )
+  }
+}

+ 6 - 0
packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActionsDTO.ts

@@ -0,0 +1,6 @@
+export interface TriggerPostSettingUpdateActionsDTO {
+  updatedSettingName: string
+  userUuid: string
+  userEmail: string
+  unencryptedValue: string | null
+}

+ 7 - 0
packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.ts

@@ -19,6 +19,8 @@ import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting'
 import { SettingHttpRepresentation } from '../../Mapping/Http/SettingHttpRepresentation'
 import { SubscriptionSettingHttpRepresentation } from '../../Mapping/Http/SubscriptionSettingHttpRepresentation'
 import { GetAllSettingsForUser } from '../../Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser'
+import { TriggerPostSettingUpdateActions } from '../../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions'
+import { Logger } from 'winston'
 
 @controller('/users/:userUuid')
 export class AnnotatedSettingsController extends BaseSettingsController {
@@ -26,18 +28,23 @@ export class AnnotatedSettingsController extends BaseSettingsController {
     @inject(TYPES.Auth_GetAllSettingsForUser) override doGetSettings: GetAllSettingsForUser,
     @inject(TYPES.Auth_GetSetting) override doGetSetting: GetSetting,
     @inject(TYPES.Auth_SetSettingValue) override setSettingValue: SetSettingValue,
+    @inject(TYPES.Auth_TriggerPostSettingUpdateActions)
+    override triggerPostSettingUpdateActions: TriggerPostSettingUpdateActions,
     @inject(TYPES.Auth_DeleteSetting) override doDeleteSetting: DeleteSetting,
     @inject(TYPES.Auth_SettingHttpMapper) settingHttMapper: MapperInterface<Setting, SettingHttpRepresentation>,
     @inject(TYPES.Auth_SubscriptionSettingHttpMapper)
     subscriptionSettingHttpMapper: MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>,
+    @inject(TYPES.Auth_Logger) logger: Logger,
   ) {
     super(
       doGetSettings,
       doGetSetting,
       setSettingValue,
+      triggerPostSettingUpdateActions,
       doDeleteSetting,
       settingHttMapper,
       subscriptionSettingHttpMapper,
+      logger,
     )
   }
 

+ 14 - 0
packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts

@@ -2,6 +2,7 @@ import { ControllerContainerInterface, MapperInterface } from '@standardnotes/do
 import { BaseHttpController, results } from 'inversify-express-utils'
 import { ErrorTag } from '@standardnotes/responses'
 import { Request, Response } from 'express'
+import { Logger } from 'winston'
 
 import { DeleteSetting } from '../../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting'
@@ -11,18 +12,21 @@ import { Setting } from '../../../Domain/Setting/Setting'
 import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting'
 import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation'
 import { SettingHttpRepresentation } from '../../../Mapping/Http/SettingHttpRepresentation'
+import { TriggerPostSettingUpdateActions } from '../../../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions'
 
 export class BaseSettingsController extends BaseHttpController {
   constructor(
     protected doGetSettings: GetAllSettingsForUser,
     protected doGetSetting: GetSetting,
     protected setSettingValue: SetSettingValue,
+    protected triggerPostSettingUpdateActions: TriggerPostSettingUpdateActions,
     protected doDeleteSetting: DeleteSetting,
     protected settingHttMapper: MapperInterface<Setting, SettingHttpRepresentation>,
     protected subscriptionSettingHttpMapper: MapperInterface<
       SubscriptionSetting,
       SubscriptionSettingHttpRepresentation
     >,
+    protected logger: Logger,
     private controllerContainer?: ControllerContainerInterface,
   ) {
     super()
@@ -175,6 +179,16 @@ export class BaseSettingsController extends BaseHttpController {
     }
     const setting = result.getValue()
 
+    const triggerResult = await this.triggerPostSettingUpdateActions.execute({
+      updatedSettingName: setting.props.name,
+      userUuid: response.locals.user.uuid,
+      userEmail: response.locals.user.email,
+      unencryptedValue: value,
+    })
+    if (triggerResult.isFailed()) {
+      this.logger.error(`Failed to trigger post setting update actions: ${triggerResult.getError()}`)
+    }
+
     return this.json({
       success: true,
       setting: setting.props.sensitive ? undefined : this.settingHttMapper.toProjection(setting),

+ 30 - 22
packages/auth/src/Infra/TypeORM/TypeORMSettingRepository.ts

@@ -1,4 +1,3 @@
-import { ReadStream } from 'fs'
 import { Repository } from 'typeorm'
 
 import { Setting } from '../../Domain/Setting/Setting'
@@ -13,6 +12,36 @@ export class TypeORMSettingRepository implements SettingRepositoryInterface {
     private mapper: MapperInterface<Setting, TypeORMSetting>,
   ) {}
 
+  async countAllByNameAndValue(dto: { name: SettingName; value: string }): Promise<number> {
+    return this.ormRepository
+      .createQueryBuilder()
+      .where('name = :name AND value = :value', {
+        name: dto.name.value,
+        value: dto.value,
+      })
+      .getCount()
+  }
+
+  async findAllByNameAndValue(dto: {
+    name: SettingName
+    value: string
+    offset: number
+    limit: number
+  }): Promise<Setting[]> {
+    const persistence = await this.ormRepository
+      .createQueryBuilder()
+      .where('name = :name AND value = :value', {
+        name: dto.name.value,
+        value: dto.value,
+      })
+      .orderBy('created_at', 'ASC')
+      .take(dto.limit)
+      .skip(dto.offset)
+      .getMany()
+
+    return persistence.map((p) => this.mapper.toDomain(p))
+  }
+
   async insert(setting: Setting): Promise<void> {
     const persistence = this.mapper.toProjection(setting)
 
@@ -42,27 +71,6 @@ export class TypeORMSettingRepository implements SettingRepositoryInterface {
     return this.mapper.toDomain(persistence)
   }
 
-  async streamAllByName(name: SettingName): Promise<ReadStream> {
-    return this.ormRepository
-      .createQueryBuilder('setting')
-      .where('setting.name = :name', {
-        name: name.value,
-      })
-      .orderBy('updated_at', 'ASC')
-      .stream()
-  }
-
-  async streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream> {
-    return this.ormRepository
-      .createQueryBuilder('setting')
-      .where('setting.name = :name AND setting.value = :value', {
-        name: name.value,
-        value,
-      })
-      .orderBy('updated_at', 'ASC')
-      .stream()
-  }
-
   async findOneByUuid(uuid: string): Promise<Setting | null> {
     const persistence = await this.ormRepository
       .createQueryBuilder('setting')