diff --git a/.pnp.cjs b/.pnp.cjs index 1f4474893..5c896bcb2 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -4601,6 +4601,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "./packages/settings/",\ "packageDependencies": [\ ["@standardnotes/settings", "workspace:packages/settings"],\ + ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.48.2"],\ ["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\ ["reflect-metadata", "npm:0.1.13"],\ diff --git a/packages/auth/bin/backup.ts b/packages/auth/bin/backup.ts index e0f6e524f..9665cc44f 100644 --- a/packages/auth/bin/backup.ts +++ b/packages/auth/bin/backup.ts @@ -32,36 +32,36 @@ const requestBackups = async ( ): Promise => { let settingName: SettingName, permissionName: PermissionName, - muteEmailsSettingName: SettingName, + muteEmailsSettingName: string, muteEmailsSettingValue: string, providerTokenSettingName: SettingName switch (backupProvider) { case 'email': - settingName = SettingName.EmailBackupFrequency + settingName = SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue() permissionName = PermissionName.DailyEmailBackup - muteEmailsSettingName = SettingName.MuteFailedBackupsEmails + muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted break case 'dropbox': - settingName = SettingName.DropboxBackupFrequency + settingName = SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue() permissionName = PermissionName.DailyDropboxBackup - muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails + muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted - providerTokenSettingName = SettingName.DropboxBackupToken + providerTokenSettingName = SettingName.create(SettingName.NAMES.DropboxBackupToken).getValue() break case 'one_drive': - settingName = SettingName.OneDriveBackupFrequency + settingName = SettingName.create(SettingName.NAMES.OneDriveBackupFrequency).getValue() permissionName = PermissionName.DailyOneDriveBackup - muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails + muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted - providerTokenSettingName = SettingName.OneDriveBackupToken + providerTokenSettingName = SettingName.create(SettingName.NAMES.OneDriveBackupToken).getValue() break case 'google_drive': - settingName = SettingName.GoogleDriveBackupFrequency + settingName = SettingName.create(SettingName.NAMES.GoogleDriveBackupFrequency).getValue() permissionName = PermissionName.DailyGDriveBackup - muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails + muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted - providerTokenSettingName = SettingName.GoogleDriveBackupToken + providerTokenSettingName = SettingName.create(SettingName.NAMES.GoogleDriveBackupToken).getValue() break default: throw new Error(`Not handled backup provider: ${backupProvider}`) diff --git a/packages/auth/bin/migrate_email_settings.ts b/packages/auth/bin/migrate_email_settings.ts new file mode 100644 index 000000000..cc292aa99 --- /dev/null +++ b/packages/auth/bin/migrate_email_settings.ts @@ -0,0 +1,137 @@ +import 'reflect-metadata' + +import 'newrelic' + +import { Stream } from 'stream' + +import { Logger } from 'winston' +import * as dayjs from 'dayjs' +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 { SettingName } from '@standardnotes/settings' +import { EmailLevel } from '@standardnotes/domain-core' +import { UserSubscriptionServiceInterface } from '../src/Domain/Subscription/UserSubscriptionServiceInterface' +import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface' +import { SubscriptionSettingServiceInterface } from '../src/Domain/Setting/SubscriptionSettingServiceInterface' +import { EncryptionVersion } from '../src/Domain/Encryption/EncryptionVersion' + +const requestSettingMigration = async ( + settingRepository: SettingRepositoryInterface, + subscriptionSettingService: SubscriptionSettingServiceInterface, + userRepository: UserRepositoryInterface, + userSubscriptionService: UserSubscriptionServiceInterface, + domainEventFactory: DomainEventFactoryInterface, + domainEventPublisher: DomainEventPublisherInterface, +): Promise => { + const stream = await settingRepository.streamAllByNameAndValue( + SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue(), + 'not_muted', + ) + + return new Promise((resolve, reject) => { + stream + .pipe( + new Stream.Transform({ + objectMode: true, + transform: async (setting, _encoding, callback) => { + const user = await userRepository.findOneByUuid(setting.setting_user_uuid) + if (!user) { + callback() + + return + } + + const { regularSubscription, sharedSubscription } = + await userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid) + + const subscription = sharedSubscription ?? regularSubscription + if (!subscription) { + await domainEventPublisher.publish( + domainEventFactory.createMuteEmailsSettingChangedEvent({ + username: user.email, + mute: true, + emailSubscriptionRejectionLevel: EmailLevel.LEVELS.SignIn, + }), + ) + + await settingRepository.deleteByUserUuid({ + userUuid: user.uuid, + settingName: SettingName.NAMES.MuteSignInEmails, + }) + + callback() + + return + } + + await subscriptionSettingService.createOrReplace({ + userSubscription: subscription, + props: { + name: SettingName.NAMES.MuteSignInEmails, + sensitive: false, + serverEncryptionVersion: EncryptionVersion.Unencrypted, + unencryptedValue: 'not_muted', + }, + }) + + await settingRepository.deleteByUserUuid({ + userUuid: user.uuid, + settingName: SettingName.NAMES.MuteSignInEmails, + }) + + callback() + }, + }), + ) + .on('finish', resolve) + .on('error', reject) + }) +} + +const container = new ContainerConfigLoader() +void container.load().then((container) => { + dayjs.extend(utc) + + const env: Env = new Env() + env.load() + + const logger: Logger = container.get(TYPES.Logger) + + logger.info('Starting migration of mute sign in emails settings to subscription settings...') + + const settingRepository: SettingRepositoryInterface = container.get(TYPES.SettingRepository) + const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory) + const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher) + const subscriptionSettingService: SubscriptionSettingServiceInterface = container.get( + TYPES.SubscriptionSettingService, + ) + const userRepository: UserRepositoryInterface = container.get(TYPES.UserRepository) + const userSubscriptionService: UserSubscriptionServiceInterface = container.get(TYPES.UserSubscriptionService) + + Promise.resolve( + requestSettingMigration( + settingRepository, + subscriptionSettingService, + userRepository, + userSubscriptionService, + domainEventFactory, + domainEventPublisher, + ), + ) + .then(() => { + logger.info('Migration of mute sign in emails settings to subscription settings finished successfully.') + + process.exit(0) + }) + .catch((error) => { + logger.error(`Migration of mute sign in emails settings to subscription settings failed: ${error.message}`) + + process.exit(1) + }) +}) diff --git a/packages/auth/bin/user_email_backup.ts b/packages/auth/bin/user_email_backup.ts index ce3288c14..f12ad4c75 100644 --- a/packages/auth/bin/user_email_backup.ts +++ b/packages/auth/bin/user_email_backup.ts @@ -28,7 +28,7 @@ const requestBackups = async ( domainEventPublisher: DomainEventPublisherInterface, ): Promise => { const permissionName = PermissionName.DailyEmailBackup - const muteEmailsSettingName = SettingName.MuteFailedBackupsEmails + const muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails const muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted if (!backupEmail) { diff --git a/packages/auth/docker/entrypoint-migrate-email-settings.js b/packages/auth/docker/entrypoint-migrate-email-settings.js new file mode 100644 index 000000000..e3d8aed23 --- /dev/null +++ b/packages/auth/docker/entrypoint-migrate-email-settings.js @@ -0,0 +1,11 @@ +'use strict' + +const path = require('path') + +const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup() + +const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/migrate_email_settings.js'))) + +Object.defineProperty(exports, '__esModule', { value: true }) + +exports.default = index diff --git a/packages/auth/docker/entrypoint.sh b/packages/auth/docker/entrypoint.sh index 76d5e2a87..ec0bf10fd 100755 --- a/packages/auth/docker/entrypoint.sh +++ b/packages/auth/docker/entrypoint.sh @@ -40,6 +40,11 @@ case "$COMMAND" in node docker/entrypoint-user-email-backup.js $EMAIL ;; + 'migrate-email-settings' ) + echo "[Docker] Starting Email Settings Migration..." + node docker/entrypoint-migrate-email-settings.js + ;; + 'dropbox-daily-backup' ) echo "[Docker] Starting Dropbox Daily Backup..." node docker/entrypoint-backup.js dropbox daily diff --git a/packages/auth/migrations/1627638504691-move_mfa_items_to_user_settings.ts b/packages/auth/migrations/1627638504691-move_mfa_items_to_user_settings.ts index ae4f64b2a..ed2dd1dee 100644 --- a/packages/auth/migrations/1627638504691-move_mfa_items_to_user_settings.ts +++ b/packages/auth/migrations/1627638504691-move_mfa_items_to_user_settings.ts @@ -34,7 +34,7 @@ export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterfa const setting = new Setting() setting.uuid = item['uuid'] - setting.name = SettingName.MfaSecret + setting.name = SettingName.NAMES.MfaSecret setting.value = item['content'] if (item['deleted']) { setting.value = null diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index eeef44e0b..351e9578e 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -168,7 +168,6 @@ import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedS import { UserSubscriptionServiceInterface } from '../Domain/Subscription/UserSubscriptionServiceInterface' import { UserSubscriptionService } from '../Domain/Subscription/UserSubscriptionService' import { SubscriptionSettingProjector } from '../Projection/SubscriptionSettingProjector' -import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting' import { SubscriptionSettingsAssociationService } from '../Domain/Setting/SubscriptionSettingsAssociationService' import { SubscriptionSettingsAssociationServiceInterface } from '../Domain/Setting/SubscriptionSettingsAssociationServiceInterface' import { PKCERepositoryInterface } from '../Domain/User/PKCERepositoryInterface' @@ -691,7 +690,6 @@ export class ContainerConfigLoader { container .bind(TYPES.ListSharedSubscriptionInvitations) .to(ListSharedSubscriptionInvitations) - container.bind(TYPES.GetSubscriptionSetting).to(GetSubscriptionSetting) container.bind(TYPES.VerifyPredicate).to(VerifyPredicate) container.bind(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken) container.bind(TYPES.ProcessUserRequest).to(ProcessUserRequest) diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index 0895b7a8c..f2a93a09b 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -132,7 +132,6 @@ const TYPES = { DeclineSharedSubscriptionInvitation: Symbol.for('DeclineSharedSubscriptionInvitation'), CancelSharedSubscriptionInvitation: Symbol.for('CancelSharedSubscriptionInvitation'), ListSharedSubscriptionInvitations: Symbol.for('ListSharedSubscriptionInvitations'), - GetSubscriptionSetting: Symbol.for('GetSubscriptionSetting'), VerifyPredicate: Symbol.for('VerifyPredicate'), CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'), ProcessUserRequest: Symbol.for('ProcessUserRequest'), diff --git a/packages/auth/src/Controller/AdminController.ts b/packages/auth/src/Controller/AdminController.ts index 765ae1502..a9156f6a8 100644 --- a/packages/auth/src/Controller/AdminController.ts +++ b/packages/auth/src/Controller/AdminController.ts @@ -69,7 +69,7 @@ export class AdminController extends BaseHttpController { const result = await this.doDeleteSetting.execute({ uuid, userUuid, - settingName: SettingName.MfaSecret, + settingName: SettingName.NAMES.MfaSecret, timestamp: updatedAt, softDelete: true, }) @@ -115,7 +115,7 @@ export class AdminController extends BaseHttpController { const result = await this.doDeleteSetting.execute({ userUuid, - settingName: SettingName.EmailBackupFrequency, + settingName: SettingName.NAMES.EmailBackupFrequency, }) if (result.success) { diff --git a/packages/auth/src/Controller/SettingsController.spec.ts b/packages/auth/src/Controller/SettingsController.spec.ts index 315524eca..70f48b0b3 100644 --- a/packages/auth/src/Controller/SettingsController.spec.ts +++ b/packages/auth/src/Controller/SettingsController.spec.ts @@ -90,7 +90,7 @@ describe('SettingsController', () => { const httpResponse = await createController().getSetting(request, response) const result = await httpResponse.executeAsync() - expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'test' }) + expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' }) expect(result.statusCode).toEqual(200) }) @@ -124,7 +124,7 @@ describe('SettingsController', () => { const httpResponse = await createController().getSetting(request, response) const result = await httpResponse.executeAsync() - expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'test' }) + expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' }) expect(result.statusCode).toEqual(400) }) diff --git a/packages/auth/src/Controller/SettingsController.ts b/packages/auth/src/Controller/SettingsController.ts index e8cdafe3f..c94a17bb1 100644 --- a/packages/auth/src/Controller/SettingsController.ts +++ b/packages/auth/src/Controller/SettingsController.ts @@ -61,7 +61,7 @@ export class SettingsController extends BaseHttpController { } const { userUuid, settingName } = request.params - const result = await this.doGetSetting.execute({ userUuid, settingName }) + const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() }) if (result.success) { return this.json(result) diff --git a/packages/auth/src/Controller/SubscriptionSettingsController.spec.ts b/packages/auth/src/Controller/SubscriptionSettingsController.spec.ts index 535623822..ba8e5e1cb 100644 --- a/packages/auth/src/Controller/SubscriptionSettingsController.spec.ts +++ b/packages/auth/src/Controller/SubscriptionSettingsController.spec.ts @@ -4,24 +4,24 @@ import * as express from 'express' import { results } from 'inversify-express-utils' import { User } from '../Domain/User/User' -import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting' import { SubscriptionSettingsController } from './SubscriptionSettingsController' +import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting' describe('SubscriptionSettingsController', () => { - let getSubscriptionSetting: GetSubscriptionSetting + let getSetting: GetSetting let request: express.Request let response: express.Response let user: User - const createController = () => new SubscriptionSettingsController(getSubscriptionSetting) + const createController = () => new SubscriptionSettingsController(getSetting) beforeEach(() => { user = {} as jest.Mocked user.uuid = '123' - getSubscriptionSetting = {} as jest.Mocked - getSubscriptionSetting.execute = jest.fn() + getSetting = {} as jest.Mocked + getSetting.execute = jest.fn() request = { headers: {}, @@ -41,12 +41,12 @@ describe('SubscriptionSettingsController', () => { uuid: '1-2-3', } - getSubscriptionSetting.execute = jest.fn().mockReturnValue({ success: true }) + getSetting.execute = jest.fn().mockReturnValue({ success: true }) const httpResponse = await createController().getSubscriptionSetting(request, response) const result = await httpResponse.executeAsync() - expect(getSubscriptionSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', subscriptionSettingName: 'test' }) + expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' }) expect(result.statusCode).toEqual(200) }) @@ -58,12 +58,12 @@ describe('SubscriptionSettingsController', () => { uuid: '1-2-3', } - getSubscriptionSetting.execute = jest.fn().mockReturnValue({ success: false }) + getSetting.execute = jest.fn().mockReturnValue({ success: false }) const httpResponse = await createController().getSubscriptionSetting(request, response) const result = await httpResponse.executeAsync() - expect(getSubscriptionSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', subscriptionSettingName: 'test' }) + expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' }) expect(result.statusCode).toEqual(400) }) diff --git a/packages/auth/src/Controller/SubscriptionSettingsController.ts b/packages/auth/src/Controller/SubscriptionSettingsController.ts index 4b5eecbec..0835a795f 100644 --- a/packages/auth/src/Controller/SubscriptionSettingsController.ts +++ b/packages/auth/src/Controller/SubscriptionSettingsController.ts @@ -1,4 +1,3 @@ -import { SubscriptionSettingName } from '@standardnotes/settings' import { Request, Response } from 'express' import { inject } from 'inversify' import { @@ -9,19 +8,19 @@ import { results, } from 'inversify-express-utils' import TYPES from '../Bootstrap/Types' -import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting' +import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting' @controller('/users/:userUuid') export class SubscriptionSettingsController extends BaseHttpController { - constructor(@inject(TYPES.GetSubscriptionSetting) private doGetSubscriptionSetting: GetSubscriptionSetting) { + constructor(@inject(TYPES.GetSetting) private doGetSetting: GetSetting) { super() } @httpGet('/subscription-settings/:subscriptionSettingName', TYPES.ApiGatewayAuthMiddleware) async getSubscriptionSetting(request: Request, response: Response): Promise { - const result = await this.doGetSubscriptionSetting.execute({ + const result = await this.doGetSetting.execute({ userUuid: response.locals.user.uuid, - subscriptionSettingName: request.params.subscriptionSettingName as SubscriptionSettingName, + settingName: request.params.subscriptionSettingName.toUpperCase(), }) if (result.success) { diff --git a/packages/auth/src/Controller/SubscriptionTokensController.ts b/packages/auth/src/Controller/SubscriptionTokensController.ts index bf2deb471..713841709 100644 --- a/packages/auth/src/Controller/SubscriptionTokensController.ts +++ b/packages/auth/src/Controller/SubscriptionTokensController.ts @@ -77,7 +77,7 @@ export class SubscriptionTokensController extends BaseHttpController { const user = authenticateTokenResponse.user as User let extensionKey = undefined const extensionKeySetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.ExtensionKey, + settingName: SettingName.create(SettingName.NAMES.ExtensionKey).getValue(), userUuid: user.uuid, }) if (extensionKeySetting !== null) { diff --git a/packages/auth/src/Domain/Handler/EmailSubscriptionUnsubscribedEventHandler.ts b/packages/auth/src/Domain/Handler/EmailSubscriptionUnsubscribedEventHandler.ts index 229717725..18ca17105 100644 --- a/packages/auth/src/Domain/Handler/EmailSubscriptionUnsubscribedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/EmailSubscriptionUnsubscribedEventHandler.ts @@ -27,13 +27,13 @@ export class EmailSubscriptionUnsubscribedEventHandler implements DomainEventHan private getSettingNameFromLevel(level: string): string { switch (level) { case EmailLevel.LEVELS.FailedCloudBackup: - return SettingName.MuteFailedCloudBackupsEmails + return SettingName.NAMES.MuteFailedCloudBackupsEmails case EmailLevel.LEVELS.FailedEmailBackup: - return SettingName.MuteFailedBackupsEmails + return SettingName.NAMES.MuteFailedBackupsEmails case EmailLevel.LEVELS.Marketing: - return SettingName.MuteMarketingEmails + return SettingName.NAMES.MuteMarketingEmails case EmailLevel.LEVELS.SignIn: - return SettingName.MuteSignInEmails + return SettingName.NAMES.MuteSignInEmails default: throw new Error(`Unknown level: ${level}`) } diff --git a/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.ts b/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.ts index 07c94d729..1da0c3ca3 100644 --- a/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/ExtensionKeyGrantedEventHandler.ts @@ -54,7 +54,7 @@ export class ExtensionKeyGrantedEventHandler implements DomainEventHandlerInterf await this.settingService.createOrReplace({ user, props: { - name: SettingName.ExtensionKey, + name: SettingName.NAMES.ExtensionKey, unencryptedValue: event.payload.extensionKey, serverEncryptionVersion: EncryptionVersion.Default, sensitive: true, diff --git a/packages/auth/src/Domain/Handler/FileRemovedEventHandler.ts b/packages/auth/src/Domain/Handler/FileRemovedEventHandler.ts index e03a9040c..a4c5310c0 100644 --- a/packages/auth/src/Domain/Handler/FileRemovedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/FileRemovedEventHandler.ts @@ -1,5 +1,5 @@ import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events' -import { SubscriptionSettingName } from '@standardnotes/settings' +import { SettingName } from '@standardnotes/settings' import { inject, injectable } from 'inversify' import { Logger } from 'winston' @@ -38,7 +38,7 @@ export class FileRemovedEventHandler implements DomainEventHandlerInterface { const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ userUuid: user.uuid, userSubscriptionUuid: subscription.uuid, - subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed, + subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(), }) if (bytesUsedSetting === null) { this.logger.warn(`Could not find bytes used setting for user with uuid: ${user.uuid}`) @@ -51,7 +51,7 @@ export class FileRemovedEventHandler implements DomainEventHandlerInterface { await this.subscriptionSettingService.createOrReplace({ userSubscription: subscription, props: { - name: SubscriptionSettingName.FileUploadBytesUsed, + name: SettingName.NAMES.FileUploadBytesUsed, unencryptedValue: (+bytesUsed - byteSize).toString(), sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, diff --git a/packages/auth/src/Domain/Handler/FileUploadedEventHandler.ts b/packages/auth/src/Domain/Handler/FileUploadedEventHandler.ts index 714a8dc04..542a6435f 100644 --- a/packages/auth/src/Domain/Handler/FileUploadedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/FileUploadedEventHandler.ts @@ -1,5 +1,5 @@ import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events' -import { SubscriptionSettingName } from '@standardnotes/settings' +import { SettingName } from '@standardnotes/settings' import { inject, injectable } from 'inversify' import { Logger } from 'winston' @@ -47,7 +47,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface { const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ userUuid: (await subscription.user).uuid, userSubscriptionUuid: subscription.uuid, - subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed, + subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(), }) if (bytesUsedSetting !== null) { bytesUsed = bytesUsedSetting.value as string @@ -56,7 +56,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface { await this.subscriptionSettingService.createOrReplace({ userSubscription: subscription, props: { - name: SubscriptionSettingName.FileUploadBytesUsed, + name: SettingName.NAMES.FileUploadBytesUsed, unencryptedValue: (+bytesUsed + byteSize).toString(), sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, diff --git a/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.ts b/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.ts index 1ab394497..c157134e4 100644 --- a/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/ListedAccountCreatedEventHandler.ts @@ -28,7 +28,7 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter let authSecrets: ListedAuthorSecretsData = [newSecret] const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.ListedAuthorSecrets, + settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(), userUuid: user.uuid, }) if (listedAuthorSecretsSetting !== null) { @@ -40,7 +40,7 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter await this.settingService.createOrReplace({ user, props: { - name: SettingName.ListedAuthorSecrets, + name: SettingName.NAMES.ListedAuthorSecrets, unencryptedValue: JSON.stringify(authSecrets), sensitive: false, }, diff --git a/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.ts b/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.ts index 945a73728..1d3bdf232 100644 --- a/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/ListedAccountDeletedEventHandler.ts @@ -24,7 +24,7 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter } const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.ListedAuthorSecrets, + settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(), userUuid: user.uuid, }) if (listedAuthorSecretsSetting === null) { @@ -43,7 +43,7 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter await this.settingService.createOrReplace({ user, props: { - name: SettingName.ListedAuthorSecrets, + name: SettingName.NAMES.ListedAuthorSecrets, unencryptedValue: JSON.stringify(filteredSecrets), sensitive: false, }, diff --git a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts b/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts index e4f22aeaa..fc46d9bf1 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts @@ -47,7 +47,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt await this.settingService.createOrReplace({ user, props: { - name: SettingName.ExtensionKey, + name: SettingName.NAMES.ExtensionKey, unencryptedValue: event.payload.extensionKey, serverEncryptionVersion: EncryptionVersion.Default, sensitive: true, diff --git a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts b/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts index fb0cfbbd4..f55b2c2da 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts @@ -95,7 +95,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler await this.settingService.createOrReplace({ user, props: { - name: SettingName.ExtensionKey, + name: SettingName.NAMES.ExtensionKey, unencryptedValue: event.payload.extensionKey, serverEncryptionVersion: EncryptionVersion.Default, sensitive: true, diff --git a/packages/auth/src/Domain/Session/SessionService.ts b/packages/auth/src/Domain/Session/SessionService.ts index 4fe335149..dfc55142f 100644 --- a/packages/auth/src/Domain/Session/SessionService.ts +++ b/packages/auth/src/Domain/Session/SessionService.ts @@ -311,7 +311,7 @@ export class SessionService implements SessionServiceInterface { private async isLoggingUserAgentEnabledOnSessions(user: User): Promise { const loggingSetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.LogSessionUserAgent, + settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(), userUuid: user.uuid, }) diff --git a/packages/auth/src/Domain/Setting/FindSubscriptionSettingDTO.ts b/packages/auth/src/Domain/Setting/FindSubscriptionSettingDTO.ts index e89f51786..2911aef85 100644 --- a/packages/auth/src/Domain/Setting/FindSubscriptionSettingDTO.ts +++ b/packages/auth/src/Domain/Setting/FindSubscriptionSettingDTO.ts @@ -1,8 +1,8 @@ -import { SubscriptionSettingName } from '@standardnotes/settings' +import { SettingName } from '@standardnotes/settings' export type FindSubscriptionSettingDTO = { userUuid: string userSubscriptionUuid: string - subscriptionSettingName: SubscriptionSettingName + subscriptionSettingName: SettingName settingUuid?: string } diff --git a/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts b/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts index c3b0af950..bc890266b 100644 --- a/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts +++ b/packages/auth/src/Domain/Setting/SettingInterpreter.spec.ts @@ -70,12 +70,11 @@ describe('SettingInterpreter', () => { }) it('should trigger session cleanup if user is disabling session user agent logging', async () => { - const setting = { - name: SettingName.LogSessionUserAgent, - value: LogSessionUserAgentOption.Disabled, - } as jest.Mocked - - await createInterpreter().interpretSettingUpdated(setting, user, LogSessionUserAgentOption.Disabled) + await createInterpreter().interpretSettingUpdated( + SettingName.NAMES.LogSessionUserAgent, + user, + LogSessionUserAgentOption.Disabled, + ) expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent).toHaveBeenCalledWith({ @@ -85,55 +84,50 @@ describe('SettingInterpreter', () => { }) it('should trigger backup if email backup setting is created - emails not muted', async () => { - const setting = { - name: SettingName.EmailBackupFrequency, - value: EmailBackupFrequency.Daily, - } as jest.Mocked - - await createInterpreter().interpretSettingUpdated(setting, user, EmailBackupFrequency.Daily) + 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 = { - name: SettingName.EmailBackupFrequency, - value: EmailBackupFrequency.Daily, - } as jest.Mocked settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({ - name: SettingName.MuteFailedBackupsEmails, + name: SettingName.NAMES.MuteFailedBackupsEmails, uuid: '6-7-8', value: 'muted', } as jest.Mocked) - await createInterpreter().interpretSettingUpdated(setting, user, EmailBackupFrequency.Daily) + await createInterpreter().interpretSettingUpdated( + SettingName.NAMES.EmailBackupFrequency, + user, + EmailBackupFrequency.Daily, + ) expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '6-7-8', true) }) it('should not trigger backup if email backup setting is disabled', async () => { - const setting = { - name: SettingName.EmailBackupFrequency, - value: EmailBackupFrequency.Disabled, - } as jest.Mocked settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) - await createInterpreter().interpretSettingUpdated(setting, user, EmailBackupFrequency.Disabled) + await createInterpreter().interpretSettingUpdated( + SettingName.NAMES.EmailBackupFrequency, + user, + EmailBackupFrequency.Disabled, + ) expect(domainEventPublisher.publish).not.toHaveBeenCalled() expect(domainEventFactory.createEmailBackupRequestedEvent).not.toHaveBeenCalled() }) it('should trigger cloud backup if dropbox backup setting is created', async () => { - const setting = { - name: SettingName.DropboxBackupToken, - value: 'test-token', - } as jest.Mocked settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) - await createInterpreter().interpretSettingUpdated(setting, user, 'test-token') + await createInterpreter().interpretSettingUpdated(SettingName.NAMES.DropboxBackupToken, user, 'test-token') expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith( @@ -146,17 +140,13 @@ describe('SettingInterpreter', () => { }) it('should trigger cloud backup if dropbox backup setting is created - muted emails', async () => { - const setting = { - name: SettingName.DropboxBackupToken, - value: 'test-token', - } as jest.Mocked settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({ - name: SettingName.MuteFailedCloudBackupsEmails, + name: SettingName.NAMES.MuteFailedCloudBackupsEmails, uuid: '6-7-8', value: 'muted', } as jest.Mocked) - await createInterpreter().interpretSettingUpdated(setting, user, 'test-token') + await createInterpreter().interpretSettingUpdated(SettingName.NAMES.DropboxBackupToken, user, 'test-token') expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith( @@ -169,13 +159,9 @@ describe('SettingInterpreter', () => { }) it('should trigger cloud backup if google drive backup setting is created', async () => { - const setting = { - name: SettingName.GoogleDriveBackupToken, - value: 'test-token', - } as jest.Mocked settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) - await createInterpreter().interpretSettingUpdated(setting, user, 'test-token') + await createInterpreter().interpretSettingUpdated(SettingName.NAMES.GoogleDriveBackupToken, user, 'test-token') expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith( @@ -188,13 +174,9 @@ describe('SettingInterpreter', () => { }) it('should trigger cloud backup if one drive backup setting is created', async () => { - const setting = { - name: SettingName.OneDriveBackupToken, - value: 'test-token', - } as jest.Mocked settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) - await createInterpreter().interpretSettingUpdated(setting, user, 'test-token') + await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupToken, user, 'test-token') expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith( @@ -207,13 +189,13 @@ describe('SettingInterpreter', () => { }) it('should trigger mute subscription emails rejection if mute setting changed', async () => { - const setting = { - name: SettingName.MuteMarketingEmails, - value: MuteMarketingEmailsOption.Muted, - } as jest.Mocked settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) - await createInterpreter().interpretSettingUpdated(setting, user, MuteMarketingEmailsOption.Muted) + await createInterpreter().interpretSettingUpdated( + SettingName.NAMES.MuteMarketingEmails, + user, + MuteMarketingEmailsOption.Muted, + ) expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventFactory.createMuteEmailsSettingChangedEvent).toHaveBeenCalledWith({ @@ -225,19 +207,13 @@ describe('SettingInterpreter', () => { it('should trigger cloud backup if backup frequency setting is updated and a backup token setting is present', async () => { settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({ - name: SettingName.OneDriveBackupToken, + name: SettingName.NAMES.OneDriveBackupToken, serverEncryptionVersion: 1, value: 'encrypted-backup-token', sensitive: true, } as jest.Mocked) - const setting = { - name: SettingName.OneDriveBackupFrequency, - serverEncryptionVersion: 0, - value: 'daily', - sensitive: false, - } as jest.Mocked - await createInterpreter().interpretSettingUpdated(setting, user, 'daily') + await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupFrequency, user, 'daily') expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith( @@ -251,19 +227,17 @@ describe('SettingInterpreter', () => { it('should not trigger cloud backup if backup frequency setting is updated as disabled', async () => { settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({ - name: SettingName.OneDriveBackupToken, + name: SettingName.NAMES.OneDriveBackupToken, serverEncryptionVersion: 1, value: 'encrypted-backup-token', sensitive: true, } as jest.Mocked) - const setting = { - name: SettingName.OneDriveBackupFrequency, - serverEncryptionVersion: 0, - value: OneDriveBackupFrequency.Disabled, - sensitive: false, - } as jest.Mocked - await createInterpreter().interpretSettingUpdated(setting, user, OneDriveBackupFrequency.Disabled) + await createInterpreter().interpretSettingUpdated( + SettingName.NAMES.OneDriveBackupFrequency, + user, + OneDriveBackupFrequency.Disabled, + ) expect(domainEventPublisher.publish).not.toHaveBeenCalled() expect(domainEventFactory.createCloudBackupRequestedEvent).not.toHaveBeenCalled() @@ -271,14 +245,8 @@ describe('SettingInterpreter', () => { it('should not trigger cloud backup if backup frequency setting is updated and a backup token setting is not present', async () => { settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce(null) - const setting = { - name: SettingName.OneDriveBackupFrequency, - serverEncryptionVersion: 0, - value: 'daily', - sensitive: false, - } as jest.Mocked - await createInterpreter().interpretSettingUpdated(setting, user, 'daily') + await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupFrequency, user, 'daily') expect(domainEventPublisher.publish).not.toHaveBeenCalled() expect(domainEventFactory.createCloudBackupRequestedEvent).not.toHaveBeenCalled() diff --git a/packages/auth/src/Domain/Setting/SettingInterpreter.ts b/packages/auth/src/Domain/Setting/SettingInterpreter.ts index 12df19e8d..375e61b91 100644 --- a/packages/auth/src/Domain/Setting/SettingInterpreter.ts +++ b/packages/auth/src/Domain/Setting/SettingInterpreter.ts @@ -15,7 +15,6 @@ import { Logger } from 'winston' import TYPES from '../../Bootstrap/Types' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { User } from '../User/User' -import { Setting } from './Setting' import { SettingDecrypterInterface } from './SettingDecrypterInterface' import { SettingInterpreterInterface } from './SettingInterpreterInterface' import { SettingRepositoryInterface } from './SettingRepositoryInterface' @@ -23,15 +22,15 @@ import { SettingRepositoryInterface } from './SettingRepositoryInterface' @injectable() export class SettingInterpreter implements SettingInterpreterInterface { private readonly cloudBackupTokenSettings = [ - SettingName.DropboxBackupToken, - SettingName.GoogleDriveBackupToken, - SettingName.OneDriveBackupToken, + SettingName.NAMES.DropboxBackupToken, + SettingName.NAMES.GoogleDriveBackupToken, + SettingName.NAMES.OneDriveBackupToken, ] private readonly cloudBackupFrequencySettings = [ - SettingName.DropboxBackupFrequency, - SettingName.GoogleDriveBackupFrequency, - SettingName.OneDriveBackupFrequency, + SettingName.NAMES.DropboxBackupFrequency, + SettingName.NAMES.GoogleDriveBackupFrequency, + SettingName.NAMES.OneDriveBackupFrequency, ] private readonly cloudBackupFrequencyDisabledValues = [ @@ -40,11 +39,11 @@ export class SettingInterpreter implements SettingInterpreterInterface { OneDriveBackupFrequency.Disabled, ] - private readonly emailSettingToSubscriptionRejectionLevelMap: Map = new Map([ - [SettingName.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup], - [SettingName.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup], - [SettingName.MuteMarketingEmails, EmailLevel.LEVELS.Marketing], - [SettingName.MuteSignInEmails, EmailLevel.LEVELS.SignIn], + 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( @@ -55,20 +54,24 @@ export class SettingInterpreter implements SettingInterpreterInterface { @inject(TYPES.Logger) private logger: Logger, ) {} - async interpretSettingUpdated(updatedSetting: Setting, user: User, unencryptedValue: string | null): Promise { - if (this.isChangingMuteEmailsSetting(updatedSetting)) { - await this.triggerEmailSubscriptionChange(user, updatedSetting.name as SettingName, unencryptedValue) + async interpretSettingUpdated( + updatedSettingName: string, + user: User, + unencryptedValue: string | null, + ): Promise { + if (this.isChangingMuteEmailsSetting(updatedSettingName)) { + await this.triggerEmailSubscriptionChange(user, updatedSettingName, unencryptedValue) } - if (this.isEnablingEmailBackupSetting(updatedSetting)) { + if (this.isEnablingEmailBackupSetting(updatedSettingName, unencryptedValue)) { await this.triggerEmailBackup(user.uuid) } - if (this.isEnablingCloudBackupSetting(updatedSetting)) { - await this.triggerCloudBackup(updatedSetting, user.uuid, unencryptedValue) + if (this.isEnablingCloudBackupSetting(updatedSettingName, unencryptedValue)) { + await this.triggerCloudBackup(updatedSettingName, user.uuid, unencryptedValue) } - if (this.isDisablingSessionUserAgentLogging(updatedSetting)) { + if (this.isDisablingSessionUserAgentLogging(updatedSettingName, unencryptedValue)) { await this.triggerSessionUserAgentCleanup(user) } } @@ -77,7 +80,7 @@ export class SettingInterpreter implements SettingInterpreterInterface { let userHasEmailsMuted = false let muteEmailsSettingUuid = '' const muteFailedEmailsBackupSetting = await this.settingRepository.findOneByNameAndUserUuid( - SettingName.MuteFailedBackupsEmails, + SettingName.NAMES.MuteFailedBackupsEmails, userUuid, ) if (muteFailedEmailsBackupSetting !== null) { @@ -90,36 +93,39 @@ export class SettingInterpreter implements SettingInterpreterInterface { ) } - private isChangingMuteEmailsSetting(setting: Setting): boolean { + private isChangingMuteEmailsSetting(settingName: string): boolean { return [ - SettingName.MuteFailedBackupsEmails, - SettingName.MuteFailedCloudBackupsEmails, - SettingName.MuteMarketingEmails, - SettingName.MuteSignInEmails, - ].includes(setting.name as SettingName) + SettingName.NAMES.MuteFailedBackupsEmails, + SettingName.NAMES.MuteFailedCloudBackupsEmails, + SettingName.NAMES.MuteMarketingEmails, + SettingName.NAMES.MuteSignInEmails, + ].includes(settingName) } - private isEnablingEmailBackupSetting(setting: Setting): boolean { - return setting.name === SettingName.EmailBackupFrequency && setting.value !== EmailBackupFrequency.Disabled - } - - private isEnablingCloudBackupSetting(setting: Setting): boolean { + private isEnablingEmailBackupSetting(settingName: string, newValue: string | null): boolean { return ( - (this.cloudBackupFrequencySettings.includes(setting.name as SettingName) || - this.cloudBackupTokenSettings.includes(setting.name as SettingName)) && + settingName === SettingName.NAMES.EmailBackupFrequency && + [EmailBackupFrequency.Daily, EmailBackupFrequency.Weekly].includes(newValue as EmailBackupFrequency) + ) + } + + private isEnablingCloudBackupSetting(settingName: string, newValue: string | null): boolean { + return ( + (this.cloudBackupFrequencySettings.includes(settingName) || + this.cloudBackupTokenSettings.includes(settingName)) && !this.cloudBackupFrequencyDisabledValues.includes( - setting.value as DropboxBackupFrequency | OneDriveBackupFrequency | GoogleDriveBackupFrequency, + newValue as DropboxBackupFrequency | OneDriveBackupFrequency | GoogleDriveBackupFrequency, ) ) } - private isDisablingSessionUserAgentLogging(setting: Setting): boolean { - return SettingName.LogSessionUserAgent === setting.name && LogSessionUserAgentOption.Disabled === setting.value + private isDisablingSessionUserAgentLogging(settingName: string, newValue: string | null): boolean { + return SettingName.NAMES.LogSessionUserAgent === settingName && LogSessionUserAgentOption.Disabled === newValue } private async triggerEmailSubscriptionChange( user: User, - settingName: SettingName, + settingName: string, unencryptedValue: string | null, ): Promise { await this.domainEventPublisher.publish( @@ -140,33 +146,34 @@ export class SettingInterpreter implements SettingInterpreterInterface { ) } - private async triggerCloudBackup(setting: Setting, userUuid: string, unencryptedValue: string | null): Promise { + private async triggerCloudBackup( + settingName: string, + userUuid: string, + unencryptedValue: string | null, + ): Promise { let cloudProvider let tokenSettingName - switch (setting.name) { - case SettingName.DropboxBackupToken: - case SettingName.DropboxBackupFrequency: + switch (settingName) { + case SettingName.NAMES.DropboxBackupToken: + case SettingName.NAMES.DropboxBackupFrequency: cloudProvider = 'DROPBOX' - tokenSettingName = SettingName.DropboxBackupToken + tokenSettingName = SettingName.NAMES.DropboxBackupToken break - case SettingName.GoogleDriveBackupToken: - case SettingName.GoogleDriveBackupFrequency: + case SettingName.NAMES.GoogleDriveBackupToken: + case SettingName.NAMES.GoogleDriveBackupFrequency: cloudProvider = 'GOOGLE_DRIVE' - tokenSettingName = SettingName.GoogleDriveBackupToken + tokenSettingName = SettingName.NAMES.GoogleDriveBackupToken break - case SettingName.OneDriveBackupToken: - case SettingName.OneDriveBackupFrequency: + case SettingName.NAMES.OneDriveBackupToken: + case SettingName.NAMES.OneDriveBackupFrequency: cloudProvider = 'ONE_DRIVE' - tokenSettingName = SettingName.OneDriveBackupToken + tokenSettingName = SettingName.NAMES.OneDriveBackupToken break } let backupToken = null - if (this.cloudBackupFrequencySettings.includes(setting.name as SettingName)) { - const tokenSetting = await this.settingRepository.findLastByNameAndUserUuid( - tokenSettingName as SettingName, - userUuid, - ) + if (this.cloudBackupFrequencySettings.includes(settingName)) { + const tokenSetting = await this.settingRepository.findLastByNameAndUserUuid(tokenSettingName as string, userUuid) if (tokenSetting !== null) { backupToken = await this.settingDecrypter.decryptSettingValue(tokenSetting, userUuid) } @@ -183,7 +190,7 @@ export class SettingInterpreter implements SettingInterpreterInterface { let userHasEmailsMuted = false let muteEmailsSettingUuid = '' const muteFailedCloudBackupSetting = await this.settingRepository.findOneByNameAndUserUuid( - SettingName.MuteFailedCloudBackupsEmails, + SettingName.NAMES.MuteFailedCloudBackupsEmails, userUuid, ) if (muteFailedCloudBackupSetting !== null) { diff --git a/packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts b/packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts index 5f1440c18..5288fded6 100644 --- a/packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts +++ b/packages/auth/src/Domain/Setting/SettingInterpreterInterface.ts @@ -1,6 +1,5 @@ import { User } from '../User/User' -import { Setting } from './Setting' export interface SettingInterpreterInterface { - interpretSettingUpdated(updatedSetting: Setting, user: User, newUnencryptedValue: string | null): Promise + interpretSettingUpdated(updatedSettingName: string, user: User, newUnencryptedValue: string | null): Promise } diff --git a/packages/auth/src/Domain/Setting/SettingService.spec.ts b/packages/auth/src/Domain/Setting/SettingService.spec.ts index 6888ef895..adb07dbfd 100644 --- a/packages/auth/src/Domain/Setting/SettingService.spec.ts +++ b/packages/auth/src/Domain/Setting/SettingService.spec.ts @@ -39,7 +39,9 @@ describe('SettingService', () => { } as jest.Mocked user.isPotentiallyAVaultAccount = jest.fn().mockReturnValue(false) - setting = {} as jest.Mocked + setting = { + name: SettingName.NAMES.DropboxBackupToken, + } as jest.Mocked factory = {} as jest.Mocked factory.create = jest.fn().mockReturnValue(setting) @@ -54,7 +56,7 @@ describe('SettingService', () => { settingsAssociationService.getDefaultSettingsAndValuesForNewUser = jest.fn().mockReturnValue( new Map([ [ - SettingName.MuteSignInEmails, + SettingName.NAMES.MuteSignInEmails, { value: MuteSignInEmailsOption.NotMuted, sensitive: 0, @@ -67,7 +69,7 @@ describe('SettingService', () => { settingsAssociationService.getDefaultSettingsAndValuesForNewVaultAccount = jest.fn().mockReturnValue( new Map([ [ - SettingName.LogSessionUserAgent, + SettingName.NAMES.LogSessionUserAgent, { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, @@ -107,7 +109,7 @@ describe('SettingService', () => { const result = await createService().createOrReplace({ user, props: { - name: 'name', + name: SettingName.NAMES.MuteFailedBackupsEmails, unencryptedValue: 'value', serverEncryptionVersion: 1, sensitive: false, @@ -117,6 +119,20 @@ describe('SettingService', () => { expect(result.status).toEqual('created') }) + it('should throw error if setting name is not valid', async () => { + await expect( + createService().createOrReplace({ + user, + props: { + name: 'invalid', + unencryptedValue: 'value', + serverEncryptionVersion: 1, + sensitive: false, + }, + }), + ).rejects.toThrowError('Invalid setting name: invalid') + }) + it('should create setting with a given uuid if it does not exist', async () => { settingRepository.findOneByUuid = jest.fn().mockReturnValue(null) @@ -124,7 +140,7 @@ describe('SettingService', () => { user, props: { uuid: '1-2-3', - name: 'name', + name: SettingName.NAMES.MuteFailedBackupsEmails, unencryptedValue: 'value', serverEncryptionVersion: 1, sensitive: false, @@ -174,7 +190,10 @@ describe('SettingService', () => { settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting) expect( - await createService().findSettingWithDecryptedValue({ userUuid: '1-2-3', settingName: 'test' as SettingName }), + await createService().findSettingWithDecryptedValue({ + userUuid: '1-2-3', + settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(), + }), ).toEqual({ serverEncryptionVersion: 1, value: 'decrypted', diff --git a/packages/auth/src/Domain/Setting/SettingService.ts b/packages/auth/src/Domain/Setting/SettingService.ts index 09a1b76e4..7460c8ca4 100644 --- a/packages/auth/src/Domain/Setting/SettingService.ts +++ b/packages/auth/src/Domain/Setting/SettingService.ts @@ -57,7 +57,7 @@ export class SettingService implements SettingServiceInterface { if (dto.settingUuid !== undefined) { setting = await this.settingRepository.findOneByUuid(dto.settingUuid) } else { - setting = await this.settingRepository.findLastByNameAndUserUuid(dto.settingName, dto.userUuid) + setting = await this.settingRepository.findLastByNameAndUserUuid(dto.settingName.value, dto.userUuid) } if (setting === null) { @@ -72,9 +72,15 @@ export class SettingService implements SettingServiceInterface { async createOrReplace(dto: CreateOrReplaceSettingDto): Promise { const { user, props } = dto + const settingNameOrError = SettingName.create(props.name) + if (settingNameOrError.isFailed()) { + throw new Error(settingNameOrError.getError()) + } + const settingName = settingNameOrError.getValue() + const existing = await this.findSettingWithDecryptedValue({ userUuid: user.uuid, - settingName: props.name as SettingName, + settingName, settingUuid: props.uuid, }) @@ -83,7 +89,7 @@ export class SettingService implements SettingServiceInterface { this.logger.debug('[%s] Created setting %s: %O', user.uuid, props.name, setting) - await this.settingInterpreter.interpretSettingUpdated(setting, user, props.unencryptedValue) + await this.settingInterpreter.interpretSettingUpdated(setting.name, user, props.unencryptedValue) return { status: 'created', @@ -95,7 +101,7 @@ export class SettingService implements SettingServiceInterface { this.logger.debug('[%s] Replaced existing setting %s with: %O', user.uuid, props.name, setting) - await this.settingInterpreter.interpretSettingUpdated(setting, user, props.unencryptedValue) + await this.settingInterpreter.interpretSettingUpdated(setting.name, user, props.unencryptedValue) return { status: 'replaced', diff --git a/packages/auth/src/Domain/Setting/SettingsAssociationService.spec.ts b/packages/auth/src/Domain/Setting/SettingsAssociationService.spec.ts index 2748072a7..9f02b8645 100644 --- a/packages/auth/src/Domain/Setting/SettingsAssociationService.spec.ts +++ b/packages/auth/src/Domain/Setting/SettingsAssociationService.spec.ts @@ -11,52 +11,68 @@ describe('SettingsAssociationService', () => { const createService = () => new SettingsAssociationService() it('should tell if a setting is mutable by the client', () => { - expect(createService().isSettingMutableByClient(SettingName.DropboxBackupFrequency)).toBeTruthy() + expect( + createService().isSettingMutableByClient(SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue()), + ).toBeTruthy() }) it('should tell if a setting is immutable by the client', () => { - expect(createService().isSettingMutableByClient(SettingName.ListedAuthorSecrets)).toBeFalsy() + expect( + createService().isSettingMutableByClient(SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue()), + ).toBeFalsy() }) it('should return default encryption version for a setting which enecryption version is not strictly defined', () => { - expect(createService().getEncryptionVersionForSetting(SettingName.MfaSecret)).toEqual(EncryptionVersion.Default) + expect( + createService().getEncryptionVersionForSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue()), + ).toEqual(EncryptionVersion.Default) }) it('should return a defined encryption version for a setting which enecryption version is strictly defined', () => { - expect(createService().getEncryptionVersionForSetting(SettingName.EmailBackupFrequency)).toEqual( - EncryptionVersion.Unencrypted, - ) + expect( + createService().getEncryptionVersionForSetting( + SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue(), + ), + ).toEqual(EncryptionVersion.Unencrypted) }) it('should return default sensitivity for a setting which sensitivity is not strictly defined', () => { - expect(createService().getSensitivityForSetting(SettingName.DropboxBackupToken)).toBeTruthy() + expect( + createService().getSensitivityForSetting(SettingName.create(SettingName.NAMES.DropboxBackupToken).getValue()), + ).toBeTruthy() }) it('should return a defined sensitivity for a setting which sensitivity is strictly defined', () => { - expect(createService().getSensitivityForSetting(SettingName.DropboxBackupFrequency)).toBeFalsy() + expect( + createService().getSensitivityForSetting(SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue()), + ).toBeFalsy() }) it('should return the default set of settings for a newly registered user', () => { const settings = createService().getDefaultSettingsAndValuesForNewUser() - const flatSettings = [...(settings as Map).keys()] - expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT']) + const flatSettings = [...(settings as Map).keys()] + expect(flatSettings).toEqual(['MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT']) }) it('should return the default set of settings for a newly registered vault account', () => { const settings = createService().getDefaultSettingsAndValuesForNewVaultAccount() - const flatSettings = [...(settings as Map).keys()] - expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT']) + const flatSettings = [...(settings as Map).keys()] + expect(flatSettings).toEqual(['MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT']) - expect(settings.get(SettingName.LogSessionUserAgent)?.value).toEqual('disabled') + expect(settings.get(SettingName.NAMES.LogSessionUserAgent)?.value).toEqual('disabled') }) it('should return a permission name associated to a given setting', () => { - expect(createService().getPermissionAssociatedWithSetting(SettingName.EmailBackupFrequency)).toEqual( - PermissionName.DailyEmailBackup, - ) + expect( + createService().getPermissionAssociatedWithSetting( + SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue(), + ), + ).toEqual(PermissionName.DailyEmailBackup) }) it('should not return a permission name if not associated to a given setting', () => { - expect(createService().getPermissionAssociatedWithSetting(SettingName.ExtensionKey)).toBeUndefined() + expect( + createService().getPermissionAssociatedWithSetting(SettingName.create(SettingName.NAMES.ExtensionKey).getValue()), + ).toBeUndefined() }) }) diff --git a/packages/auth/src/Domain/Setting/SettingsAssociationService.ts b/packages/auth/src/Domain/Setting/SettingsAssociationService.ts index d1018f356..8ecdf651a 100644 --- a/packages/auth/src/Domain/Setting/SettingsAssociationService.ts +++ b/packages/auth/src/Domain/Setting/SettingsAssociationService.ts @@ -1,10 +1,5 @@ import { PermissionName } from '@standardnotes/features' -import { - LogSessionUserAgentOption, - MuteMarketingEmailsOption, - MuteSignInEmailsOption, - SettingName, -} from '@standardnotes/settings' +import { LogSessionUserAgentOption, MuteMarketingEmailsOption, SettingName } from '@standardnotes/settings' import { injectable } from 'inversify' import { EncryptionVersion } from '../Encryption/EncryptionVersion' @@ -15,49 +10,44 @@ import { SettingsAssociationServiceInterface } from './SettingsAssociationServic @injectable() export class SettingsAssociationService implements SettingsAssociationServiceInterface { private readonly UNENCRYPTED_SETTINGS = [ - SettingName.EmailBackupFrequency, - SettingName.MuteFailedBackupsEmails, - SettingName.MuteFailedCloudBackupsEmails, - SettingName.MuteSignInEmails, - SettingName.MuteMarketingEmails, - SettingName.DropboxBackupFrequency, - SettingName.GoogleDriveBackupFrequency, - SettingName.OneDriveBackupFrequency, - SettingName.LogSessionUserAgent, + SettingName.NAMES.EmailBackupFrequency, + SettingName.NAMES.MuteFailedBackupsEmails, + SettingName.NAMES.MuteFailedCloudBackupsEmails, + SettingName.NAMES.MuteSignInEmails, + SettingName.NAMES.MuteMarketingEmails, + SettingName.NAMES.DropboxBackupFrequency, + SettingName.NAMES.GoogleDriveBackupFrequency, + SettingName.NAMES.OneDriveBackupFrequency, + SettingName.NAMES.LogSessionUserAgent, ] private readonly UNSENSITIVE_SETTINGS = [ - SettingName.DropboxBackupFrequency, - SettingName.GoogleDriveBackupFrequency, - SettingName.OneDriveBackupFrequency, - SettingName.EmailBackupFrequency, - SettingName.MuteFailedBackupsEmails, - SettingName.MuteFailedCloudBackupsEmails, - SettingName.MuteSignInEmails, - SettingName.MuteMarketingEmails, - SettingName.ListedAuthorSecrets, - SettingName.LogSessionUserAgent, + SettingName.NAMES.DropboxBackupFrequency, + SettingName.NAMES.GoogleDriveBackupFrequency, + SettingName.NAMES.OneDriveBackupFrequency, + SettingName.NAMES.EmailBackupFrequency, + SettingName.NAMES.MuteFailedBackupsEmails, + SettingName.NAMES.MuteFailedCloudBackupsEmails, + SettingName.NAMES.MuteSignInEmails, + SettingName.NAMES.MuteMarketingEmails, + SettingName.NAMES.ListedAuthorSecrets, + SettingName.NAMES.LogSessionUserAgent, ] - private readonly CLIENT_IMMUTABLE_SETTINGS = [SettingName.ListedAuthorSecrets] + private readonly CLIENT_IMMUTABLE_SETTINGS = [ + SettingName.NAMES.ListedAuthorSecrets, + SettingName.NAMES.FileUploadBytesLimit, + SettingName.NAMES.FileUploadBytesUsed, + ] - private readonly permissionsAssociatedWithSettings = new Map([ - [SettingName.EmailBackupFrequency, PermissionName.DailyEmailBackup], - [SettingName.MuteSignInEmails, PermissionName.SignInAlerts], + private readonly permissionsAssociatedWithSettings = new Map([ + [SettingName.NAMES.EmailBackupFrequency, PermissionName.DailyEmailBackup], + [SettingName.NAMES.MuteSignInEmails, PermissionName.SignInAlerts], ]) - private readonly defaultSettings = new Map([ + private readonly defaultSettings = new Map([ [ - SettingName.MuteSignInEmails, - { - sensitive: false, - serverEncryptionVersion: EncryptionVersion.Unencrypted, - value: MuteSignInEmailsOption.Muted, - replaceable: false, - }, - ], - [ - SettingName.MuteMarketingEmails, + SettingName.NAMES.MuteMarketingEmails, { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, @@ -66,7 +56,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt }, ], [ - SettingName.LogSessionUserAgent, + SettingName.NAMES.LogSessionUserAgent, { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, @@ -76,9 +66,9 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt ], ]) - private readonly vaultAccountDefaultSettingsOverwrites = new Map([ + private readonly vaultAccountDefaultSettingsOverwrites = new Map([ [ - SettingName.LogSessionUserAgent, + SettingName.NAMES.LogSessionUserAgent, { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, @@ -89,7 +79,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt ]) isSettingMutableByClient(settingName: SettingName): boolean { - if (this.CLIENT_IMMUTABLE_SETTINGS.includes(settingName)) { + if (this.CLIENT_IMMUTABLE_SETTINGS.includes(settingName.value)) { return false } @@ -97,7 +87,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt } getSensitivityForSetting(settingName: SettingName): boolean { - if (this.UNSENSITIVE_SETTINGS.includes(settingName)) { + if (this.UNSENSITIVE_SETTINGS.includes(settingName.value)) { return false } @@ -105,7 +95,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt } getEncryptionVersionForSetting(settingName: SettingName): EncryptionVersion { - if (this.UNENCRYPTED_SETTINGS.includes(settingName)) { + if (this.UNENCRYPTED_SETTINGS.includes(settingName.value)) { return EncryptionVersion.Unencrypted } @@ -113,18 +103,18 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt } getPermissionAssociatedWithSetting(settingName: SettingName): PermissionName | undefined { - if (!this.permissionsAssociatedWithSettings.has(settingName)) { + if (!this.permissionsAssociatedWithSettings.has(settingName.value)) { return undefined } - return this.permissionsAssociatedWithSettings.get(settingName) + return this.permissionsAssociatedWithSettings.get(settingName.value) } - getDefaultSettingsAndValuesForNewUser(): Map { + getDefaultSettingsAndValuesForNewUser(): Map { return this.defaultSettings } - getDefaultSettingsAndValuesForNewVaultAccount(): Map { + getDefaultSettingsAndValuesForNewVaultAccount(): Map { const defaultVaultSettings = new Map(this.defaultSettings) for (const vaultAccountDefaultSettingOverwriteKey of this.vaultAccountDefaultSettingsOverwrites.keys()) { diff --git a/packages/auth/src/Domain/Setting/SettingsAssociationServiceInterface.ts b/packages/auth/src/Domain/Setting/SettingsAssociationServiceInterface.ts index 3e7cce194..80a32b896 100644 --- a/packages/auth/src/Domain/Setting/SettingsAssociationServiceInterface.ts +++ b/packages/auth/src/Domain/Setting/SettingsAssociationServiceInterface.ts @@ -1,13 +1,14 @@ import { PermissionName } from '@standardnotes/features' -import { SettingName, SubscriptionSettingName } from '@standardnotes/settings' +import { SettingName } from '@standardnotes/settings' + import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { SettingDescription } from './SettingDescription' export interface SettingsAssociationServiceInterface { - getDefaultSettingsAndValuesForNewUser(): Map - getDefaultSettingsAndValuesForNewVaultAccount(): Map + getDefaultSettingsAndValuesForNewUser(): Map + getDefaultSettingsAndValuesForNewVaultAccount(): Map getPermissionAssociatedWithSetting(settingName: SettingName): PermissionName | undefined getEncryptionVersionForSetting(settingName: SettingName): EncryptionVersion getSensitivityForSetting(settingName: SettingName): boolean - isSettingMutableByClient(settingName: SettingName | SubscriptionSettingName): boolean + isSettingMutableByClient(settingName: SettingName): boolean } diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingRepositoryInterface.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingRepositoryInterface.ts index 374f15833..885a3353c 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingRepositoryInterface.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingRepositoryInterface.ts @@ -3,5 +3,6 @@ import { SubscriptionSetting } from './SubscriptionSetting' export interface SubscriptionSettingRepositoryInterface { findOneByUuid(uuid: string): Promise findLastByNameAndUserSubscriptionUuid(name: string, userSubscriptionUuid: string): Promise + findAllBySubscriptionUuid(userSubscriptionUuid: string): Promise save(subscriptionSetting: SubscriptionSetting): Promise } diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts index 2642077e7..8a311a074 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts @@ -1,6 +1,5 @@ import 'reflect-metadata' -import { SubscriptionSettingName } from '@standardnotes/settings' import { Logger } from 'winston' import { EncryptionVersion } from '../Encryption/EncryptionVersion' @@ -14,6 +13,8 @@ import { User } from '../User/User' import { SettingFactoryInterface } from './SettingFactoryInterface' import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface' import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' +import { SettingName } from '@standardnotes/settings' +import { SettingInterpreterInterface } from './SettingInterpreterInterface' describe('SubscriptionSettingService', () => { let setting: SubscriptionSetting @@ -22,6 +23,7 @@ describe('SubscriptionSettingService', () => { let factory: SettingFactoryInterface let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface let subscriptionSettingsAssociationService: SubscriptionSettingsAssociationServiceInterface + let settingInterpreter: SettingInterpreterInterface let settingDecrypter: SettingDecrypterInterface let userSubscriptionRepository: UserSubscriptionRepositoryInterface let logger: Logger @@ -31,6 +33,7 @@ describe('SubscriptionSettingService', () => { factory, subscriptionSettingRepository, subscriptionSettingsAssociationService, + settingInterpreter, settingDecrypter, userSubscriptionRepository, logger, @@ -44,7 +47,9 @@ describe('SubscriptionSettingService', () => { user: Promise.resolve(user), } as jest.Mocked - setting = {} as jest.Mocked + setting = { + name: SettingName.NAMES.FileUploadBytesUsed, + } as jest.Mocked factory = {} as jest.Mocked factory.createSubscriptionSetting = jest.fn().mockReturnValue(setting) @@ -68,7 +73,7 @@ describe('SubscriptionSettingService', () => { subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( new Map([ [ - SubscriptionSettingName.FileUploadBytesUsed, + SettingName.NAMES.FileUploadBytesUsed, { value: '0', sensitive: 0, @@ -79,6 +84,9 @@ describe('SubscriptionSettingService', () => { ]), ) + settingInterpreter = {} as jest.Mocked + settingInterpreter.interpretSettingUpdated = jest.fn() + settingDecrypter = {} as jest.Mocked settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted') @@ -98,11 +106,59 @@ describe('SubscriptionSettingService', () => { expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting) }) + it('should throw error if subscription setting is invalid', async () => { + subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( + new Map([ + [ + 'invalid', + { + value: '0', + sensitive: 0, + serverEncryptionVersion: EncryptionVersion.Unencrypted, + replaceable: true, + }, + ], + ]), + ) + + await expect( + createService().applyDefaultSubscriptionSettingsForSubscription( + userSubscription, + SubscriptionName.PlusPlan, + '1-2-3', + ), + ).rejects.toThrow() + }) + + it('should throw error if setting name is not a subscription setting when applying defaults', async () => { + subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( + new Map([ + [ + SettingName.NAMES.DropboxBackupFrequency, + { + value: '0', + sensitive: 0, + serverEncryptionVersion: EncryptionVersion.Unencrypted, + replaceable: false, + }, + ], + ]), + ) + + await expect( + createService().applyDefaultSubscriptionSettingsForSubscription( + userSubscription, + SubscriptionName.PlusPlan, + '1-2-3', + ), + ).rejects.toThrow() + }) + it('should reassign existing default settings for a subscription if it is not replaceable', async () => { subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( new Map([ [ - SubscriptionSettingName.FileUploadBytesUsed, + SettingName.NAMES.FileUploadBytesUsed, { value: '0', sensitive: 0, @@ -127,7 +183,7 @@ describe('SubscriptionSettingService', () => { subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( new Map([ [ - SubscriptionSettingName.FileUploadBytesUsed, + SettingName.NAMES.FileUploadBytesUsed, { value: '0', sensitive: 0, @@ -152,7 +208,7 @@ describe('SubscriptionSettingService', () => { subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( new Map([ [ - SubscriptionSettingName.FileUploadBytesUsed, + SettingName.NAMES.FileUploadBytesUsed, { value: '0', sensitive: 0, @@ -196,7 +252,7 @@ describe('SubscriptionSettingService', () => { const result = await createService().createOrReplace({ userSubscription, props: { - name: 'name', + name: SettingName.NAMES.FileUploadBytesLimit, unencryptedValue: 'value', serverEncryptionVersion: 1, sensitive: false, @@ -206,6 +262,34 @@ describe('SubscriptionSettingService', () => { expect(result.status).toEqual('created') }) + it('should throw error if the setting name is not valid', async () => { + await expect( + createService().createOrReplace({ + userSubscription, + props: { + name: 'invalid', + unencryptedValue: 'value', + serverEncryptionVersion: 1, + sensitive: false, + }, + }), + ).rejects.toThrow() + }) + + it('should throw error if the setting name is not a subscription setting', async () => { + await expect( + createService().createOrReplace({ + userSubscription, + props: { + name: SettingName.NAMES.DropboxBackupFrequency, + unencryptedValue: 'value', + serverEncryptionVersion: 1, + sensitive: false, + }, + }), + ).rejects.toThrow() + }) + it('should create setting with a given uuid if it does not exist', async () => { subscriptionSettingRepository.findOneByUuid = jest.fn().mockReturnValue(null) @@ -213,7 +297,7 @@ describe('SubscriptionSettingService', () => { userSubscription, props: { uuid: '1-2-3', - name: 'name', + name: SettingName.NAMES.FileUploadBytesLimit, unencryptedValue: 'value', serverEncryptionVersion: 1, sensitive: false, @@ -266,11 +350,21 @@ describe('SubscriptionSettingService', () => { await createService().findSubscriptionSettingWithDecryptedValue({ userSubscriptionUuid: '2-3-4', userUuid: '1-2-3', - subscriptionSettingName: 'test' as SubscriptionSettingName, + subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), }), ).toEqual({ serverEncryptionVersion: 1, value: 'decrypted', }) }) + + it('should throw error when trying to find and decrypt a setting with invalid subscription setting name', async () => { + await expect( + createService().findSubscriptionSettingWithDecryptedValue({ + userSubscriptionUuid: '2-3-4', + userUuid: '1-2-3', + subscriptionSettingName: SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue(), + }), + ).rejects.toThrow() + }) }) diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts index 17e7a21f1..f511189cc 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts @@ -1,5 +1,4 @@ import { SubscriptionName } from '@standardnotes/common' -import { SubscriptionSettingName } from '@standardnotes/settings' import { inject, injectable } from 'inversify' import { Logger } from 'winston' @@ -17,6 +16,8 @@ import { SubscriptionSettingRepositoryInterface } from './SubscriptionSettingRep import { SettingFactoryInterface } from './SettingFactoryInterface' import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface' import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' +import { SettingName } from '@standardnotes/settings' +import { SettingInterpreterInterface } from './SettingInterpreterInterface' @injectable() export class SubscriptionSettingService implements SubscriptionSettingServiceInterface { @@ -26,6 +27,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface, @inject(TYPES.SubscriptionSettingsAssociationService) private subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface, + @inject(TYPES.SettingInterpreter) private settingInterpreter: SettingInterpreterInterface, @inject(TYPES.SettingDecrypter) private settingDecrypter: SettingDecrypterInterface, @inject(TYPES.UserSubscriptionRepository) private userSubscriptionRepository: UserSubscriptionRepositoryInterface, @inject(TYPES.Logger) private logger: Logger, @@ -44,8 +46,17 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt return } - for (const settingName of defaultSettingsWithValues.keys()) { - const setting = defaultSettingsWithValues.get(settingName) as SettingDescription + for (const settingNameString of defaultSettingsWithValues.keys()) { + const settingNameOrError = SettingName.create(settingNameString) + if (settingNameOrError.isFailed()) { + throw new Error(settingNameOrError.getError()) + } + const settingName = settingNameOrError.getValue() + if (!settingName.isASubscriptionSetting()) { + throw new Error(`Setting ${settingName.value} is not a subscription setting`) + } + + const setting = defaultSettingsWithValues.get(settingName.value) as SettingDescription if (!setting.replaceable) { const existingSetting = await this.findPreviousSubscriptionSetting(settingName, userSubscription.uuid, userUuid) if (existingSetting !== null) { @@ -59,7 +70,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt await this.createOrReplace({ userSubscription, props: { - name: settingName, + name: settingName.value, unencryptedValue: setting.value, serverEncryptionVersion: setting.serverEncryptionVersion, sensitive: setting.sensitive, @@ -71,12 +82,16 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt async findSubscriptionSettingWithDecryptedValue( dto: FindSubscriptionSettingDTO, ): Promise { + if (!dto.subscriptionSettingName.isASubscriptionSetting()) { + throw new Error(`Setting ${dto.subscriptionSettingName.value} is not a subscription setting`) + } + let setting: SubscriptionSetting | null if (dto.settingUuid !== undefined) { setting = await this.subscriptionSettingRepository.findOneByUuid(dto.settingUuid) } else { setting = await this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid( - dto.subscriptionSettingName, + dto.subscriptionSettingName.value, dto.userSubscriptionUuid, ) } @@ -95,10 +110,21 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt ): Promise { const { userSubscription, props } = dto + const settingNameOrError = SettingName.create(props.name) + if (settingNameOrError.isFailed()) { + throw new Error(settingNameOrError.getError()) + } + const settingName = settingNameOrError.getValue() + + if (!settingName.isASubscriptionSetting()) { + throw new Error(`Setting ${settingName.value} is not a subscription setting`) + } + + const user = await userSubscription.user const existing = await this.findSubscriptionSettingWithDecryptedValue({ - userUuid: (await userSubscription.user).uuid, + userUuid: user.uuid, userSubscriptionUuid: userSubscription.uuid, - subscriptionSettingName: props.name as SubscriptionSettingName, + subscriptionSettingName: settingName, settingUuid: props.uuid, }) @@ -109,6 +135,8 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt this.logger.debug('Created subscription setting %s: %O', props.name, subscriptionSetting) + await this.settingInterpreter.interpretSettingUpdated(settingName.value, user, props.unencryptedValue) + return { status: 'created', subscriptionSetting, @@ -121,6 +149,8 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt this.logger.debug('Replaced existing subscription setting %s with: %O', props.name, subscriptionSetting) + await this.settingInterpreter.interpretSettingUpdated(settingName.value, user, props.unencryptedValue) + return { status: 'replaced', subscriptionSetting, @@ -128,7 +158,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt } private async findPreviousSubscriptionSetting( - settingName: SubscriptionSettingName, + settingName: SettingName, currentUserSubscriptionUuid: string, userUuid: string, ): Promise { @@ -142,6 +172,9 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt return null } - return this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid(settingName, lastSubscription.uuid) + return this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid( + settingName.value, + lastSubscription.uuid, + ) } } diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.spec.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.spec.ts index c748b7b92..57e4b2cae 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.spec.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.spec.ts @@ -2,9 +2,9 @@ import 'reflect-metadata' import { SubscriptionName } from '@standardnotes/common' import { RoleName } from '@standardnotes/domain-core' -import { SubscriptionSettingName } from '@standardnotes/settings' - +import { SettingName } from '@standardnotes/settings' import { PermissionName } from '@standardnotes/features' + import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface' import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface' @@ -51,14 +51,11 @@ describe('SubscriptionSettingsAssociationService', () => { const flatSettings = [ ...( - settings as Map< - SubscriptionSettingName, - { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion } - > + settings as Map ).keys(), ] - expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'FILE_UPLOAD_BYTES_LIMIT']) - expect(settings?.get(SubscriptionSettingName.FileUploadBytesLimit)).toEqual({ + expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'MUTE_SIGN_IN_EMAILS', 'FILE_UPLOAD_BYTES_LIMIT']) + expect(settings?.get(SettingName.NAMES.FileUploadBytesLimit)).toEqual({ sensitive: false, serverEncryptionVersion: 0, value: '107374182400', @@ -79,14 +76,11 @@ describe('SubscriptionSettingsAssociationService', () => { const flatSettings = [ ...( - settings as Map< - SubscriptionSettingName, - { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion } - > + settings as Map ).keys(), ] - expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'FILE_UPLOAD_BYTES_LIMIT']) - expect(settings?.get(SubscriptionSettingName.FileUploadBytesLimit)).toEqual({ + expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'MUTE_SIGN_IN_EMAILS', 'FILE_UPLOAD_BYTES_LIMIT']) + expect(settings?.get(SettingName.NAMES.FileUploadBytesLimit)).toEqual({ sensitive: false, serverEncryptionVersion: 0, value: '104857600', diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.ts index ff94c4c0c..7d4f77c10 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationService.ts @@ -1,6 +1,6 @@ import { SubscriptionName } from '@standardnotes/common' import { PermissionName } from '@standardnotes/features' -import { SubscriptionSettingName } from '@standardnotes/settings' +import { SettingName } from '@standardnotes/settings' import { inject, injectable } from 'inversify' import TYPES from '../../Bootstrap/Types' @@ -19,40 +19,55 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti @inject(TYPES.RoleRepository) private roleRepository: RoleRepositoryInterface, ) {} - private readonly settingsToSubscriptionNameMap = new Map< - SubscriptionName, - Map - >([ + private readonly settingsToSubscriptionNameMap = new Map>([ [ SubscriptionName.PlusPlan, new Map([ [ - SubscriptionSettingName.FileUploadBytesUsed, + SettingName.NAMES.FileUploadBytesUsed, { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false }, ], + [ + SettingName.NAMES.MuteSignInEmails, + { + sensitive: false, + serverEncryptionVersion: EncryptionVersion.Unencrypted, + value: 'not_muted', + replaceable: false, + }, + ], ]), ], [ SubscriptionName.ProPlan, new Map([ [ - SubscriptionSettingName.FileUploadBytesUsed, + SettingName.NAMES.FileUploadBytesUsed, { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false }, ], + [ + SettingName.NAMES.MuteSignInEmails, + { + sensitive: false, + serverEncryptionVersion: EncryptionVersion.Unencrypted, + value: 'not_muted', + replaceable: false, + }, + ], ]), ], ]) async getDefaultSettingsAndValuesForSubscriptionName( subscriptionName: SubscriptionName, - ): Promise | undefined> { + ): Promise | undefined> { const defaultSettings = this.settingsToSubscriptionNameMap.get(subscriptionName) if (defaultSettings === undefined) { return undefined } - defaultSettings.set(SubscriptionSettingName.FileUploadBytesLimit, { + defaultSettings.set(SettingName.NAMES.FileUploadBytesLimit, { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: (await this.getFileUploadLimit(subscriptionName)).toString(), diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationServiceInterface.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationServiceInterface.ts index aa8250d8b..e01b8ab85 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationServiceInterface.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingsAssociationServiceInterface.ts @@ -1,11 +1,10 @@ import { SubscriptionName } from '@standardnotes/common' -import { SubscriptionSettingName } from '@standardnotes/settings' import { SettingDescription } from './SettingDescription' export interface SubscriptionSettingsAssociationServiceInterface { getDefaultSettingsAndValuesForSubscriptionName( subscriptionName: SubscriptionName, - ): Promise | undefined> + ): Promise | undefined> getFileUploadLimit(subscriptionName: SubscriptionName): Promise } diff --git a/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts b/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts index 0d58ff935..a7c608a5b 100644 --- a/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts +++ b/packages/auth/src/Domain/UseCase/CreateValetToken/CreateValetToken.ts @@ -3,7 +3,7 @@ import { SubscriptionName } from '@standardnotes/common' import { TimerInterface } from '@standardnotes/time' import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security' import { CreateValetTokenPayload, CreateValetTokenResponseData } from '@standardnotes/responses' -import { SubscriptionSettingName } from '@standardnotes/settings' +import { SettingName } from '@standardnotes/settings' import TYPES from '../../../Bootstrap/Types' import { UseCaseInterface } from '../UseCaseInterface' @@ -56,7 +56,7 @@ export class CreateValetToken implements UseCaseInterface { const uploadBytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ userUuid: regularSubscriptionUserUuid, userSubscriptionUuid: regularSubscription.uuid, - subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed, + subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(), }) if (uploadBytesUsedSetting !== null) { uploadBytesUsed = +(uploadBytesUsedSetting.value as string) @@ -70,7 +70,7 @@ export class CreateValetToken implements UseCaseInterface { await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ userUuid: regularSubscriptionUserUuid, userSubscriptionUuid: regularSubscription.uuid, - subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, + subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), }) if (overwriteWithUserUploadBytesLimitSetting !== null) { uploadBytesLimit = +(overwriteWithUserUploadBytesLimitSetting.value as string) diff --git a/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.ts b/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.ts index 0848750fb..ee429b981 100644 --- a/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.ts +++ b/packages/auth/src/Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes.ts @@ -35,7 +35,7 @@ export class GenerateRecoveryCodes implements UseCaseInterface { await this.settingService.createOrReplace({ user, props: { - name: SettingName.RecoveryCodes, + name: SettingName.NAMES.RecoveryCodes, unencryptedValue: recoveryCodes, serverEncryptionVersion: EncryptionVersion.Default, sensitive: false, diff --git a/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.spec.ts b/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.spec.ts index 0d7ec9540..d0d28d75a 100644 --- a/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.spec.ts +++ b/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.spec.ts @@ -1,75 +1,239 @@ -import { SettingName } from '@standardnotes/settings' import 'reflect-metadata' + +import { SettingName } from '@standardnotes/settings' + import { SettingProjector } from '../../../Projection/SettingProjector' import { Setting } from '../../Setting/Setting' import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' import { GetSetting } from './GetSetting' +import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' +import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' +import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { UserSubscription } from '../../Subscription/UserSubscription' +import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' describe('GetSetting', () => { let settingProjector: SettingProjector let setting: Setting + let subscriptionSetting: SubscriptionSetting let settingService: SettingServiceInterface + let userSubscriptionService: UserSubscriptionServiceInterface + let subscriptionSettingProjector: SubscriptionSettingProjector + let subscriptionSettingService: SubscriptionSettingServiceInterface + let regularSubscription: UserSubscription + let sharedSubscription: UserSubscription - const createUseCase = () => new GetSetting(settingProjector, settingService) + const createUseCase = () => + new GetSetting( + settingProjector, + subscriptionSettingProjector, + settingService, + subscriptionSettingService, + userSubscriptionService, + ) beforeEach(() => { setting = {} as jest.Mocked + subscriptionSetting = { + sensitive: false, + } as jest.Mocked + settingService = {} as jest.Mocked settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) settingProjector = {} as jest.Mocked settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' }) + + subscriptionSettingService = {} as jest.Mocked + subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest + .fn() + .mockReturnValue(subscriptionSetting) + + regularSubscription = { + uuid: '1-2-3', + subscriptionType: UserSubscriptionType.Regular, + } as jest.Mocked + + sharedSubscription = { + uuid: '2-3-4', + subscriptionType: UserSubscriptionType.Shared, + } as jest.Mocked + + userSubscriptionService = {} as jest.Mocked + userSubscriptionService.findRegularSubscriptionForUserUuid = jest + .fn() + .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) + + subscriptionSettingProjector = {} as jest.Mocked + subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' }) }) - it('should find a setting for user', async () => { - expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test' })).toEqual({ - success: true, - userUuid: '1-2-3', - setting: { foo: 'bar' }, + describe('no subscription', () => { + it('should find a setting for user', async () => { + expect( + await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }), + ).toEqual({ + success: true, + userUuid: '1-2-3', + setting: { foo: 'bar' }, + }) + }) + + it('should not find a setting if the setting name is invalid', async () => { + expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({ + success: false, + error: { + message: 'Invalid setting name: invalid', + }, + }) + }) + + it('should not get a setting for user if it does not exist', async () => { + settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) + + expect( + await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }), + ).toEqual({ + success: false, + error: { + message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!', + }, + }) + }) + + it('should not retrieve a sensitive setting for user', async () => { + setting = { + sensitive: true, + name: SettingName.NAMES.MfaSecret, + } as jest.Mocked + + settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) + + expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({ + success: true, + sensitive: true, + }) + }) + + it('should not retrieve a subscription setting for user', async () => { + expect( + await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), + ).toEqual({ + success: false, + error: { + message: 'No subscription found.', + }, + }) + }) + + it('should retrieve a sensitive setting for user if explicitly told to', async () => { + setting = { + sensitive: true, + name: SettingName.NAMES.MfaSecret, + } as jest.Mocked + + settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) + + expect( + await createUseCase().execute({ + userUuid: '1-2-3', + settingName: SettingName.NAMES.MfaSecret, + allowSensitiveRetrieval: true, + }), + ).toEqual({ + success: true, + userUuid: '1-2-3', + setting: { foo: 'bar' }, + }) }) }) - it('should not get a setting for user if it does not exist', async () => { - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) + describe('regular subscription', () => { + beforeEach(() => { + userSubscriptionService.findRegularSubscriptionForUserUuid = jest + .fn() + .mockReturnValue({ regularSubscription, sharedSubscription: null }) + }) - expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test' })).toEqual({ - success: false, - error: { - message: 'Setting test for user 1-2-3 not found!', - }, + it('should find a setting for user', async () => { + expect( + await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), + ).toEqual({ + success: true, + userUuid: '1-2-3', + setting: { foo: 'sub-bar' }, + }) + }) + + it('should not get a suscription setting for user if it does not exist', async () => { + subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null) + + expect( + await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), + ).toEqual({ + success: false, + error: { + message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!', + }, + }) + }) + + it('should not retrieve a sensitive subscription setting for user', async () => { + subscriptionSetting.sensitive = true + + subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest + .fn() + .mockReturnValue(subscriptionSetting) + + expect( + await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), + ).toEqual({ + success: true, + sensitive: true, + }) }) }) - it('should not retrieve a sensitive setting for user', async () => { - setting = { - sensitive: true, - name: SettingName.MfaSecret, - } as jest.Mocked - - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) - - expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.MfaSecret })).toEqual({ - success: true, - sensitive: true, + describe('shared subscription', () => { + beforeEach(() => { + userSubscriptionService.findRegularSubscriptionForUserUuid = jest + .fn() + .mockReturnValue({ regularSubscription, sharedSubscription }) }) - }) - it('should retrieve a sensitive setting for user if explicitly told to', async () => { - setting = { - sensitive: true, - name: SettingName.MfaSecret, - } as jest.Mocked + it('should find a setting for user', async () => { + expect( + await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), + ).toEqual({ + success: true, + userUuid: '1-2-3', + setting: { foo: 'sub-bar' }, + }) - settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) + expect(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({ + subscriptionSettingName: SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue(), + userSubscriptionUuid: '2-3-4', + userUuid: '1-2-3', + }) + }) - expect( - await createUseCase().execute({ userUuid: '1-2-3', settingName: 'MFA_SECRET', allowSensitiveRetrieval: true }), - ).toEqual({ - success: true, - userUuid: '1-2-3', - setting: { foo: 'bar' }, + it('should find a regular subscription only setting for user', async () => { + expect( + await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }), + ).toEqual({ + success: true, + userUuid: '1-2-3', + setting: { foo: 'sub-bar' }, + }) + + expect(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({ + subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), + userSubscriptionUuid: '1-2-3', + userUuid: '1-2-3', + }) }) }) }) diff --git a/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.ts b/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.ts index e78041fec..6957aa7fe 100644 --- a/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.ts +++ b/packages/auth/src/Domain/UseCase/GetSetting/GetSetting.ts @@ -1,32 +1,100 @@ import { SettingName } from '@standardnotes/settings' import { inject, injectable } from 'inversify' -import { GetSettingDto } from './GetSettingDto' -import { GetSettingResponse } from './GetSettingResponse' + import { UseCaseInterface } from '../UseCaseInterface' import TYPES from '../../../Bootstrap/Types' import { SettingProjector } from '../../../Projection/SettingProjector' import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' +import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' +import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' +import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' + +import { GetSettingDto } from './GetSettingDto' +import { GetSettingResponse } from './GetSettingResponse' +import { UserSubscription } from '../../Subscription/UserSubscription' @injectable() export class GetSetting implements UseCaseInterface { constructor( @inject(TYPES.SettingProjector) private settingProjector: SettingProjector, + @inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector, @inject(TYPES.SettingService) private settingService: SettingServiceInterface, + @inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface, + @inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, ) {} async execute(dto: GetSettingDto): Promise { - const { userUuid, settingName } = dto + const settingNameOrError = SettingName.create(dto.settingName) + if (settingNameOrError.isFailed()) { + return { + success: false, + error: { + message: settingNameOrError.getError(), + }, + } + } + const settingName = settingNameOrError.getValue() + + if (settingName.isASubscriptionSetting()) { + const { regularSubscription, sharedSubscription } = + await this.userSubscriptionService.findRegularSubscriptionForUserUuid(dto.userUuid) + let subscription: UserSubscription | null + if (settingName.isARegularOnlySubscriptionSetting()) { + subscription = regularSubscription + } else { + subscription = sharedSubscription ?? regularSubscription + } + + if (!subscription) { + return { + success: false, + error: { + message: 'No subscription found.', + }, + } + } + + const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ + userUuid: dto.userUuid, + subscriptionSettingName: settingName, + userSubscriptionUuid: subscription.uuid, + }) + + if (subscriptionSetting === null) { + return { + success: false, + error: { + message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`, + }, + } + } + + if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) { + return { + success: true, + sensitive: true, + } + } + + const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting) + + return { + success: true, + userUuid: dto.userUuid, + setting: simpleSubscriptionSetting, + } + } const setting = await this.settingService.findSettingWithDecryptedValue({ - userUuid, - settingName: settingName as SettingName, + userUuid: dto.userUuid, + settingName, }) if (setting === null) { return { success: false, error: { - message: `Setting ${settingName} for user ${userUuid} not found!`, + message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`, }, } } @@ -42,7 +110,7 @@ export class GetSetting implements UseCaseInterface { return { success: true, - userUuid, + userUuid: dto.userUuid, setting: simpleSetting, } } diff --git a/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.spec.ts b/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.spec.ts index 60c92195e..784397237 100644 --- a/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.spec.ts +++ b/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.spec.ts @@ -11,36 +11,84 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { User } from '../../User/User' import { CrypterInterface } from '../../Encryption/CrypterInterface' import { EncryptionVersion } from '../../Encryption/EncryptionVersion' +import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' +import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' +import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { UserSubscription } from '../../Subscription/UserSubscription' +import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' describe('GetSettings', () => { let settingRepository: SettingRepositoryInterface + let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface + let userSubscriptionService: UserSubscriptionServiceInterface let settingProjector: SettingProjector + let subscriptionSettingProjector: SubscriptionSettingProjector let setting: Setting let mfaSetting: Setting + let signInEmailsSetting: SubscriptionSetting let userRepository: UserRepositoryInterface let user: User let crypter: CrypterInterface + let regularSubscription: UserSubscription + let sharedSubscription: UserSubscription - const createUseCase = () => new GetSettings(settingRepository, settingProjector, userRepository, crypter) + const createUseCase = () => + new GetSettings( + settingRepository, + subscriptionSettingRepository, + userSubscriptionService, + settingProjector, + subscriptionSettingProjector, + userRepository, + crypter, + ) beforeEach(() => { - setting = { - name: 'test', - updatedAt: 345, - sensitive: false, - } as jest.Mocked + setting = new Setting() + setting.name = 'test' + setting.updatedAt = 345 + setting.sensitive = false - mfaSetting = { - name: SettingName.MfaSecret, - updatedAt: 122, - sensitive: true, - } as jest.Mocked + mfaSetting = new Setting() + mfaSetting.name = SettingName.NAMES.MfaSecret + mfaSetting.updatedAt = 122 + mfaSetting.sensitive = true + + signInEmailsSetting = new SubscriptionSetting() + signInEmailsSetting.name = SettingName.NAMES.MuteSignInEmails + signInEmailsSetting.updatedAt = 122 + signInEmailsSetting.sensitive = false + signInEmailsSetting.value = 'not_muted' settingRepository = {} as jest.Mocked settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting, mfaSetting]) + subscriptionSettingRepository = {} as jest.Mocked + subscriptionSettingRepository.findAllBySubscriptionUuid = jest.fn().mockReturnValue([signInEmailsSetting]) + + regularSubscription = { + uuid: '1-2-3', + subscriptionType: UserSubscriptionType.Regular, + user: Promise.resolve(user), + } as jest.Mocked + + sharedSubscription = { + uuid: '2-3-4', + subscriptionType: UserSubscriptionType.Shared, + user: Promise.resolve(user), + } as jest.Mocked + + userSubscriptionService = {} as jest.Mocked + userSubscriptionService.findRegularSubscriptionForUserUuid = jest + .fn() + .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) + settingProjector = {} as jest.Mocked - settingProjector.projectManySimple = jest.fn().mockReturnValue([{ foo: 'bar' }]) + settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' }) + + subscriptionSettingProjector = {} as jest.Mocked + subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' }) user = {} as jest.Mocked @@ -51,83 +99,126 @@ describe('GetSettings', () => { crypter.decryptForUser = jest.fn().mockReturnValue('decrypted') }) - it('should fail if a user is not found', async () => { - userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + describe('no subscription', () => { + it('should fail if a user is not found', async () => { + userRepository.findOneByUuid = jest.fn().mockReturnValue(null) - expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ - success: false, - error: { - message: 'User 1-2-3 not found.', - }, - }) - }) - - it('should return all user settings except mfa', async () => { - expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ - success: true, - userUuid: '1-2-3', - settings: [{ foo: 'bar' }], + expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ + success: false, + error: { + message: 'User 1-2-3 not found.', + }, + }) }) - expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting]) - }) + it('should return all user settings except mfa', async () => { + expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ + success: true, + userUuid: '1-2-3', + settings: [{ foo: 'bar' }], + }) - it('should return all setting with decrypted values', async () => { - setting = { - name: 'test', - updatedAt: 345, - value: 'encrypted', - serverEncryptionVersion: EncryptionVersion.Default, - } as jest.Mocked - settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting]) - - expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ - success: true, - userUuid: '1-2-3', - settings: [{ foo: 'bar' }], + expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) + expect(subscriptionSettingProjector.projectSimple).not.toHaveBeenCalled() }) - expect(settingProjector.projectManySimple).toHaveBeenCalledWith([ - { + it('should return all setting with decrypted values', async () => { + setting = { + name: 'test', + updatedAt: 345, + value: 'encrypted', + serverEncryptionVersion: EncryptionVersion.Default, + } as jest.Mocked + settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting]) + + expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ + success: true, + userUuid: '1-2-3', + settings: [{ foo: 'bar' }], + }) + + expect(settingProjector.projectSimple).toHaveBeenCalledWith({ name: 'test', updatedAt: 345, value: 'decrypted', serverEncryptionVersion: 1, - }, - ]) - }) - - it('should return all user settings of certain name', async () => { - expect( - await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test', allowSensitiveRetrieval: true }), - ).toEqual({ - success: true, - userUuid: '1-2-3', - settings: [{ foo: 'bar' }], + }) }) - expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting]) - }) + it('should return all user settings of certain name', async () => { + expect( + await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test', allowSensitiveRetrieval: true }), + ).toEqual({ + success: true, + userUuid: '1-2-3', + settings: [{ foo: 'bar' }], + }) - it('should return all user settings updated after', async () => { - expect( - await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true, updatedAfter: 123 }), - ).toEqual({ - success: true, - userUuid: '1-2-3', - settings: [{ foo: 'bar' }], + expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) }) - expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting]) - }) + it('should return all user settings updated after', async () => { + expect( + await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true, updatedAfter: 123 }), + ).toEqual({ + success: true, + userUuid: '1-2-3', + settings: [{ foo: 'bar' }], + }) - it('should return all sensitive user settings if explicit', async () => { - expect(await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true })).toEqual({ - success: true, - userUuid: '1-2-3', - settings: [{ foo: 'bar' }], + expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) }) - expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting, mfaSetting]) + it('should return all sensitive user settings if explicit', async () => { + expect(await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true })).toEqual({ + success: true, + userUuid: '1-2-3', + settings: [{ foo: 'bar' }, { foo: 'bar' }], + }) + + expect(settingProjector.projectSimple).toHaveBeenCalledTimes(2) + expect(settingProjector.projectSimple).toHaveBeenNthCalledWith(1, setting) + expect(settingProjector.projectSimple).toHaveBeenNthCalledWith(2, mfaSetting) + }) + }) + + describe('regular subscription', () => { + beforeEach(() => { + userSubscriptionService.findRegularSubscriptionForUserUuid = jest + .fn() + .mockReturnValue({ regularSubscription, sharedSubscription: null }) + }) + + it('should return all user settings except mfa', async () => { + expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ + success: true, + userUuid: '1-2-3', + settings: [{ foo: 'bar' }, { foo: 'sub-bar' }], + }) + + expect(subscriptionSettingRepository.findAllBySubscriptionUuid).toHaveBeenCalledWith('1-2-3') + expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) + expect(subscriptionSettingProjector.projectSimple).toHaveBeenCalledWith(signInEmailsSetting) + }) + }) + + describe('shared subscription', () => { + beforeEach(() => { + userSubscriptionService.findRegularSubscriptionForUserUuid = jest + .fn() + .mockReturnValue({ regularSubscription, sharedSubscription }) + }) + + it('should return all user settings except mfa', async () => { + expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ + success: true, + userUuid: '1-2-3', + settings: [{ foo: 'bar' }, { foo: 'sub-bar' }], + }) + + expect(subscriptionSettingRepository.findAllBySubscriptionUuid).toHaveBeenCalledWith('2-3-4') + expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting) + expect(subscriptionSettingProjector.projectSimple).toHaveBeenCalledWith(signInEmailsSetting) + }) }) }) diff --git a/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.ts b/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.ts index 6619a0c90..0506de565 100644 --- a/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.ts +++ b/packages/auth/src/Domain/UseCase/GetSettings/GetSettings.ts @@ -9,12 +9,22 @@ import { Setting } from '../../Setting/Setting' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { CrypterInterface } from '../../Encryption/CrypterInterface' import { EncryptionVersion } from '../../Encryption/EncryptionVersion' +import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' +import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface' +import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' +import { SimpleSetting } from '../../Setting/SimpleSetting' +import { SimpleSubscriptionSetting } from '../../Setting/SimpleSubscriptionSetting' +import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' @injectable() export class GetSettings implements UseCaseInterface { constructor( @inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface, + @inject(TYPES.SubscriptionSettingRepository) + private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface, + @inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, @inject(TYPES.SettingProjector) private settingProjector: SettingProjector, + @inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector, @inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface, @inject(TYPES.Crypter) private crypter: CrypterInterface, ) {} @@ -33,27 +43,43 @@ export class GetSettings implements UseCaseInterface { } } - let settings = await this.settingRepository.findAllByUserUuid(userUuid) + let settings: Array + settings = await this.settingRepository.findAllByUserUuid(userUuid) + + const { regularSubscription, sharedSubscription } = + await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid) + const subscription = sharedSubscription ?? regularSubscription + if (subscription) { + const subscriptionSettings = await this.subscriptionSettingRepository.findAllBySubscriptionUuid(subscription.uuid) + settings = settings.concat(subscriptionSettings) + } if (dto.settingName !== undefined) { - settings = settings.filter((setting: Setting) => setting.name === dto.settingName) + settings = settings.filter((setting: Setting | SubscriptionSetting) => setting.name === dto.settingName) } if (dto.updatedAfter !== undefined) { - settings = settings.filter((setting: Setting) => setting.updatedAt >= (dto.updatedAfter as number)) + settings = settings.filter( + (setting: Setting | SubscriptionSetting) => setting.updatedAt >= (dto.updatedAfter as number), + ) } if (!dto.allowSensitiveRetrieval) { - settings = settings.filter((setting: Setting) => !setting.sensitive) + settings = settings.filter((setting: Setting | SubscriptionSetting) => !setting.sensitive) } + const simpleSettings: Array = [] for (const setting of settings) { if (setting.value !== null && setting.serverEncryptionVersion === EncryptionVersion.Default) { setting.value = await this.crypter.decryptForUser(setting.value, user) } - } - const simpleSettings = await this.settingProjector.projectManySimple(settings) + if (setting instanceof SubscriptionSetting) { + simpleSettings.push(await this.subscriptionSettingProjector.projectSimple(setting)) + } else { + simpleSettings.push(await this.settingProjector.projectSimple(setting)) + } + } return { success: true, diff --git a/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsResponse.ts b/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsResponse.ts index d90b3bc67..a258c2115 100644 --- a/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsResponse.ts +++ b/packages/auth/src/Domain/UseCase/GetSettings/GetSettingsResponse.ts @@ -1,10 +1,11 @@ import { SimpleSetting } from '../../Setting/SimpleSetting' +import { SimpleSubscriptionSetting } from '../../Setting/SimpleSubscriptionSetting' export type GetSettingsResponse = | { success: true userUuid: string - settings: SimpleSetting[] + settings: Array } | { success: false diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.spec.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.spec.ts deleted file mode 100644 index 0d8058a34..000000000 --- a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionSettingName } from '@standardnotes/settings' - -import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' -import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' - -import { GetSubscriptionSetting } from './GetSubscriptionSetting' -import { UserSubscription } from '../../Subscription/UserSubscription' -import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType' -import { User } from '../../User/User' - -describe('GetSubscriptionSetting', () => { - let userSubscriptionService: UserSubscriptionServiceInterface - let subscriptionSettingService: SubscriptionSettingServiceInterface - let subscriptionSettingProjector: SubscriptionSettingProjector - let subscriptionSetting: SubscriptionSetting - let regularSubscription: UserSubscription - let user: User - - const createUseCase = () => - new GetSubscriptionSetting(userSubscriptionService, subscriptionSettingService, subscriptionSettingProjector) - - beforeEach(() => { - subscriptionSetting = {} as jest.Mocked - - user = { - uuid: '1-2-3', - } as jest.Mocked - - regularSubscription = { - uuid: '1-2-3', - subscriptionType: UserSubscriptionType.Regular, - user: Promise.resolve(user), - } as jest.Mocked - - userSubscriptionService = {} as jest.Mocked - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription, sharedSubscription: null }) - - subscriptionSettingService = {} as jest.Mocked - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest - .fn() - .mockReturnValue(subscriptionSetting) - - subscriptionSettingProjector = {} as jest.Mocked - subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' }) - }) - - it('should find a setting for user', async () => { - expect( - await createUseCase().execute({ - userUuid: '1-2-3', - subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed, - }), - ).toEqual({ - success: true, - setting: { foo: 'bar' }, - }) - }) - - it('should not get a setting for user if user has no corresponding regular subscription', async () => { - userSubscriptionService.findRegularSubscriptionForUserUuid = jest - .fn() - .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) - - expect( - await createUseCase().execute({ - userUuid: '1-2-3', - subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, - }), - ).toEqual({ - success: false, - error: { - message: 'No subscription found.', - }, - }) - }) - - it('should not get a setting for user if it does not exist', async () => { - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null) - - expect( - await createUseCase().execute({ - userUuid: '1-2-3', - subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, - }), - ).toEqual({ - success: false, - error: { - message: 'Setting FILE_UPLOAD_BYTES_LIMIT for user 1-2-3 not found!', - }, - }) - }) - - it('should not retrieve a sensitive setting for user', async () => { - subscriptionSetting = { - sensitive: true, - name: SubscriptionSettingName.FileUploadBytesLimit, - } as jest.Mocked - - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest - .fn() - .mockReturnValue(subscriptionSetting) - - expect( - await createUseCase().execute({ - userUuid: '1-2-3', - subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, - }), - ).toEqual({ - success: true, - sensitive: true, - }) - }) - - it('should retrieve a sensitive setting for user if explicitly told to', async () => { - subscriptionSetting = { - sensitive: true, - name: SubscriptionSettingName.FileUploadBytesLimit, - } as jest.Mocked - - subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest - .fn() - .mockReturnValue(subscriptionSetting) - - expect( - await createUseCase().execute({ - userUuid: '1-2-3', - subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, - allowSensitiveRetrieval: true, - }), - ).toEqual({ - success: true, - setting: { foo: 'bar' }, - }) - }) -}) diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.ts deleted file mode 100644 index d89bf837f..000000000 --- a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { inject, injectable } from 'inversify' - -import { GetSubscriptionSettingDTO } from './GetSubscriptionSettingDTO' -import { GetSubscriptionSettingResponse } from './GetSubscriptionSettingResponse' -import { UseCaseInterface } from '../UseCaseInterface' -import TYPES from '../../../Bootstrap/Types' -import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' -import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' -import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' - -@injectable() -export class GetSubscriptionSetting implements UseCaseInterface { - constructor( - @inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, - @inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface, - @inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector, - ) {} - - async execute(dto: GetSubscriptionSettingDTO): Promise { - const { regularSubscription } = await this.userSubscriptionService.findRegularSubscriptionForUserUuid(dto.userUuid) - if (regularSubscription === null) { - return { - success: false, - error: { - message: 'No subscription found.', - }, - } - } - - const regularSubscriptionUser = await regularSubscription.user - - const setting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ - userUuid: regularSubscriptionUser.uuid, - userSubscriptionUuid: regularSubscription.uuid, - subscriptionSettingName: dto.subscriptionSettingName, - }) - - if (setting === null) { - return { - success: false, - error: { - message: `Setting ${dto.subscriptionSettingName} for user ${dto.userUuid} not found!`, - }, - } - } - - if (setting.sensitive && !dto.allowSensitiveRetrieval) { - return { - success: true, - sensitive: true, - } - } - - const simpleSetting = await this.subscriptionSettingProjector.projectSimple(setting) - - return { - success: true, - setting: simpleSetting, - } - } -} diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSettingDTO.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSettingDTO.ts deleted file mode 100644 index d5cfd11d6..000000000 --- a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSettingDTO.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SubscriptionSettingName } from '@standardnotes/settings' - -export type GetSubscriptionSettingDTO = { - userUuid: string - subscriptionSettingName: SubscriptionSettingName - allowSensitiveRetrieval?: boolean -} diff --git a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSettingResponse.ts b/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSettingResponse.ts deleted file mode 100644 index 157371071..000000000 --- a/packages/auth/src/Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSettingResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SimpleSetting } from '../../Setting/SimpleSetting' - -export type GetSubscriptionSettingResponse = - | { - success: true - setting: SimpleSetting - } - | { - success: true - sensitive: true - } - | { - success: false - error: { - message: string - } - } diff --git a/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery.ts b/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery.ts index 340c45c42..a17989780 100644 --- a/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery.ts +++ b/packages/auth/src/Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery.ts @@ -40,7 +40,7 @@ export class GetUserKeyParamsRecovery implements UseCaseInterface } const recoveryCodesSetting = await this.settingService.findSettingWithDecryptedValue({ - settingName: SettingName.RecoveryCodes, + settingName: SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(), userUuid: user.uuid, }) if (!recoveryCodesSetting) { diff --git a/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts b/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts index 704e9e1a1..753faf847 100644 --- a/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts +++ b/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts @@ -83,7 +83,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface { let settingService: SettingServiceInterface @@ -25,9 +30,24 @@ describe('UpdateSetting', () => { let userRepository: UserRepositoryInterface let roleService: RoleServiceInterface let logger: Logger + let userSubscriptionService: UserSubscriptionServiceInterface + let subscriptionSettingProjector: SubscriptionSettingProjector + let subscriptionSettingService: SubscriptionSettingServiceInterface + let regularSubscription: UserSubscription + let sharedSubscription: UserSubscription const createUseCase = () => - new UpdateSetting(settingService, settingProjector, settingsAssociationService, userRepository, roleService, logger) + new UpdateSetting( + settingService, + subscriptionSettingService, + userSubscriptionService, + settingProjector, + subscriptionSettingProjector, + settingsAssociationService, + userRepository, + roleService, + logger, + ) beforeEach(() => { setting = {} as jest.Mocked @@ -35,9 +55,32 @@ describe('UpdateSetting', () => { settingService = {} as jest.Mocked settingService.createOrReplace = jest.fn().mockReturnValue({ status: 'created', setting }) + subscriptionSettingService = {} as jest.Mocked + subscriptionSettingService.createOrReplace = jest.fn().mockReturnValue({ status: 'created', setting }) + settingProjector = {} as jest.Mocked settingProjector.projectSimple = jest.fn().mockReturnValue(settingProjection) + regularSubscription = { + uuid: '1-2-3', + subscriptionType: UserSubscriptionType.Regular, + user: Promise.resolve(user), + } as jest.Mocked + + sharedSubscription = { + uuid: '2-3-4', + subscriptionType: UserSubscriptionType.Shared, + user: Promise.resolve(user), + } as jest.Mocked + + userSubscriptionService = {} as jest.Mocked + userSubscriptionService.findRegularSubscriptionForUserUuid = jest + .fn() + .mockReturnValue({ regularSubscription: null, sharedSubscription: null }) + + subscriptionSettingProjector = {} as jest.Mocked + subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' }) + user = {} as jest.Mocked userRepository = {} as jest.Mocked @@ -57,124 +100,217 @@ describe('UpdateSetting', () => { logger.error = jest.fn() }) - it('should create a setting', async () => { - const props = { - name: SettingName.ExtensionKey, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Default, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) - - expect(settingService.createOrReplace).toHaveBeenCalledWith({ - props: { - name: 'EXTENSION_KEY', + describe('no subscription', () => { + it('should create a setting', async () => { + const props = { + name: SettingName.NAMES.ExtensionKey, unencryptedValue: 'test-setting-value', - serverEncryptionVersion: 1, + serverEncryptionVersion: EncryptionVersion.Default, sensitive: false, - }, - user, + } + + const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) + + expect(settingService.createOrReplace).toHaveBeenCalledWith({ + props: { + name: 'EXTENSION_KEY', + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: 1, + sensitive: false, + }, + user, + }) + + expect(response).toEqual({ + success: true, + setting: settingProjection, + statusCode: 201, + }) }) - expect(response).toEqual({ - success: true, - setting: settingProjection, - statusCode: 201, + it('should not create a setting if user does not exist', async () => { + userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + const props = { + name: SettingName.NAMES.ExtensionKey, + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: EncryptionVersion.Unencrypted, + sensitive: false, + } + + const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) + + expect(settingService.createOrReplace).not.toHaveBeenCalled() + + expect(response).toEqual({ + success: false, + error: { + message: 'User 1-2-3 not found.', + }, + statusCode: 404, + }) + }) + + it('should not create a subscription setting', async () => { + const props = { + name: SettingName.NAMES.MuteSignInEmails, + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: EncryptionVersion.Unencrypted, + sensitive: false, + } + + const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) + + expect(settingService.createOrReplace).not.toHaveBeenCalled() + + expect(response).toEqual({ + success: false, + error: { + message: 'User 1-2-3 has no subscription to change a subscription setting.', + }, + statusCode: 401, + }) + }) + + it('should not create a setting if the setting name is invalid', async () => { + const props = { + name: 'random-setting', + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: EncryptionVersion.Unencrypted, + sensitive: false, + } + + const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) + + expect(settingService.createOrReplace).not.toHaveBeenCalled() + + expect(response).toEqual({ + success: false, + error: { + message: 'Invalid setting name: random-setting', + }, + statusCode: 400, + }) + }) + + it('should not create a setting if user is not permitted to', async () => { + settingsAssociationService.getPermissionAssociatedWithSetting = jest + .fn() + .mockReturnValue(PermissionName.DailyEmailBackup) + + roleService.userHasPermission = jest.fn().mockReturnValue(false) + + const props = { + name: SettingName.NAMES.ExtensionKey, + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: EncryptionVersion.Unencrypted, + sensitive: false, + } + + const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) + + expect(settingService.createOrReplace).not.toHaveBeenCalled() + + expect(response).toEqual({ + success: false, + error: { + message: 'User 1-2-3 is not permitted to change the setting.', + }, + statusCode: 401, + }) + }) + + it('should not create a setting if setting is not mutable by the client', async () => { + settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(false) + + const props = { + name: SettingName.NAMES.ExtensionKey, + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: EncryptionVersion.Unencrypted, + sensitive: false, + } + + const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) + + expect(settingService.createOrReplace).not.toHaveBeenCalled() + + expect(response).toEqual({ + success: false, + error: { + message: 'User 1-2-3 is not permitted to change the setting.', + }, + statusCode: 401, + }) }) }) - it('should not create a setting if user does not exist', async () => { - userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + describe('regular subscription', () => { + beforeEach(() => { + userSubscriptionService.findRegularSubscriptionForUserUuid = jest + .fn() + .mockReturnValue({ regularSubscription, sharedSubscription: null }) + }) - const props = { - name: SettingName.ExtensionKey, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } + it('should create a subscription setting', async () => { + const props = { + name: SettingName.NAMES.MuteSignInEmails, + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: EncryptionVersion.Default, + sensitive: false, + } - const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) + const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) - expect(settingService.createOrReplace).not.toHaveBeenCalled() + expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({ + props: { + name: 'MUTE_SIGN_IN_EMAILS', + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: 1, + sensitive: false, + }, + userSubscription: regularSubscription, + }) - expect(response).toEqual({ - success: false, - error: { - message: 'User 1-2-3 not found.', - }, - statusCode: 404, + expect(response).toEqual({ + success: true, + setting: { foo: 'sub-bar' }, + statusCode: 201, + }) }) }) - it('should not create a setting if the setting name is invalid', async () => { - const props = { - name: 'random-setting', - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - - expect(response).toEqual({ - success: false, - error: { - message: 'Setting name random-setting is invalid.', - }, - statusCode: 400, + describe('shared subscription', () => { + beforeEach(() => { + userSubscriptionService.findRegularSubscriptionForUserUuid = jest + .fn() + .mockReturnValue({ regularSubscription, sharedSubscription }) }) - }) - it('should not create a setting if user is not permitted to', async () => { - settingsAssociationService.getPermissionAssociatedWithSetting = jest - .fn() - .mockReturnValue(PermissionName.DailyEmailBackup) + it('should create a subscription setting', async () => { + const props = { + name: SettingName.NAMES.MuteSignInEmails, + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: EncryptionVersion.Default, + sensitive: false, + } - roleService.userHasPermission = jest.fn().mockReturnValue(false) + const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) - const props = { - name: SettingName.ExtensionKey, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } + expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({ + props: { + name: 'MUTE_SIGN_IN_EMAILS', + unencryptedValue: 'test-setting-value', + serverEncryptionVersion: 1, + sensitive: false, + }, + userSubscription: sharedSubscription, + }) - const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - - expect(response).toEqual({ - success: false, - error: { - message: 'User 1-2-3 is not permitted to change the setting.', - }, - statusCode: 401, - }) - }) - - it('should not create a setting if setting is not mutable by the client', async () => { - settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(false) - - const props = { - name: SettingName.ExtensionKey, - unencryptedValue: 'test-setting-value', - serverEncryptionVersion: EncryptionVersion.Unencrypted, - sensitive: false, - } - - const response = await createUseCase().execute({ props, userUuid: '1-2-3' }) - - expect(settingService.createOrReplace).not.toHaveBeenCalled() - - expect(response).toEqual({ - success: false, - error: { - message: 'User 1-2-3 is not permitted to change the setting.', - }, - statusCode: 401, + expect(response).toEqual({ + success: true, + setting: { foo: 'sub-bar' }, + statusCode: 201, + }) }) }) }) diff --git a/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSetting.ts b/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSetting.ts index b626703f7..712b2e1b2 100644 --- a/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSetting.ts +++ b/packages/auth/src/Domain/UseCase/UpdateSetting/UpdateSetting.ts @@ -12,12 +12,19 @@ import { User } from '../../User/User' import { SettingName } from '@standardnotes/settings' import { RoleServiceInterface } from '../../Role/RoleServiceInterface' import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface' +import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface' +import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' +import { CreateOrReplaceSubscriptionSettingResponse } from '../../Setting/CreateOrReplaceSubscriptionSettingResponse' +import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' @injectable() export class UpdateSetting implements UseCaseInterface { constructor( @inject(TYPES.SettingService) private settingService: SettingServiceInterface, + @inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface, + @inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, @inject(TYPES.SettingProjector) private settingProjector: SettingProjector, + @inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector, @inject(TYPES.SettingsAssociationService) private settingsAssociationService: SettingsAssociationServiceInterface, @inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface, @inject(TYPES.RoleService) private roleService: RoleServiceInterface, @@ -25,15 +32,17 @@ export class UpdateSetting implements UseCaseInterface { ) {} async execute(dto: UpdateSettingDto): Promise { - if (!Object.values(SettingName).includes(dto.props.name as SettingName)) { + const settingNameOrError = SettingName.create(dto.props.name) + if (settingNameOrError.isFailed()) { return { success: false, error: { - message: `Setting name ${dto.props.name} is invalid.`, + message: settingNameOrError.getError(), }, statusCode: 400, } } + const settingName = settingNameOrError.getValue() this.logger.debug('[%s] Updating setting: %O', dto.userUuid, dto) @@ -51,7 +60,7 @@ export class UpdateSetting implements UseCaseInterface { } } - if (!(await this.userHasPermissionToUpdateSetting(user, props.name as SettingName))) { + if (!(await this.userHasPermissionToUpdateSetting(user, settingName))) { return { success: false, error: { @@ -61,10 +70,34 @@ export class UpdateSetting implements UseCaseInterface { } } - props.serverEncryptionVersion = this.settingsAssociationService.getEncryptionVersionForSetting( - props.name as SettingName, - ) - props.sensitive = this.settingsAssociationService.getSensitivityForSetting(props.name as SettingName) + props.serverEncryptionVersion = this.settingsAssociationService.getEncryptionVersionForSetting(settingName) + props.sensitive = this.settingsAssociationService.getSensitivityForSetting(settingName) + + if (settingName.isASubscriptionSetting()) { + const { regularSubscription, sharedSubscription } = + await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid) + const subscription = sharedSubscription ?? regularSubscription + if (!subscription) { + return { + success: false, + error: { + message: `User ${userUuid} has no subscription to change a subscription setting.`, + }, + statusCode: 401, + } + } + + const response = await this.subscriptionSettingService.createOrReplace({ + userSubscription: subscription, + props, + }) + + return { + success: true, + setting: await this.subscriptionSettingProjector.projectSimple(response.subscriptionSetting), + statusCode: this.statusToStatusCode(response), + } + } const response = await this.settingService.createOrReplace({ user, @@ -79,7 +112,9 @@ export class UpdateSetting implements UseCaseInterface { } /* istanbul ignore next */ - private statusToStatusCode(response: CreateOrReplaceSettingResponse): number { + private statusToStatusCode( + response: CreateOrReplaceSettingResponse | CreateOrReplaceSubscriptionSettingResponse, + ): number { if (response.status === 'created') { return 201 } @@ -92,7 +127,7 @@ export class UpdateSetting implements UseCaseInterface { } private async userHasPermissionToUpdateSetting(user: User, settingName: SettingName): Promise { - const settingIsMutableByClient = await this.settingsAssociationService.isSettingMutableByClient(settingName) + const settingIsMutableByClient = this.settingsAssociationService.isSettingMutableByClient(settingName) if (!settingIsMutableByClient) { return false } diff --git a/packages/auth/src/Domain/UseCase/VerifyMFA.spec.ts b/packages/auth/src/Domain/UseCase/VerifyMFA.spec.ts index 1b983f2cb..142632acb 100644 --- a/packages/auth/src/Domain/UseCase/VerifyMFA.spec.ts +++ b/packages/auth/src/Domain/UseCase/VerifyMFA.spec.ts @@ -55,7 +55,7 @@ describe('VerifyMFA', () => { lockRepository.lockSuccessfullOTP = jest.fn() setting = { - name: SettingName.MfaSecret, + name: SettingName.NAMES.MfaSecret, value: 'shhhh', } as jest.Mocked @@ -87,7 +87,7 @@ describe('VerifyMFA', () => { it('should pass MFA verification if user has MFA deleted', async () => { setting = { - name: SettingName.MfaSecret, + name: SettingName.NAMES.MfaSecret, value: null, } as jest.Mocked @@ -177,7 +177,7 @@ describe('VerifyMFA', () => { it('should not pass MFA verification if mfa is not correct', async () => { setting = { - name: SettingName.MfaSecret, + name: SettingName.NAMES.MfaSecret, value: 'shhhh2', } as jest.Mocked diff --git a/packages/auth/src/Domain/UseCase/VerifyMFA.ts b/packages/auth/src/Domain/UseCase/VerifyMFA.ts index d7d39f53f..25213e56a 100644 --- a/packages/auth/src/Domain/UseCase/VerifyMFA.ts +++ b/packages/auth/src/Domain/UseCase/VerifyMFA.ts @@ -90,7 +90,7 @@ export class VerifyMFA implements UseCaseInterface { const mfaSecret = await this.settingService.findSettingWithDecryptedValue({ userUuid: user.uuid, - settingName: SettingName.MfaSecret, + settingName: SettingName.create(SettingName.NAMES.MfaSecret).getValue(), }) const twoFactorEnabled = mfaSecret !== null && mfaSecret.value !== null diff --git a/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.ts b/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.ts index f94350a7f..b8fea3a58 100644 --- a/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.ts +++ b/packages/auth/src/Domain/UseCase/VerifyPredicate/VerifyPredicate.ts @@ -39,7 +39,10 @@ export class VerifyPredicate implements UseCaseInterface { } private async hasUserEnabledEmailBackups(userUuid: string): Promise { - const setting = await this.settingRepository.findOneByNameAndUserUuid(SettingName.EmailBackupFrequency, userUuid) + const setting = await this.settingRepository.findOneByNameAndUserUuid( + SettingName.NAMES.EmailBackupFrequency, + userUuid, + ) if (setting === null || setting.value === EmailBackupFrequency.Disabled) { return false diff --git a/packages/auth/src/Infra/MySQL/MySQLSettingRepository.ts b/packages/auth/src/Infra/MySQL/MySQLSettingRepository.ts index 0e1be9e81..8ef1823c4 100644 --- a/packages/auth/src/Infra/MySQL/MySQLSettingRepository.ts +++ b/packages/auth/src/Infra/MySQL/MySQLSettingRepository.ts @@ -87,7 +87,7 @@ export class MySQLSettingRepository implements SettingRepositoryInterface { async deleteByUserUuid({ settingName, userUuid }: DeleteSettingDto): Promise { await this.ormRepository - .createQueryBuilder('setting') + .createQueryBuilder() .delete() .where('name = :name AND user_uuid = :user_uuid', { user_uuid: userUuid, diff --git a/packages/auth/src/Infra/MySQL/MySQLSubscriptionSettingRepository.ts b/packages/auth/src/Infra/MySQL/MySQLSubscriptionSettingRepository.ts index 03b68852f..7f08d74ce 100644 --- a/packages/auth/src/Infra/MySQL/MySQLSubscriptionSettingRepository.ts +++ b/packages/auth/src/Infra/MySQL/MySQLSubscriptionSettingRepository.ts @@ -12,6 +12,15 @@ export class MySQLSubscriptionSettingRepository implements SubscriptionSettingRe private ormRepository: Repository, ) {} + async findAllBySubscriptionUuid(userSubscriptionUuid: string): Promise { + return this.ormRepository + .createQueryBuilder('setting') + .where('setting.user_subscription_uuid = :userSubscriptionUuid', { + userSubscriptionUuid, + }) + .getMany() + } + async save(subscriptionSetting: SubscriptionSetting): Promise { return this.ormRepository.save(subscriptionSetting) } diff --git a/packages/settings/package.json b/packages/settings/package.json index 5b9f78518..95bcbffbf 100644 --- a/packages/settings/package.json +++ b/packages/settings/package.json @@ -29,6 +29,7 @@ "typescript": "^4.8.4" }, "dependencies": { + "@standardnotes/domain-core": "workspace:^", "reflect-metadata": "^0.1.13" } } diff --git a/packages/settings/src/Domain/Setting/SensitiveSettingName.ts b/packages/settings/src/Domain/Setting/SensitiveSettingName.ts deleted file mode 100644 index de0a88455..000000000 --- a/packages/settings/src/Domain/Setting/SensitiveSettingName.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SettingName } from './SettingName' - -export type SensitiveSettingName = SettingName.MfaSecret | SettingName.ExtensionKey diff --git a/packages/settings/src/Domain/Setting/SettingName.ts b/packages/settings/src/Domain/Setting/SettingName.ts index d53616df5..0f36d6d7f 100644 --- a/packages/settings/src/Domain/Setting/SettingName.ts +++ b/packages/settings/src/Domain/Setting/SettingName.ts @@ -1,18 +1,63 @@ -export enum SettingName { - MfaSecret = 'MFA_SECRET', - ExtensionKey = 'EXTENSION_KEY', - EmailBackupFrequency = 'EMAIL_BACKUP_FREQUENCY', - DropboxBackupFrequency = 'DROPBOX_BACKUP_FREQUENCY', - DropboxBackupToken = 'DROPBOX_BACKUP_TOKEN', - OneDriveBackupFrequency = 'ONE_DRIVE_BACKUP_FREQUENCY', - OneDriveBackupToken = 'ONE_DRIVE_BACKUP_TOKEN', - GoogleDriveBackupFrequency = 'GOOGLE_DRIVE_BACKUP_FREQUENCY', - GoogleDriveBackupToken = 'GOOGLE_DRIVE_BACKUP_TOKEN', - MuteFailedBackupsEmails = 'MUTE_FAILED_BACKUPS_EMAILS', - MuteFailedCloudBackupsEmails = 'MUTE_FAILED_CLOUD_BACKUPS_EMAILS', - MuteSignInEmails = 'MUTE_SIGN_IN_EMAILS', - MuteMarketingEmails = 'MUTE_MARKETING_EMAILS', - ListedAuthorSecrets = 'LISTED_AUTHOR_SECRETS', - LogSessionUserAgent = 'LOG_SESSION_USER_AGENT', - RecoveryCodes = 'RECOVERY_CODES', +import { Result, ValueObject } from '@standardnotes/domain-core' + +import { SettingNameProps } from './SettingNameProps' + +export class SettingName extends ValueObject { + static readonly NAMES = { + MfaSecret: 'MFA_SECRET', + ExtensionKey: 'EXTENSION_KEY', + EmailBackupFrequency: 'EMAIL_BACKUP_FREQUENCY', + DropboxBackupFrequency: 'DROPBOX_BACKUP_FREQUENCY', + DropboxBackupToken: 'DROPBOX_BACKUP_TOKEN', + OneDriveBackupFrequency: 'ONE_DRIVE_BACKUP_FREQUENCY', + OneDriveBackupToken: 'ONE_DRIVE_BACKUP_TOKEN', + GoogleDriveBackupFrequency: 'GOOGLE_DRIVE_BACKUP_FREQUENCY', + GoogleDriveBackupToken: 'GOOGLE_DRIVE_BACKUP_TOKEN', + MuteFailedBackupsEmails: 'MUTE_FAILED_BACKUPS_EMAILS', + MuteFailedCloudBackupsEmails: 'MUTE_FAILED_CLOUD_BACKUPS_EMAILS', + MuteSignInEmails: 'MUTE_SIGN_IN_EMAILS', + MuteMarketingEmails: 'MUTE_MARKETING_EMAILS', + ListedAuthorSecrets: 'LISTED_AUTHOR_SECRETS', + LogSessionUserAgent: 'LOG_SESSION_USER_AGENT', + RecoveryCodes: 'RECOVERY_CODES', + FileUploadBytesLimit: 'FILE_UPLOAD_BYTES_LIMIT', + FileUploadBytesUsed: 'FILE_UPLOAD_BYTES_USED', + } + + get value(): string { + return this.props.value + } + + isSensitive(): boolean { + return [SettingName.NAMES.MfaSecret, SettingName.NAMES.ExtensionKey].includes(this.props.value) + } + + isASubscriptionSetting(): boolean { + return [ + SettingName.NAMES.FileUploadBytesLimit, + SettingName.NAMES.FileUploadBytesUsed, + SettingName.NAMES.MuteSignInEmails, + ].includes(this.props.value) + } + + isARegularOnlySubscriptionSetting(): boolean { + return [SettingName.NAMES.FileUploadBytesLimit, SettingName.NAMES.FileUploadBytesUsed].includes(this.props.value) + } + + isASharedAndRegularOnlySubscriptionSetting(): boolean { + return [SettingName.NAMES.MuteSignInEmails].includes(this.props.value) + } + + private constructor(props: SettingNameProps) { + super(props) + } + + static create(name: string): Result { + const isValidName = Object.values(this.NAMES).includes(name) + if (!isValidName) { + return Result.fail(`Invalid setting name: ${name}`) + } else { + return Result.ok(new SettingName({ value: name })) + } + } } diff --git a/packages/settings/src/Domain/Setting/SettingNameProps.ts b/packages/settings/src/Domain/Setting/SettingNameProps.ts new file mode 100644 index 000000000..51f006672 --- /dev/null +++ b/packages/settings/src/Domain/Setting/SettingNameProps.ts @@ -0,0 +1,3 @@ +export interface SettingNameProps { + value: string +} diff --git a/packages/settings/src/Domain/Setting/SubscriptionSettingName.ts b/packages/settings/src/Domain/Setting/SubscriptionSettingName.ts deleted file mode 100644 index 35e8e23ea..000000000 --- a/packages/settings/src/Domain/Setting/SubscriptionSettingName.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum SubscriptionSettingName { - FileUploadBytesLimit = 'FILE_UPLOAD_BYTES_LIMIT', - FileUploadBytesUsed = 'FILE_UPLOAD_BYTES_USED', -} diff --git a/packages/settings/src/Domain/index.ts b/packages/settings/src/Domain/index.ts index ef06eb721..835d4cb1a 100644 --- a/packages/settings/src/Domain/index.ts +++ b/packages/settings/src/Domain/index.ts @@ -9,6 +9,5 @@ export * from './MuteFailedCloudBackupsEmails/MuteFailedCloudBackupsEmailsOption export * from './MuteMarketingEmails/MuteMarketingEmailsOption' export * from './MuteSignInEmails/MuteSignInEmailsOption' export * from './OneDriveBackupFrequency/OneDriveBackupFrequency' -export * from './Setting/SensitiveSettingName' export * from './Setting/SettingName' -export * from './Setting/SubscriptionSettingName' +export * from './Setting/SettingNameProps' diff --git a/packages/syncing-server/src/Infra/HTTP/AuthHttpService.spec.ts b/packages/syncing-server/src/Infra/HTTP/AuthHttpService.spec.ts index 5e7fc84c4..55ce6a7f1 100644 --- a/packages/syncing-server/src/Infra/HTTP/AuthHttpService.spec.ts +++ b/packages/syncing-server/src/Infra/HTTP/AuthHttpService.spec.ts @@ -49,7 +49,7 @@ describe('AuthHttpService', () => { }, }) - await createService().getUserSetting('1-2-3', SettingName.MuteFailedBackupsEmails) + await createService().getUserSetting('1-2-3', SettingName.NAMES.MuteFailedBackupsEmails) expect(httpClient.request).toHaveBeenCalledWith({ method: 'GET', @@ -64,7 +64,7 @@ describe('AuthHttpService', () => { it('should throw an error if a request to auth service in order to get user setting fails', async () => { let error = null try { - await createService().getUserSetting('1-2-3', SettingName.MuteFailedCloudBackupsEmails) + await createService().getUserSetting('1-2-3', SettingName.NAMES.MuteFailedCloudBackupsEmails) } catch (caughtError) { error = caughtError } diff --git a/yarn.lock b/yarn.lock index e9f743a18..c9782fd4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3694,6 +3694,7 @@ __metadata: version: 0.0.0-use.local resolution: "@standardnotes/settings@workspace:packages/settings" dependencies: + "@standardnotes/domain-core": "workspace:^" "@typescript-eslint/eslint-plugin": "npm:^5.48.2" eslint-plugin-prettier: "npm:^4.2.1" reflect-metadata: "npm:^0.1.13"