From d228a86f48c9ff62b7810244c347abf7770e2b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Tue, 7 Nov 2023 15:14:51 +0100 Subject: [PATCH] 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 --- packages/auth/bin/backup.ts | 95 ++--------- packages/auth/docker/entrypoint.sh | 19 +-- packages/auth/package.json | 7 +- packages/auth/src/Bootstrap/Container.ts | 46 ++++-- packages/auth/src/Bootstrap/Types.ts | 4 +- .../Domain/Setting/SettingInterpreter.spec.ts | 153 ------------------ .../src/Domain/Setting/SettingInterpreter.ts | 113 ------------- .../Setting/SettingInterpreterInterface.ts | 5 - .../Setting/SettingRepositoryInterface.ts | 5 +- .../TriggerEmailBackupForAllUsers.spec.ts | 47 ++++++ .../TriggerEmailBackupForAllUsers.ts | 55 +++++++ .../TriggerEmailBackupForAllUsersDTO.ts | 3 + .../TriggerEmailBackupForUser.spec.ts | 78 +++++++++ .../TriggerEmailBackupForUser.ts | 66 ++++++++ .../TriggerEmailBackupForUserDTO.ts | 3 + .../TriggerPostSettingUpdateActions.spec.ts | 104 ++++++++++++ .../TriggerPostSettingUpdateActions.ts | 83 ++++++++++ .../TriggerPostSettingUpdateActionsDTO.ts | 6 + .../AnnotatedSettingsController.ts | 7 + .../Base/BaseSettingsController.ts | 14 ++ .../Infra/TypeORM/TypeORMSettingRepository.ts | 52 +++--- 21 files changed, 550 insertions(+), 415 deletions(-) delete mode 100644 packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts delete mode 100644 packages/auth/src/Domain/Setting/SettingInterpreter.ts delete mode 100644 packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts create mode 100644 packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.spec.ts create mode 100644 packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.ts create mode 100644 packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsersDTO.ts create mode 100644 packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.spec.ts create mode 100644 packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.ts create mode 100644 packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUserDTO.ts create mode 100644 packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.spec.ts create mode 100644 packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.ts create mode 100644 packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActionsDTO.ts diff --git a/packages/auth/bin/backup.ts b/packages/auth/bin/backup.ts index 0cc871834..bd16cb5a4 100644 --- a/packages/auth/bin/backup.ts +++ b/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 backupFrequency = inputArgs[0] -const requestBackups = async ( - settingRepository: SettingRepositoryInterface, - roleService: RoleServiceInterface, - domainEventFactory: DomainEventFactoryInterface, - domainEventPublisher: DomainEventPublisherInterface, - getUserKeyParamsUseCase: GetUserKeyParams, -): Promise => { - 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 - } - - 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 => { + 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) }) diff --git a/packages/auth/docker/entrypoint.sh b/packages/auth/docker/entrypoint.sh index 76d5e2a87..23ff5db3f 100755 --- a/packages/auth/docker/entrypoint.sh +++ b/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" ;; diff --git a/packages/auth/package.json b/packages/auth/package.json index e1a4acdae..bf90f953b 100644 --- a/packages/auth/package.json +++ b/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" diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 94ba33bac..d64698cac 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/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(TYPES.Auth_Logger), ), ) - container - .bind(TYPES.Auth_SettingInterpreter) - .toConstantValue( - new SettingInterpreter( - container.get(TYPES.Auth_DomainEventPublisher), - container.get(TYPES.Auth_DomainEventFactory), - container.get(TYPES.Auth_SettingRepository), - container.get(TYPES.Auth_GetUserKeyParams), - ), - ) container.bind(TYPES.Auth_OfflineSettingService).to(OfflineSettingService) container.bind(TYPES.Auth_ContenDecoder).toConstantValue(new ContentDecoder()) @@ -1231,6 +1222,35 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_GetSharedOrRegularSubscriptionForUser), ), ) + container + .bind(TYPES.Auth_TriggerEmailBackupForUser) + .toConstantValue( + new TriggerEmailBackupForUser( + container.get(TYPES.Auth_RoleService), + container.get(TYPES.Auth_GetSetting), + container.get(TYPES.Auth_GetUserKeyParams), + container.get(TYPES.Auth_DomainEventPublisher), + container.get(TYPES.Auth_DomainEventFactory), + ), + ) + container + .bind(TYPES.Auth_TriggerEmailBackupForAllUsers) + .toConstantValue( + new TriggerEmailBackupForAllUsers( + container.get(TYPES.Auth_SettingRepository), + container.get(TYPES.Auth_TriggerEmailBackupForUser), + container.get(TYPES.Auth_Logger), + ), + ) + container + .bind(TYPES.Auth_TriggerPostSettingUpdateActions) + .toConstantValue( + new TriggerPostSettingUpdateActions( + container.get(TYPES.Auth_DomainEventPublisher), + container.get(TYPES.Auth_DomainEventFactory), + container.get(TYPES.Auth_TriggerEmailBackupForUser), + ), + ) // Controller container @@ -1655,11 +1675,13 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_GetAllSettingsForUser), container.get(TYPES.Auth_GetSetting), container.get(TYPES.Auth_SetSettingValue), + container.get(TYPES.Auth_TriggerPostSettingUpdateActions), container.get(TYPES.Auth_DeleteSetting), container.get>(TYPES.Auth_SettingHttpMapper), container.get>( TYPES.Auth_SubscriptionSettingHttpMapper, ), + container.get(TYPES.Auth_Logger), container.get(TYPES.Auth_ControllerContainer), ), ) diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index 5f8d32b16..48abf8918 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/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'), diff --git a/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts b/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts deleted file mode 100644 index 01e7c20c3..000000000 --- a/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts +++ /dev/null @@ -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 - - settingRepository = {} as jest.Mocked - settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null) - settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) - - settingCrypter = {} as jest.Mocked - settingCrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted') - - domainEventPublisher = {} as jest.Mocked - domainEventPublisher.publish = jest.fn() - - domainEventFactory = {} as jest.Mocked - domainEventFactory.createEmailBackupRequestedEvent = jest - .fn() - .mockReturnValue({} as jest.Mocked) - domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent = jest - .fn() - .mockReturnValue({} as jest.Mocked) - domainEventFactory.createMuteEmailsSettingChangedEvent = jest - .fn() - .mockReturnValue({} as jest.Mocked) - - logger = {} as jest.Mocked - logger.debug = jest.fn() - logger.warn = jest.fn() - logger.error = jest.fn() - - getUserKeyParams = {} as jest.Mocked - getUserKeyParams.execute = jest.fn().mockReturnValue({ keyParams: {} as jest.Mocked }) - }) - - 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', - }) - }) -}) diff --git a/packages/auth/src/Domain/Setting/SettingInterpreter.ts b/packages/auth/src/Domain/Setting/SettingInterpreter.ts deleted file mode 100644 index d58456410..000000000 --- a/packages/auth/src/Domain/Setting/SettingInterpreter.ts +++ /dev/null @@ -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 = 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 { - 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 { - 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 { - 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, - }), - ) - } -} diff --git a/packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts b/packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts deleted file mode 100644 index 5288fded6..000000000 --- a/packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { User } from '../User/User' - -export interface SettingInterpreterInterface { - interpretSettingUpdated(updatedSettingName: string, user: User, newUnencryptedValue: string | null): Promise -} diff --git a/packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts b/packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts index 1de1a2700..10a59c861 100644 --- a/packages/auth/src/Domain/Setting/SettingRepositoryInterface.ts +++ b/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 findLastByNameAndUserUuid(name: string, userUuid: string): Promise findAllByUserUuid(userUuid: string): Promise - streamAllByNameAndValue(name: SettingName, value: string): Promise - streamAllByName(name: SettingName): Promise + countAllByNameAndValue(dto: { name: SettingName; value: string }): Promise + findAllByNameAndValue(dto: { name: SettingName; value: string; offset: number; limit: number }): Promise deleteByUserUuid(dto: DeleteSettingDto): Promise insert(setting: Setting): Promise update(setting: Setting): Promise diff --git a/packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.spec.ts b/packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.spec.ts new file mode 100644 index 000000000..c0177b3ed --- /dev/null +++ b/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 + settingRepository.countAllByNameAndValue = jest.fn().mockResolvedValue(1) + settingRepository.findAllByNameAndValue = jest.fn().mockResolvedValue([setting]) + + triggerEmailBackupForUserUseCase = {} as jest.Mocked + triggerEmailBackupForUserUseCase.execute = jest.fn().mockResolvedValue(Result.ok()) + + logger = {} as jest.Mocked + 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() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.ts b/packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsers.ts new file mode 100644 index 000000000..f3502caa1 --- /dev/null +++ b/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 { + private PAGING_LIMIT = 100 + + constructor( + private settingRepository: SettingRepositoryInterface, + private triggerEmailBackupForUserUseCase: TriggerEmailBackupForUser, + private logger: Logger, + ) {} + + async execute(dto: TriggerEmailBackupForAllUsersDTO): Promise> { + 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() + } +} diff --git a/packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsersDTO.ts b/packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsersDTO.ts new file mode 100644 index 000000000..f15324640 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/TriggerEmailBackupForAllUsers/TriggerEmailBackupForAllUsersDTO.ts @@ -0,0 +1,3 @@ +export interface TriggerEmailBackupForAllUsersDTO { + backupFrequency: string +} diff --git a/packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.spec.ts b/packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.spec.ts new file mode 100644 index 000000000..ded2aa598 --- /dev/null +++ b/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 + 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.execute = jest.fn().mockResolvedValue(Result.ok({ setting, decryptedValue: 'not_muted' })) + + getUserKeyParamsUseCase = {} as jest.Mocked + getUserKeyParamsUseCase.execute = jest.fn().mockResolvedValue({ keyParams: {} }) + + domainEventPublisher = {} as jest.Mocked + domainEventPublisher.publish = jest.fn() + + domainEventFactory = {} as jest.Mocked + 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) + }) +}) diff --git a/packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.ts b/packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUser.ts new file mode 100644 index 000000000..f753cc530 --- /dev/null +++ b/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 { + constructor( + private roleService: RoleServiceInterface, + private getSetting: GetSetting, + private getUserKeyParamsUseCase: GetUserKeyParams, + private domainEventPublisher: DomainEventPublisherInterface, + private domainEventFactory: DomainEventFactoryInterface, + ) {} + + async execute(dto: TriggerEmailBackupForUserDTO): Promise> { + 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() + } +} diff --git a/packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUserDTO.ts b/packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUserDTO.ts new file mode 100644 index 000000000..3fcc43951 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/TriggerEmailBackupForUser/TriggerEmailBackupForUserDTO.ts @@ -0,0 +1,3 @@ +export interface TriggerEmailBackupForUserDTO { + userUuid: string +} diff --git a/packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.spec.ts b/packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.spec.ts new file mode 100644 index 000000000..288605315 --- /dev/null +++ b/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.execute = jest.fn().mockReturnValue(Result.ok()) + + domainEventPublisher = {} as jest.Mocked + domainEventPublisher.publish = jest.fn() + + domainEventFactory = {} as jest.Mocked + domainEventFactory.createEmailBackupRequestedEvent = jest + .fn() + .mockReturnValue({} as jest.Mocked) + domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent = jest + .fn() + .mockReturnValue({} as jest.Mocked) + domainEventFactory.createMuteEmailsSettingChangedEvent = jest + .fn() + .mockReturnValue({} as jest.Mocked) + }) + + 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', + }) + }) +}) diff --git a/packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.ts b/packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions.ts new file mode 100644 index 000000000..8ca326eae --- /dev/null +++ b/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 { + private readonly emailSettingToSubscriptionRejectionLevelMap: Map = 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> { + 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 { + 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, + }), + ) + } +} diff --git a/packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActionsDTO.ts b/packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActionsDTO.ts new file mode 100644 index 000000000..d2f598a04 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActionsDTO.ts @@ -0,0 +1,6 @@ +export interface TriggerPostSettingUpdateActionsDTO { + updatedSettingName: string + userUuid: string + userEmail: string + unencryptedValue: string | null +} diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.ts index bca0df709..d4d317657 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSettingsController.ts +++ b/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, @inject(TYPES.Auth_SubscriptionSettingHttpMapper) subscriptionSettingHttpMapper: MapperInterface, + @inject(TYPES.Auth_Logger) logger: Logger, ) { super( doGetSettings, doGetSetting, setSettingValue, + triggerPostSettingUpdateActions, doDeleteSetting, settingHttMapper, subscriptionSettingHttpMapper, + logger, ) } diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts index e8356f1ca..058f82d8c 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts +++ b/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, 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), diff --git a/packages/auth/src/Infra/TypeORM/TypeORMSettingRepository.ts b/packages/auth/src/Infra/TypeORM/TypeORMSettingRepository.ts index e71bc4293..c080226cf 100644 --- a/packages/auth/src/Infra/TypeORM/TypeORMSettingRepository.ts +++ b/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, ) {} + async countAllByNameAndValue(dto: { name: SettingName; value: string }): Promise { + 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 { + 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 { 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 { - return this.ormRepository - .createQueryBuilder('setting') - .where('setting.name = :name', { - name: name.value, - }) - .orderBy('updated_at', 'ASC') - .stream() - } - - async streamAllByNameAndValue(name: SettingName, value: string): Promise { - 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 { const persistence = await this.ormRepository .createQueryBuilder('setting')