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
This commit is contained in:
parent
0cb234aa47
commit
d228a86f48
21 changed files with 550 additions and 415 deletions
|
@ -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<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
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
;;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { User } from '../User/User'
|
||||
|
||||
export interface SettingInterpreterInterface {
|
||||
interpretSettingUpdated(updatedSettingName: string, user: User, newUnencryptedValue: string | null): Promise<void>
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface TriggerEmailBackupForAllUsersDTO {
|
||||
backupFrequency: string
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface TriggerEmailBackupForUserDTO {
|
||||
userUuid: string
|
||||
}
|
|
@ -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',
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface TriggerPostSettingUpdateActionsDTO {
|
||||
updatedSettingName: string
|
||||
userUuid: string
|
||||
userEmail: string
|
||||
unencryptedValue: string | null
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue