feat: sign in setting refactor (#472)
* fix(auth): refactor setting names into domain core value objects * fix(auth): refactor specs with setting name value objects * feat(auth): move mute sign in emails to a subscription kind of setting * feat(auth): add migration script to change sign in email settings to subscription settings * chore: fix setting name usage * fix(auth): upper casing setting names --------- Co-authored-by: Karol Sójko <karolsojko@protonmail.com>
This commit is contained in:
parent
ec0fb7e0b9
commit
27cf093f85
67 changed files with 1450 additions and 809 deletions
1
.pnp.cjs
generated
1
.pnp.cjs
generated
|
@ -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"],\
|
||||
|
|
|
@ -32,36 +32,36 @@ const requestBackups = async (
|
|||
): Promise<void> => {
|
||||
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}`)
|
||||
|
|
137
packages/auth/bin/migrate_email_settings.ts
Normal file
137
packages/auth/bin/migrate_email_settings.ts
Normal file
|
@ -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<void> => {
|
||||
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)
|
||||
})
|
||||
})
|
|
@ -28,7 +28,7 @@ const requestBackups = async (
|
|||
domainEventPublisher: DomainEventPublisherInterface,
|
||||
): Promise<void> => {
|
||||
const permissionName = PermissionName.DailyEmailBackup
|
||||
const muteEmailsSettingName = SettingName.MuteFailedBackupsEmails
|
||||
const muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
|
||||
const muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted
|
||||
|
||||
if (!backupEmail) {
|
||||
|
|
11
packages/auth/docker/entrypoint-migrate-email-settings.js
Normal file
11
packages/auth/docker/entrypoint-migrate-email-settings.js
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<ListSharedSubscriptionInvitations>(TYPES.ListSharedSubscriptionInvitations)
|
||||
.to(ListSharedSubscriptionInvitations)
|
||||
container.bind<GetSubscriptionSetting>(TYPES.GetSubscriptionSetting).to(GetSubscriptionSetting)
|
||||
container.bind<VerifyPredicate>(TYPES.VerifyPredicate).to(VerifyPredicate)
|
||||
container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
|
||||
container.bind<ProcessUserRequest>(TYPES.ProcessUserRequest).to(ProcessUserRequest)
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -90,7 +90,7 @@ describe('SettingsController', () => {
|
|||
const httpResponse = <results.JsonResult>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 = <results.JsonResult>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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
user.uuid = '123'
|
||||
|
||||
getSubscriptionSetting = {} as jest.Mocked<GetSubscriptionSetting>
|
||||
getSubscriptionSetting.execute = jest.fn()
|
||||
getSetting = {} as jest.Mocked<GetSetting>
|
||||
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 = <results.JsonResult>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 = <results.JsonResult>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)
|
||||
})
|
||||
|
|
|
@ -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<results.JsonResult> {
|
||||
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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -311,7 +311,7 @@ export class SessionService implements SessionServiceInterface {
|
|||
|
||||
private async isLoggingUserAgentEnabledOnSessions(user: User): Promise<boolean> {
|
||||
const loggingSetting = await this.settingService.findSettingWithDecryptedValue({
|
||||
settingName: SettingName.LogSessionUserAgent,
|
||||
settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(),
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<Setting>
|
||||
|
||||
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<Setting>
|
||||
|
||||
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<Setting>
|
||||
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({
|
||||
name: SettingName.MuteFailedBackupsEmails,
|
||||
name: SettingName.NAMES.MuteFailedBackupsEmails,
|
||||
uuid: '6-7-8',
|
||||
value: 'muted',
|
||||
} as jest.Mocked<Setting>)
|
||||
|
||||
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<Setting>
|
||||
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<Setting>
|
||||
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<Setting>
|
||||
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({
|
||||
name: SettingName.MuteFailedCloudBackupsEmails,
|
||||
name: SettingName.NAMES.MuteFailedCloudBackupsEmails,
|
||||
uuid: '6-7-8',
|
||||
value: 'muted',
|
||||
} as jest.Mocked<Setting>)
|
||||
|
||||
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<Setting>
|
||||
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<Setting>
|
||||
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<Setting>
|
||||
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<Setting>)
|
||||
const setting = {
|
||||
name: SettingName.OneDriveBackupFrequency,
|
||||
serverEncryptionVersion: 0,
|
||||
value: 'daily',
|
||||
sensitive: false,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
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<Setting>)
|
||||
const setting = {
|
||||
name: SettingName.OneDriveBackupFrequency,
|
||||
serverEncryptionVersion: 0,
|
||||
value: OneDriveBackupFrequency.Disabled,
|
||||
sensitive: false,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
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<Setting>
|
||||
|
||||
await createInterpreter().interpretSettingUpdated(setting, user, 'daily')
|
||||
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupFrequency, user, 'daily')
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createCloudBackupRequestedEvent).not.toHaveBeenCalled()
|
||||
|
|
|
@ -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<SettingName, string> = 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<string, string> = new Map([
|
||||
[SettingName.NAMES.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
|
||||
[SettingName.NAMES.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup],
|
||||
[SettingName.NAMES.MuteMarketingEmails, EmailLevel.LEVELS.Marketing],
|
||||
[SettingName.NAMES.MuteSignInEmails, EmailLevel.LEVELS.SignIn],
|
||||
])
|
||||
|
||||
constructor(
|
||||
|
@ -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<void> {
|
||||
if (this.isChangingMuteEmailsSetting(updatedSetting)) {
|
||||
await this.triggerEmailSubscriptionChange(user, updatedSetting.name as SettingName, unencryptedValue)
|
||||
async interpretSettingUpdated(
|
||||
updatedSettingName: string,
|
||||
user: User,
|
||||
unencryptedValue: string | null,
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
await this.domainEventPublisher.publish(
|
||||
|
@ -140,33 +146,34 @@ export class SettingInterpreter implements SettingInterpreterInterface {
|
|||
)
|
||||
}
|
||||
|
||||
private async triggerCloudBackup(setting: Setting, userUuid: string, unencryptedValue: string | null): Promise<void> {
|
||||
private async triggerCloudBackup(
|
||||
settingName: string,
|
||||
userUuid: string,
|
||||
unencryptedValue: string | null,
|
||||
): Promise<void> {
|
||||
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) {
|
||||
|
|
|
@ -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<void>
|
||||
interpretSettingUpdated(updatedSettingName: string, user: User, newUnencryptedValue: string | null): Promise<void>
|
||||
}
|
||||
|
|
|
@ -39,7 +39,9 @@ describe('SettingService', () => {
|
|||
} as jest.Mocked<User>
|
||||
user.isPotentiallyAVaultAccount = jest.fn().mockReturnValue(false)
|
||||
|
||||
setting = {} as jest.Mocked<Setting>
|
||||
setting = {
|
||||
name: SettingName.NAMES.DropboxBackupToken,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
factory = {} as jest.Mocked<SettingFactoryInterface>
|
||||
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',
|
||||
|
|
|
@ -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<CreateOrReplaceSettingResponse> {
|
||||
const { user, props } = dto
|
||||
|
||||
const settingNameOrError = SettingName.create(props.name)
|
||||
if (settingNameOrError.isFailed()) {
|
||||
throw new Error(settingNameOrError.getError())
|
||||
}
|
||||
const settingName = settingNameOrError.getValue()
|
||||
|
||||
const existing = await this.findSettingWithDecryptedValue({
|
||||
userUuid: user.uuid,
|
||||
settingName: 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',
|
||||
|
|
|
@ -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<SettingName, SettingDescription>).keys()]
|
||||
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
|
||||
const flatSettings = [...(settings as Map<string, SettingDescription>).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<SettingName, SettingDescription>).keys()]
|
||||
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
|
||||
const flatSettings = [...(settings as Map<string, SettingDescription>).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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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, PermissionName>([
|
||||
[SettingName.EmailBackupFrequency, PermissionName.DailyEmailBackup],
|
||||
[SettingName.MuteSignInEmails, PermissionName.SignInAlerts],
|
||||
private readonly permissionsAssociatedWithSettings = new Map<string, PermissionName>([
|
||||
[SettingName.NAMES.EmailBackupFrequency, PermissionName.DailyEmailBackup],
|
||||
[SettingName.NAMES.MuteSignInEmails, PermissionName.SignInAlerts],
|
||||
])
|
||||
|
||||
private readonly defaultSettings = new Map<SettingName, SettingDescription>([
|
||||
private readonly defaultSettings = new Map<string, SettingDescription>([
|
||||
[
|
||||
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<SettingName, SettingDescription>([
|
||||
private readonly vaultAccountDefaultSettingsOverwrites = new Map<string, SettingDescription>([
|
||||
[
|
||||
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<SettingName, SettingDescription> {
|
||||
getDefaultSettingsAndValuesForNewUser(): Map<string, SettingDescription> {
|
||||
return this.defaultSettings
|
||||
}
|
||||
|
||||
getDefaultSettingsAndValuesForNewVaultAccount(): Map<SettingName, SettingDescription> {
|
||||
getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription> {
|
||||
const defaultVaultSettings = new Map(this.defaultSettings)
|
||||
|
||||
for (const vaultAccountDefaultSettingOverwriteKey of this.vaultAccountDefaultSettingsOverwrites.keys()) {
|
||||
|
|
|
@ -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<SettingName, SettingDescription>
|
||||
getDefaultSettingsAndValuesForNewVaultAccount(): Map<SettingName, SettingDescription>
|
||||
getDefaultSettingsAndValuesForNewUser(): Map<string, SettingDescription>
|
||||
getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription>
|
||||
getPermissionAssociatedWithSetting(settingName: SettingName): PermissionName | undefined
|
||||
getEncryptionVersionForSetting(settingName: SettingName): EncryptionVersion
|
||||
getSensitivityForSetting(settingName: SettingName): boolean
|
||||
isSettingMutableByClient(settingName: SettingName | SubscriptionSettingName): boolean
|
||||
isSettingMutableByClient(settingName: SettingName): boolean
|
||||
}
|
||||
|
|
|
@ -3,5 +3,6 @@ import { SubscriptionSetting } from './SubscriptionSetting'
|
|||
export interface SubscriptionSettingRepositoryInterface {
|
||||
findOneByUuid(uuid: string): Promise<SubscriptionSetting | null>
|
||||
findLastByNameAndUserSubscriptionUuid(name: string, userSubscriptionUuid: string): Promise<SubscriptionSetting | null>
|
||||
findAllBySubscriptionUuid(userSubscriptionUuid: string): Promise<SubscriptionSetting[]>
|
||||
save(subscriptionSetting: SubscriptionSetting): Promise<SubscriptionSetting>
|
||||
}
|
||||
|
|
|
@ -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<UserSubscription>
|
||||
|
||||
setting = {} as jest.Mocked<SubscriptionSetting>
|
||||
setting = {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
} as jest.Mocked<SubscriptionSetting>
|
||||
|
||||
factory = {} as jest.Mocked<SettingFactoryInterface>
|
||||
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<SettingInterpreterInterface>
|
||||
settingInterpreter.interpretSettingUpdated = jest.fn()
|
||||
|
||||
settingDecrypter = {} as jest.Mocked<SettingDecrypterInterface>
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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<SubscriptionSetting | null> {
|
||||
if (!dto.subscriptionSettingName.isASubscriptionSetting()) {
|
||||
throw new Error(`Setting ${dto.subscriptionSettingName.value} is not a subscription setting`)
|
||||
}
|
||||
|
||||
let setting: SubscriptionSetting | null
|
||||
if (dto.settingUuid !== undefined) {
|
||||
setting = await this.subscriptionSettingRepository.findOneByUuid(dto.settingUuid)
|
||||
} else {
|
||||
setting = await this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid(
|
||||
dto.subscriptionSettingName,
|
||||
dto.subscriptionSettingName.value,
|
||||
dto.userSubscriptionUuid,
|
||||
)
|
||||
}
|
||||
|
@ -95,10 +110,21 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
|
|||
): Promise<CreateOrReplaceSubscriptionSettingResponse> {
|
||||
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<SubscriptionSetting | null> {
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
|
||||
).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<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
|
||||
).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',
|
||||
|
|
|
@ -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<SubscriptionSettingName, SettingDescription>
|
||||
>([
|
||||
private readonly settingsToSubscriptionNameMap = new Map<SubscriptionName, Map<string, SettingDescription>>([
|
||||
[
|
||||
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<Map<SubscriptionSettingName, SettingDescription> | undefined> {
|
||||
): Promise<Map<string, SettingDescription> | 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(),
|
||||
|
|
|
@ -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<Map<SubscriptionSettingName, SettingDescription> | undefined>
|
||||
): Promise<Map<string, SettingDescription> | undefined>
|
||||
getFileUploadLimit(subscriptionName: SubscriptionName): Promise<number>
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -35,7 +35,7 @@ export class GenerateRecoveryCodes implements UseCaseInterface<string> {
|
|||
await this.settingService.createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.RecoveryCodes,
|
||||
name: SettingName.NAMES.RecoveryCodes,
|
||||
unencryptedValue: recoveryCodes,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
sensitive: false,
|
||||
|
|
|
@ -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<Setting>
|
||||
|
||||
subscriptionSetting = {
|
||||
sensitive: false,
|
||||
} as jest.Mocked<SubscriptionSetting>
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
settingProjector = {} as jest.Mocked<SettingProjector>
|
||||
settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
|
||||
.fn()
|
||||
.mockReturnValue(subscriptionSetting)
|
||||
|
||||
regularSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
sharedSubscription = {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
|
||||
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
|
||||
})
|
||||
|
||||
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<Setting>
|
||||
|
||||
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<Setting>
|
||||
|
||||
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<Setting>
|
||||
|
||||
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<Setting>
|
||||
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',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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<GetSettingResponse> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
setting = new Setting()
|
||||
setting.name = 'test'
|
||||
setting.updatedAt = 345
|
||||
setting.sensitive = false
|
||||
|
||||
mfaSetting = {
|
||||
name: SettingName.MfaSecret,
|
||||
updatedAt: 122,
|
||||
sensitive: true,
|
||||
} as jest.Mocked<Setting>
|
||||
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<SettingRepositoryInterface>
|
||||
settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting, mfaSetting])
|
||||
|
||||
subscriptionSettingRepository = {} as jest.Mocked<SubscriptionSettingRepositoryInterface>
|
||||
subscriptionSettingRepository.findAllBySubscriptionUuid = jest.fn().mockReturnValue([signInEmailsSetting])
|
||||
|
||||
regularSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
user: Promise.resolve(user),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
sharedSubscription = {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
user: Promise.resolve(user),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
settingProjector = {} as jest.Mocked<SettingProjector>
|
||||
settingProjector.projectManySimple = jest.fn().mockReturnValue([{ foo: 'bar' }])
|
||||
settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
|
||||
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
|
||||
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
|
@ -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<Setting>
|
||||
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<Setting>
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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<Setting | SubscriptionSetting>
|
||||
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<SimpleSetting | SimpleSubscriptionSetting> = []
|
||||
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,
|
||||
|
|
|
@ -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<SimpleSetting | SimpleSubscriptionSetting>
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
|
|
|
@ -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<SubscriptionSetting>
|
||||
|
||||
user = {
|
||||
uuid: '1-2-3',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
regularSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
user: Promise.resolve(user),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription: null })
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
|
||||
.fn()
|
||||
.mockReturnValue(subscriptionSetting)
|
||||
|
||||
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
|
||||
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<SubscriptionSetting>
|
||||
|
||||
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<SubscriptionSetting>
|
||||
|
||||
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' },
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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<GetSubscriptionSettingResponse> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { SubscriptionSettingName } from '@standardnotes/settings'
|
||||
|
||||
export type GetSubscriptionSettingDTO = {
|
||||
userUuid: string
|
||||
subscriptionSettingName: SubscriptionSettingName
|
||||
allowSensitiveRetrieval?: boolean
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ export class GetUserKeyParamsRecovery implements UseCaseInterface<KeyParamsData>
|
|||
}
|
||||
|
||||
const recoveryCodesSetting = await this.settingService.findSettingWithDecryptedValue({
|
||||
settingName: SettingName.RecoveryCodes,
|
||||
settingName: SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(),
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
if (!recoveryCodesSetting) {
|
||||
|
|
|
@ -83,7 +83,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
|
|||
}
|
||||
|
||||
const recoveryCodesSetting = await this.settingService.findSettingWithDecryptedValue({
|
||||
settingName: SettingName.RecoveryCodes,
|
||||
settingName: SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(),
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
if (!recoveryCodesSetting) {
|
||||
|
@ -116,7 +116,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
|
|||
}
|
||||
|
||||
await this.deleteSetting.execute({
|
||||
settingName: SettingName.MfaSecret,
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
|
||||
|
|
|
@ -14,6 +14,11 @@ import { User } from '../../User/User'
|
|||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UpdateSetting } from './UpdateSetting'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
|
||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
|
||||
describe('UpdateSetting', () => {
|
||||
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<Setting>
|
||||
|
@ -35,9 +55,32 @@ describe('UpdateSetting', () => {
|
|||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.createOrReplace = jest.fn().mockReturnValue({ status: 'created', setting })
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.createOrReplace = jest.fn().mockReturnValue({ status: 'created', setting })
|
||||
|
||||
settingProjector = {} as jest.Mocked<SettingProjector>
|
||||
settingProjector.projectSimple = jest.fn().mockReturnValue(settingProjection)
|
||||
|
||||
regularSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
user: Promise.resolve(user),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
sharedSubscription = {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
user: Promise.resolve(user),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
|
||||
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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<UpdateSettingResponse> {
|
||||
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<boolean> {
|
||||
const settingIsMutableByClient = await this.settingsAssociationService.isSettingMutableByClient(settingName)
|
||||
const settingIsMutableByClient = this.settingsAssociationService.isSettingMutableByClient(settingName)
|
||||
if (!settingIsMutableByClient) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ describe('VerifyMFA', () => {
|
|||
lockRepository.lockSuccessfullOTP = jest.fn()
|
||||
|
||||
setting = {
|
||||
name: SettingName.MfaSecret,
|
||||
name: SettingName.NAMES.MfaSecret,
|
||||
value: 'shhhh',
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
|
@ -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<Setting>
|
||||
|
||||
|
@ -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<Setting>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -39,7 +39,10 @@ export class VerifyPredicate implements UseCaseInterface {
|
|||
}
|
||||
|
||||
private async hasUserEnabledEmailBackups(userUuid: string): Promise<boolean> {
|
||||
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
|
||||
|
|
|
@ -87,7 +87,7 @@ export class MySQLSettingRepository implements SettingRepositoryInterface {
|
|||
|
||||
async deleteByUserUuid({ settingName, userUuid }: DeleteSettingDto): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder('setting')
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.where('name = :name AND user_uuid = :user_uuid', {
|
||||
user_uuid: userUuid,
|
||||
|
|
|
@ -12,6 +12,15 @@ export class MySQLSubscriptionSettingRepository implements SubscriptionSettingRe
|
|||
private ormRepository: Repository<SubscriptionSetting>,
|
||||
) {}
|
||||
|
||||
async findAllBySubscriptionUuid(userSubscriptionUuid: string): Promise<SubscriptionSetting[]> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('setting')
|
||||
.where('setting.user_subscription_uuid = :userSubscriptionUuid', {
|
||||
userSubscriptionUuid,
|
||||
})
|
||||
.getMany()
|
||||
}
|
||||
|
||||
async save(subscriptionSetting: SubscriptionSetting): Promise<SubscriptionSetting> {
|
||||
return this.ormRepository.save(subscriptionSetting)
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"typescript": "^4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import { SettingName } from './SettingName'
|
||||
|
||||
export type SensitiveSettingName = SettingName.MfaSecret | SettingName.ExtensionKey
|
|
@ -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<SettingNameProps> {
|
||||
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<SettingName> {
|
||||
const isValidName = Object.values(this.NAMES).includes(name)
|
||||
if (!isValidName) {
|
||||
return Result.fail<SettingName>(`Invalid setting name: ${name}`)
|
||||
} else {
|
||||
return Result.ok<SettingName>(new SettingName({ value: name }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
3
packages/settings/src/Domain/Setting/SettingNameProps.ts
Normal file
3
packages/settings/src/Domain/Setting/SettingNameProps.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export interface SettingNameProps {
|
||||
value: string
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export enum SubscriptionSettingName {
|
||||
FileUploadBytesLimit = 'FILE_UPLOAD_BYTES_LIMIT',
|
||||
FileUploadBytesUsed = 'FILE_UPLOAD_BYTES_USED',
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue