mirror of
https://github.com/standardnotes/server
synced 2026-01-19 02:06:04 -05:00
Compare commits
20 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74adddd1e7 | ||
|
|
0e43bc0042 | ||
|
|
b40d539611 | ||
|
|
654663d17f | ||
|
|
75830c3a98 | ||
|
|
1b5078eb96 | ||
|
|
a5e019e290 | ||
|
|
a812f3400a | ||
|
|
15af5635f0 | ||
|
|
cee6d62791 | ||
|
|
6aee51bd45 | ||
|
|
599a84e634 | ||
|
|
1c3d19cca4 | ||
|
|
9986e8e7ce | ||
|
|
e19f7a7b7f | ||
|
|
d570146378 | ||
|
|
8a9e4370e5 | ||
|
|
ce357679e9 | ||
|
|
acab402747 | ||
|
|
e385926046 |
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@@ -112,13 +112,13 @@ jobs:
|
||||
suite: 'vaults'
|
||||
|
||||
publish-self-hosting:
|
||||
needs: [ test, lint, e2e-base ]
|
||||
needs: [ test, lint, e2e-base, e2e-vaults ]
|
||||
name: Publish Self Hosting Docker Image
|
||||
uses: standardnotes/server/.github/workflows/common-self-hosting.yml@main
|
||||
secrets: inherit
|
||||
|
||||
publish-services:
|
||||
needs: [ test, lint, e2e-base ]
|
||||
needs: [ test, lint, e2e-base, e2e-vaults ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
1
.pnp.cjs
generated
1
.pnp.cjs
generated
@@ -5546,7 +5546,6 @@ const RAW_RUNTIME_STATE =
|
||||
["@types/uuid", "npm:9.0.3"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["axios", "npm:1.4.0"],\
|
||||
["bcryptjs", "npm:2.4.3"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
["dayjs", "npm:1.11.7"],\
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.32.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.3...@standardnotes/analytics@2.32.4) (2023-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.32.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.2...@standardnotes/analytics@2.32.3) (2023-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.32.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.1...@standardnotes/analytics@2.32.2) (2023-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.32.2",
|
||||
"version": "2.32.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.81.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.4...@standardnotes/api-gateway@1.81.5) (2023-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.81.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.3...@standardnotes/api-gateway@1.81.4) (2023-10-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** retry attempts and logs ([654663d](https://github.com/standardnotes/api-gateway/commit/654663d17f6eee15f7bf2bc7f40e6c37a3d8e53c))
|
||||
|
||||
## [1.81.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.2...@standardnotes/api-gateway@1.81.3) (2023-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.81.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.1...@standardnotes/api-gateway@1.81.2) (2023-10-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** add session validation retry attempts on timedout requests ([6aee51b](https://github.com/standardnotes/api-gateway/commit/6aee51bd45c25e85d01075a9c8d2854b32dd6e3c))
|
||||
|
||||
## [1.81.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.0...@standardnotes/api-gateway@1.81.1) (2023-10-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** logs severity on retry attempts ([1c3d19c](https://github.com/standardnotes/api-gateway/commit/1c3d19cca43a7a3eba2b0d05c820de5112edf89e))
|
||||
|
||||
# [1.81.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.80.1...@standardnotes/api-gateway@1.81.0) (2023-10-20)
|
||||
|
||||
### Features
|
||||
|
||||
* **api-gateway:** add retry attempts on timedout requests ([#885](https://github.com/standardnotes/api-gateway/issues/885)) ([ce35767](https://github.com/standardnotes/api-gateway/commit/ce357679e9bc704ab562e9d6ca192f49a794a664))
|
||||
|
||||
## [1.80.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.80.0...@standardnotes/api-gateway@1.80.1) (2023-10-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** stringify error in service proxy ([e385926](https://github.com/standardnotes/api-gateway/commit/e38592604644e0f52df0865ffae5b7e79d1d3d07))
|
||||
|
||||
# [1.80.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.79.14...@standardnotes/api-gateway@1.80.0) (2023-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.80.0",
|
||||
"version": "1.81.5",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -103,6 +103,8 @@ export class ContainerConfigLoader {
|
||||
.to(SubscriptionTokenAuthMiddleware)
|
||||
|
||||
// Services
|
||||
container.bind<TimerInterface>(TYPES.ApiGateway_Timer).toConstantValue(new Timer())
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
if (!configuration?.serviceContainer) {
|
||||
throw new Error('Service container is required when configured for home server')
|
||||
@@ -115,7 +117,6 @@ export class ContainerConfigLoader {
|
||||
} else {
|
||||
container.bind<ServiceProxyInterface>(TYPES.ApiGateway_ServiceProxy).to(HttpServiceProxy)
|
||||
}
|
||||
container.bind<TimerInterface>(TYPES.ApiGateway_Timer).toConstantValue(new Timer())
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
container
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Logger } from 'winston'
|
||||
import { TYPES } from '../../Bootstrap/Types'
|
||||
import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
|
||||
import { ServiceProxyInterface } from './ServiceProxyInterface'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
@injectable()
|
||||
export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
@@ -22,31 +23,50 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
@inject(TYPES.ApiGateway_HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
|
||||
@inject(TYPES.ApiGateway_CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
|
||||
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
|
||||
@inject(TYPES.ApiGateway_Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: headers.authorization,
|
||||
Accept: 'application/json',
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
},
|
||||
url: `${this.authServerUrl}/sessions/validate`,
|
||||
})
|
||||
async validateSession(
|
||||
headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
retryAttempt?: number,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
try {
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: headers.authorization,
|
||||
Accept: 'application/json',
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
},
|
||||
url: `${this.authServerUrl}/sessions/validate`,
|
||||
})
|
||||
|
||||
return {
|
||||
status: authResponse.status,
|
||||
data: authResponse.data,
|
||||
headers: {
|
||||
contentType: authResponse.headers['content-type'] as string,
|
||||
},
|
||||
return {
|
||||
status: authResponse.status,
|
||||
data: authResponse.data,
|
||||
headers: {
|
||||
contentType: authResponse.headers['content-type'] as string,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
const requestTimedOut =
|
||||
'code' in (error as Record<string, unknown>) && (error as Record<string, unknown>).code === 'ETIMEDOUT'
|
||||
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
|
||||
if (!tooManyRetryAttempts && requestTimedOut) {
|
||||
await this.timer.sleep(50)
|
||||
|
||||
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
|
||||
|
||||
return this.validateSession(headers, nextRetryAttempt)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +189,7 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
response: Response,
|
||||
endpointOrMethodIdentifier: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
retryAttempt?: number,
|
||||
): Promise<AxiosResponse | undefined> {
|
||||
try {
|
||||
const headers: Record<string, string> = {}
|
||||
@@ -211,17 +232,46 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
await this.crossServiceTokenCache.invalidate(userUuid)
|
||||
}
|
||||
|
||||
if (retryAttempt) {
|
||||
this.logger.debug(
|
||||
`Request to ${serverUrl}/${endpointOrMethodIdentifier} succeeded after ${retryAttempt} retries`,
|
||||
)
|
||||
}
|
||||
|
||||
return serviceResponse
|
||||
} catch (error) {
|
||||
const requestDidNotMakeIt = this.requestTimedOutOrDidNotReachDestination(error as Record<string, unknown>)
|
||||
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
|
||||
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
|
||||
await this.timer.sleep(50)
|
||||
|
||||
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
|
||||
|
||||
this.logger.debug(
|
||||
`Retrying request to ${serverUrl}/${endpointOrMethodIdentifier} for the ${nextRetryAttempt} time`,
|
||||
)
|
||||
|
||||
return this.getServerResponse(
|
||||
serverUrl,
|
||||
request,
|
||||
response,
|
||||
endpointOrMethodIdentifier,
|
||||
payload,
|
||||
nextRetryAttempt,
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
|
||||
this.logger.error(
|
||||
`Could not pass the request to ${serverUrl}/${endpointOrMethodIdentifier} on underlying service: ${errorMessage}`,
|
||||
tooManyRetryAttempts
|
||||
? `Request to ${serverUrl}/${endpointOrMethodIdentifier} timed out after ${retryAttempt} retries`
|
||||
: `Could not pass the request to ${serverUrl}/${endpointOrMethodIdentifier} on underlying service: ${errorMessage}`,
|
||||
)
|
||||
|
||||
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
|
||||
this.logger.debug(`Response error: ${JSON.stringify(error)}`)
|
||||
|
||||
if ((error as AxiosError).response?.headers['content-type']) {
|
||||
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
|
||||
@@ -363,4 +413,13 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private requestTimedOutOrDidNotReachDestination(error: Record<string, unknown>): boolean {
|
||||
return (
|
||||
('code' in error && error.code === 'ETIMEDOUT') ||
|
||||
('response' in error &&
|
||||
'status' in (error.response as Record<string, unknown>) &&
|
||||
[503, 504].includes((error.response as Record<string, unknown>).status as number))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,6 @@ SNS_AWS_REGION=
|
||||
SQS_QUEUE_URL=
|
||||
SQS_AWS_REGION=
|
||||
|
||||
SYNCING_SERVER_URL=http://syncing-server-js:3000
|
||||
|
||||
# (Optional) User Server
|
||||
USER_SERVER_REGISTRATION_URL=
|
||||
USER_SERVER_CHANGE_EMAIL_URL=
|
||||
USER_SERVER_AUTH_KEY=
|
||||
|
||||
VALET_TOKEN_SECRET=
|
||||
VALET_TOKEN_TTL=
|
||||
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.163.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.162.0...@standardnotes/auth-server@1.163.0) (2023-10-26)
|
||||
|
||||
### Features
|
||||
|
||||
* extract setting name to domain-core package ([0e43bc0](https://github.com/standardnotes/server/commit/0e43bc00427113f421b0c4b67c067f0de96caf52))
|
||||
|
||||
# [1.162.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.161.0...@standardnotes/auth-server@1.162.0) (2023-10-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor settings ([#890](https://github.com/standardnotes/server/issues/890)) ([1b5078e](https://github.com/standardnotes/server/commit/1b5078eb9629397822f5403643c60fbf4182df92))
|
||||
|
||||
# [1.161.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.160.0...@standardnotes/auth-server@1.161.0) (2023-10-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow to cancel previous subscription when activating premium features in e2e tests ([15af563](https://github.com/standardnotes/server/commit/15af5635f05a8363336aa33830e0157f519eee83))
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** remove axios http calls to payments server ([#889](https://github.com/standardnotes/server/issues/889)) ([a812f34](https://github.com/standardnotes/server/commit/a812f3400af3712fd5481b0c38c8805bb9c79e2c))
|
||||
|
||||
# [1.160.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.159.2...@standardnotes/auth-server@1.160.0) (2023-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
|
||||
import { ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
import { ServiceIdentifier, SettingName } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
|
||||
sdk.start()
|
||||
@@ -18,7 +18,7 @@ import { Env } from '../src/Bootstrap/Env'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
|
||||
import { MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
|
||||
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { GetUserKeyParams } from '../src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
|
||||
@@ -62,8 +62,8 @@ const requestBackups = async (
|
||||
muteEmailsSettingName,
|
||||
setting.setting_user_uuid,
|
||||
)
|
||||
if (emailsMutedSetting !== null && emailsMutedSetting.value !== null) {
|
||||
userHasEmailsMuted = emailsMutedSetting.value === muteEmailsSettingValue
|
||||
if (emailsMutedSetting !== null && emailsMutedSetting.props.value !== null) {
|
||||
userHasEmailsMuted = emailsMutedSetting.props.value === muteEmailsSettingValue
|
||||
}
|
||||
|
||||
const keyParamsResponse = await getUserKeyParamsUseCase.execute({
|
||||
@@ -74,7 +74,7 @@ const requestBackups = async (
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createEmailBackupRequestedEvent(
|
||||
setting.setting_user_uuid,
|
||||
emailsMutedSetting?.uuid as string,
|
||||
emailsMutedSetting?.id.toString() as string,
|
||||
userHasEmailsMuted,
|
||||
keyParamsResponse.keyParams,
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
|
||||
import { Email, ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
import { Email, ServiceIdentifier, SettingName } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
|
||||
sdk.start()
|
||||
@@ -16,7 +16,7 @@ import { Env } from '../src/Bootstrap/Env'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
|
||||
import { MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
|
||||
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
|
||||
@@ -55,8 +55,8 @@ const requestBackups = async (
|
||||
|
||||
let userHasEmailsMuted = false
|
||||
const emailsMutedSetting = await settingRepository.findOneByNameAndUserUuid(muteEmailsSettingName, user.uuid)
|
||||
if (emailsMutedSetting !== null && emailsMutedSetting.value !== null) {
|
||||
userHasEmailsMuted = emailsMutedSetting.value === muteEmailsSettingValue
|
||||
if (emailsMutedSetting !== null && emailsMutedSetting.props.value !== null) {
|
||||
userHasEmailsMuted = emailsMutedSetting.props.value === muteEmailsSettingValue
|
||||
}
|
||||
|
||||
const keyParamsResponse = await getUserKeyParamsUseCase.execute({
|
||||
@@ -67,7 +67,7 @@ const requestBackups = async (
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createEmailBackupRequestedEvent(
|
||||
user.uuid,
|
||||
emailsMutedSetting?.uuid as string,
|
||||
emailsMutedSetting?.id.toString() as string,
|
||||
userHasEmailsMuted,
|
||||
keyParamsResponse.keyParams,
|
||||
),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Redis, { Cluster } from 'ioredis'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
import { Setting } from '../../src/Domain/Setting/Setting'
|
||||
import { User } from '../../src/Domain/User/User'
|
||||
import { EncryptionVersion } from '../../src/Domain/Encryption/EncryptionVersion'
|
||||
import { SettingName, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
@@ -32,19 +32,21 @@ export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterfa
|
||||
usersMFAStatus.set(item['user_uuid'], 1)
|
||||
usersMFAUpdatedAt.set(item['user_uuid'], item['updated_at_timestamp'])
|
||||
|
||||
const setting = new Setting()
|
||||
setting.uuid = item['uuid']
|
||||
setting.name = SettingName.NAMES.MfaSecret
|
||||
setting.value = item['content']
|
||||
const settingOrError = Setting.create(
|
||||
{
|
||||
name: SettingName.NAMES.MfaSecret,
|
||||
value: item['deleted'] ? null : item['content'],
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
timestamps: Timestamps.create(item['created_at_timestamp'], item['updated_at_timestamp']).getValue(),
|
||||
userUuid: Uuid.create(user.uuid).getValue(),
|
||||
sensitive: true,
|
||||
},
|
||||
new UniqueEntityId(item['uuid']),
|
||||
)
|
||||
if (item['deleted']) {
|
||||
setting.value = null
|
||||
usersMFAStatus.set(item['user_uuid'], 0)
|
||||
}
|
||||
setting.serverEncryptionVersion = EncryptionVersion.Unencrypted
|
||||
setting.createdAt = item['created_at_timestamp']
|
||||
setting.updatedAt = item['updated_at_timestamp']
|
||||
setting.user = Promise.resolve(user)
|
||||
await queryRunner.manager.save(setting)
|
||||
await queryRunner.manager.save(settingOrError.getValue())
|
||||
}
|
||||
|
||||
const redisClient = this.getRedisClient()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.160.0",
|
||||
"version": "1.163.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -54,7 +54,6 @@
|
||||
"@standardnotes/sncrypto-common": "^1.13.4",
|
||||
"@standardnotes/sncrypto-node": "workspace:*",
|
||||
"@standardnotes/time": "workspace:*",
|
||||
"axios": "^1.1.3",
|
||||
"bcryptjs": "2.4.3",
|
||||
"cors": "2.8.5",
|
||||
"dayjs": "^1.11.6",
|
||||
|
||||
@@ -3,7 +3,9 @@ import { Result, ServiceInterface } from '@standardnotes/domain-core'
|
||||
export interface AuthServiceInterface extends ServiceInterface {
|
||||
activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionId: number
|
||||
subscriptionPlanName?: string
|
||||
endsAt?: Date
|
||||
cancelPreviousSubscription?: boolean
|
||||
}): Promise<Result<string>>
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
DomainEventSubscriberInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { TimerInterface, Timer } from '@standardnotes/time'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
import { UAParser, UAParserInstance } from 'ua-parser-js'
|
||||
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
@@ -45,7 +45,6 @@ import { LockRepository } from '../Infra/Redis/LockRepository'
|
||||
import { TypeORMRevokedSessionRepository } from '../Infra/TypeORM/TypeORMRevokedSessionRepository'
|
||||
import { AuthenticationMethodResolver } from '../Domain/Auth/AuthenticationMethodResolver'
|
||||
import { RevokedSession } from '../Domain/Session/RevokedSession'
|
||||
import { UserRegisteredEventHandler } from '../Domain/Handler/UserRegisteredEventHandler'
|
||||
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
|
||||
import { AuthenticateRequest } from '../Domain/UseCase/AuthenticateRequest'
|
||||
import { Role } from '../Domain/Role/Role'
|
||||
@@ -57,10 +56,7 @@ import { TypeORMSettingRepository } from '../Infra/TypeORM/TypeORMSettingReposit
|
||||
import { CrypterInterface } from '../Domain/Encryption/CrypterInterface'
|
||||
import { CrypterNode } from '../Domain/Encryption/CrypterNode'
|
||||
import { CryptoNode } from '@standardnotes/sncrypto-node'
|
||||
import { GetSettings } from '../Domain/UseCase/GetSettings/GetSettings'
|
||||
import { SettingProjector } from '../Projection/SettingProjector'
|
||||
import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { UpdateSetting } from '../Domain/UseCase/UpdateSetting/UpdateSetting'
|
||||
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
|
||||
import { SubscriptionPurchasedEventHandler } from '../Domain/Handler/SubscriptionPurchasedEventHandler'
|
||||
import { SubscriptionRenewedEventHandler } from '../Domain/Handler/SubscriptionRenewedEventHandler'
|
||||
@@ -68,11 +64,6 @@ import { SubscriptionRefundedEventHandler } from '../Domain/Handler/Subscription
|
||||
import { SubscriptionExpiredEventHandler } from '../Domain/Handler/SubscriptionExpiredEventHandler'
|
||||
import { DeleteAccount } from '../Domain/UseCase/DeleteAccount/DeleteAccount'
|
||||
import { DeleteSetting } from '../Domain/UseCase/DeleteSetting/DeleteSetting'
|
||||
import { SettingFactory } from '../Domain/Setting/SettingFactory'
|
||||
import { SettingService } from '../Domain/Setting/SettingService'
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const axios = require('axios')
|
||||
import { AxiosInstance } from 'axios'
|
||||
import { UserSubscription } from '../Domain/Subscription/UserSubscription'
|
||||
import { TypeORMUserSubscriptionRepository } from '../Infra/TypeORM/TypeORMUserSubscriptionRepository'
|
||||
import { WebSocketsClientService } from '../Infra/WebSockets/WebSocketsClientService'
|
||||
@@ -84,7 +75,6 @@ import { RoleToSubscriptionMapInterface } from '../Domain/Role/RoleToSubscriptio
|
||||
import { RoleToSubscriptionMap } from '../Domain/Role/RoleToSubscriptionMap'
|
||||
import { FeatureServiceInterface } from '../Domain/Feature/FeatureServiceInterface'
|
||||
import { FeatureService } from '../Domain/Feature/FeatureService'
|
||||
import { SettingServiceInterface } from '../Domain/Setting/SettingServiceInterface'
|
||||
import { ExtensionKeyGrantedEventHandler } from '../Domain/Handler/ExtensionKeyGrantedEventHandler'
|
||||
import {
|
||||
DirectCallDomainEventPublisher,
|
||||
@@ -117,7 +107,6 @@ import { AuthenticateOfflineSubscriptionToken } from '../Domain/UseCase/Authenti
|
||||
import { SubscriptionCancelledEventHandler } from '../Domain/Handler/SubscriptionCancelledEventHandler'
|
||||
import { ContentDecoder, ContentDecoderInterface, ProtocolVersion } from '@standardnotes/common'
|
||||
import { GetUserOfflineSubscription } from '../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
|
||||
import { UserEmailChangedEventHandler } from '../Domain/Handler/UserEmailChangedEventHandler'
|
||||
import { SettingsAssociationServiceInterface } from '../Domain/Setting/SettingsAssociationServiceInterface'
|
||||
import { SettingsAssociationService } from '../Domain/Setting/SettingsAssociationService'
|
||||
import { SubscriptionSyncRequestedEventHandler } from '../Domain/Handler/SubscriptionSyncRequestedEventHandler'
|
||||
@@ -143,8 +132,8 @@ import { FileRemovedEventHandler } from '../Domain/Handler/FileRemovedEventHandl
|
||||
import { UserDisabledSessionUserAgentLoggingEventHandler } from '../Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler'
|
||||
import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface'
|
||||
import { SettingInterpreter } from '../Domain/Setting/SettingInterpreter'
|
||||
import { SettingDecrypterInterface } from '../Domain/Setting/SettingDecrypterInterface'
|
||||
import { SettingDecrypter } from '../Domain/Setting/SettingDecrypter'
|
||||
import { SettingCrypterInterface } from '../Domain/Setting/SettingCrypterInterface'
|
||||
import { SettingCrypter } from '../Domain/Setting/SettingCrypter'
|
||||
import { SharedSubscriptionInvitationRepositoryInterface } from '../Domain/SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
|
||||
import { TypeORMSharedSubscriptionInvitationRepository } from '../Infra/TypeORM/TypeORMSharedSubscriptionInvitationRepository'
|
||||
import { InviteToSharedSubscription } from '../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
|
||||
@@ -153,16 +142,9 @@ import { AcceptSharedSubscriptionInvitation } from '../Domain/UseCase/AcceptShar
|
||||
import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSharedSubscriptionInvitation/DeclineSharedSubscriptionInvitation'
|
||||
import { CancelSharedSubscriptionInvitation } from '../Domain/UseCase/CancelSharedSubscriptionInvitation/CancelSharedSubscriptionInvitation'
|
||||
import { SharedSubscriptionInvitationCreatedEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCreatedEventHandler'
|
||||
import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
|
||||
import { SubscriptionSettingServiceInterface } from '../Domain/Setting/SubscriptionSettingServiceInterface'
|
||||
import { SubscriptionSettingService } from '../Domain/Setting/SubscriptionSettingService'
|
||||
import { SubscriptionSettingRepositoryInterface } from '../Domain/Setting/SubscriptionSettingRepositoryInterface'
|
||||
import { TypeORMSubscriptionSettingRepository } from '../Infra/TypeORM/TypeORMSubscriptionSettingRepository'
|
||||
import { SettingFactoryInterface } from '../Domain/Setting/SettingFactoryInterface'
|
||||
import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
|
||||
import { UserSubscriptionServiceInterface } from '../Domain/Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserSubscriptionService } from '../Domain/Subscription/UserSubscriptionService'
|
||||
import { SubscriptionSettingProjector } from '../Projection/SubscriptionSettingProjector'
|
||||
import { SubscriptionSettingsAssociationService } from '../Domain/Setting/SubscriptionSettingsAssociationService'
|
||||
import { SubscriptionSettingsAssociationServiceInterface } from '../Domain/Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||
import { PKCERepositoryInterface } from '../Domain/User/PKCERepositoryInterface'
|
||||
@@ -271,6 +253,27 @@ import { UserDesignatedAsSurvivorInSharedVaultEventHandler } from '../Domain/Han
|
||||
import { DisableEmailSettingBasedOnEmailSubscription } from '../Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription'
|
||||
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
|
||||
import { KeyParamsFactoryInterface } from '../Domain/User/KeyParamsFactoryInterface'
|
||||
import { TypeORMSubscriptionSetting } from '../Infra/TypeORM/TypeORMSubscriptionSetting'
|
||||
import { SetSettingValue } from '../Domain/UseCase/SetSettingValue/SetSettingValue'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting'
|
||||
import { SetSubscriptionSettingValue } from '../Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue'
|
||||
import { GetSettings } from '../Domain/UseCase/GetSettings/GetSettings'
|
||||
import { GetSubscriptionSettings } from '../Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings'
|
||||
import { GetAllSettingsForUser } from '../Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser'
|
||||
import { GetRegularSubscriptionForUser } from '../Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
|
||||
import { GetSharedSubscriptionForUser } from '../Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
|
||||
import { GetSharedOrRegularSubscriptionForUser } from '../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser'
|
||||
import { ProjectorInterface } from '../Projection/ProjectorInterface'
|
||||
import { SettingHttpRepresentation } from '../Mapping/Http/SettingHttpRepresentation'
|
||||
import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
|
||||
import { SubscriptionSettingHttpRepresentation } from '../Mapping/Http/SubscriptionSettingHttpRepresentation'
|
||||
import { SettingHttpMapper } from '../Mapping/Http/SettingHttpMapper'
|
||||
import { SubscriptionSettingHttpMapper } from '../Mapping/Http/SubscriptionSettingHttpMapper'
|
||||
import { TypeORMSetting } from '../Infra/TypeORM/TypeORMSetting'
|
||||
import { SettingPersistenceMapper } from '../Mapping/Persistence/SettingPersistenceMapper'
|
||||
import { SubscriptionSettingPersistenceMapper } from '../Mapping/Persistence/SubscriptionSettingPersistenceMapper'
|
||||
import { ApplyDefaultSettings } from '../Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
@@ -401,6 +404,22 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>>(TYPES.Auth_SharedVaultUserPersistenceMapper)
|
||||
.toConstantValue(new SharedVaultUserPersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<Setting, SettingHttpRepresentation>>(TYPES.Auth_SettingHttpMapper)
|
||||
.toConstantValue(new SettingHttpMapper())
|
||||
container
|
||||
.bind<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
|
||||
TYPES.Auth_SubscriptionSettingHttpMapper,
|
||||
)
|
||||
.toConstantValue(new SubscriptionSettingHttpMapper())
|
||||
container
|
||||
.bind<MapperInterface<Setting, TypeORMSetting>>(TYPES.Auth_SettingPersistenceMapper)
|
||||
.toConstantValue(new SettingPersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<SubscriptionSetting, TypeORMSubscriptionSetting>>(
|
||||
TYPES.Auth_SubscriptionSettingPersistenceMapper,
|
||||
)
|
||||
.toConstantValue(new SubscriptionSettingPersistenceMapper())
|
||||
|
||||
// ORM
|
||||
container
|
||||
@@ -417,14 +436,14 @@ export class ContainerConfigLoader {
|
||||
.bind<Repository<Session>>(TYPES.Auth_ORMSessionRepository)
|
||||
.toConstantValue(appDataSource.getRepository(Session))
|
||||
container
|
||||
.bind<Repository<Setting>>(TYPES.Auth_ORMSettingRepository)
|
||||
.toConstantValue(appDataSource.getRepository(Setting))
|
||||
.bind<Repository<TypeORMSetting>>(TYPES.Auth_ORMSettingRepository)
|
||||
.toConstantValue(appDataSource.getRepository(TypeORMSetting))
|
||||
container
|
||||
.bind<Repository<SharedSubscriptionInvitation>>(TYPES.Auth_ORMSharedSubscriptionInvitationRepository)
|
||||
.toConstantValue(appDataSource.getRepository(SharedSubscriptionInvitation))
|
||||
container
|
||||
.bind<Repository<SubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository)
|
||||
.toConstantValue(appDataSource.getRepository(SubscriptionSetting))
|
||||
.bind<Repository<TypeORMSubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository)
|
||||
.toConstantValue(appDataSource.getRepository(TypeORMSubscriptionSetting))
|
||||
container.bind<Repository<User>>(TYPES.Auth_ORMUserRepository).toConstantValue(appDataSource.getRepository(User))
|
||||
container
|
||||
.bind<Repository<UserSubscription>>(TYPES.Auth_ORMUserSubscriptionRepository)
|
||||
@@ -451,10 +470,24 @@ export class ContainerConfigLoader {
|
||||
.bind<RevokedSessionRepositoryInterface>(TYPES.Auth_RevokedSessionRepository)
|
||||
.to(TypeORMRevokedSessionRepository)
|
||||
container.bind<UserRepositoryInterface>(TYPES.Auth_UserRepository).to(TypeORMUserRepository)
|
||||
container.bind<SettingRepositoryInterface>(TYPES.Auth_SettingRepository).to(TypeORMSettingRepository)
|
||||
container
|
||||
.bind<SettingRepositoryInterface>(TYPES.Auth_SettingRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMSettingRepository(
|
||||
container.get<Repository<TypeORMSetting>>(TYPES.Auth_ORMSettingRepository),
|
||||
container.get<MapperInterface<Setting, TypeORMSetting>>(TYPES.Auth_SettingPersistenceMapper),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository)
|
||||
.to(TypeORMSubscriptionSettingRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMSubscriptionSettingRepository(
|
||||
container.get<Repository<TypeORMSubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository),
|
||||
container.get<MapperInterface<SubscriptionSetting, TypeORMSubscriptionSetting>>(
|
||||
TYPES.Auth_SubscriptionSettingPersistenceMapper,
|
||||
),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<OfflineSettingRepositoryInterface>(TYPES.Auth_OfflineSettingRepository)
|
||||
.to(TypeORMOfflineSettingRepository)
|
||||
@@ -517,13 +550,6 @@ export class ContainerConfigLoader {
|
||||
container.bind<UserProjector>(TYPES.Auth_UserProjector).to(UserProjector)
|
||||
container.bind<RoleProjector>(TYPES.Auth_RoleProjector).to(RoleProjector)
|
||||
container.bind<PermissionProjector>(TYPES.Auth_PermissionProjector).to(PermissionProjector)
|
||||
container.bind<SettingProjector>(TYPES.Auth_SettingProjector).to(SettingProjector)
|
||||
container
|
||||
.bind<SubscriptionSettingProjector>(TYPES.Auth_SubscriptionSettingProjector)
|
||||
.to(SubscriptionSettingProjector)
|
||||
|
||||
// Factories
|
||||
container.bind<SettingFactoryInterface>(TYPES.Auth_SettingFactory).to(SettingFactory)
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.Auth_JWT_SECRET).toConstantValue(env.get('JWT_SECRET'))
|
||||
@@ -562,16 +588,7 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(env.get('DISABLE_USER_REGISTRATION', true) === 'true')
|
||||
container.bind(TYPES.Auth_SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
container.bind(TYPES.Auth_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
|
||||
container
|
||||
.bind(TYPES.Auth_USER_SERVER_REGISTRATION_URL)
|
||||
.toConstantValue(env.get('USER_SERVER_REGISTRATION_URL', true))
|
||||
container.bind(TYPES.Auth_USER_SERVER_AUTH_KEY).toConstantValue(env.get('USER_SERVER_AUTH_KEY', true))
|
||||
container
|
||||
.bind(TYPES.Auth_USER_SERVER_CHANGE_EMAIL_URL)
|
||||
.toConstantValue(env.get('USER_SERVER_CHANGE_EMAIL_URL', true))
|
||||
container.bind(TYPES.Auth_SYNCING_SERVER_URL).toConstantValue(env.get('SYNCING_SERVER_URL', true))
|
||||
container.bind(TYPES.Auth_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
|
||||
container.bind(TYPES.Auth_PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
|
||||
container
|
||||
.bind(TYPES.Auth_SESSION_TRACE_DAYS_TTL)
|
||||
.toConstantValue(env.get('SESSION_TRACE_DAYS_TTL', true) ? +env.get('SESSION_TRACE_DAYS_TTL', true) : 90)
|
||||
@@ -654,12 +671,55 @@ export class ContainerConfigLoader {
|
||||
.to(RedisSubscriptionTokenRepository)
|
||||
}
|
||||
|
||||
// Services
|
||||
container
|
||||
.bind<TraceSession>(TYPES.Auth_TraceSession)
|
||||
.toConstantValue(
|
||||
new TraceSession(
|
||||
container.get(TYPES.Auth_SessionTraceRepository),
|
||||
container.get(TYPES.Auth_Timer),
|
||||
container.get(TYPES.Auth_SESSION_TRACE_DAYS_TTL),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SelectorInterface<ProtocolVersion>>(TYPES.Auth_ProtocolVersionSelector)
|
||||
.toConstantValue(new DeterministicSelector<ProtocolVersion>())
|
||||
container.bind<UAParser>(TYPES.Auth_DeviceDetector).toConstantValue(new UAParser())
|
||||
container.bind<SessionService>(TYPES.Auth_SessionService).to(SessionService)
|
||||
container.bind<UAParserInstance>(TYPES.Auth_DeviceDetector).toConstantValue(new UAParser())
|
||||
container.bind<CrypterInterface>(TYPES.Auth_Crypter).to(CrypterNode)
|
||||
container
|
||||
.bind<SettingCrypterInterface>(TYPES.Auth_SettingCrypter)
|
||||
.toConstantValue(
|
||||
new SettingCrypter(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<CrypterInterface>(TYPES.Auth_Crypter),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetSetting>(TYPES.Auth_GetSetting)
|
||||
.toConstantValue(
|
||||
new GetSetting(
|
||||
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
|
||||
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SessionService>(TYPES.Auth_SessionService)
|
||||
.toConstantValue(
|
||||
new SessionService(
|
||||
container.get<SessionRepositoryInterface>(TYPES.Auth_SessionRepository),
|
||||
container.get<EphemeralSessionRepositoryInterface>(TYPES.Auth_EphemeralSessionRepository),
|
||||
container.get<RevokedSessionRepositoryInterface>(TYPES.Auth_RevokedSessionRepository),
|
||||
container.get<UAParserInstance>(TYPES.Auth_DeviceDetector),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
container.get<number>(TYPES.Auth_ACCESS_TOKEN_AGE),
|
||||
container.get<number>(TYPES.Auth_REFRESH_TOKEN_AGE),
|
||||
container.get<CryptoNode>(TYPES.Auth_CryptoNode),
|
||||
container.get<TraceSession>(TYPES.Auth_TraceSession),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<string[]>(TYPES.Auth_READONLY_USERS),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
),
|
||||
)
|
||||
container.bind<AuthResponseFactory20161215>(TYPES.Auth_AuthResponseFactory20161215).to(AuthResponseFactory20161215)
|
||||
container.bind<AuthResponseFactory20190520>(TYPES.Auth_AuthResponseFactory20190520).to(AuthResponseFactory20190520)
|
||||
container.bind<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115).to(AuthResponseFactory20200115)
|
||||
@@ -698,12 +758,9 @@ export class ContainerConfigLoader {
|
||||
.bind<AuthenticationMethodResolver>(TYPES.Auth_AuthenticationMethodResolver)
|
||||
.to(AuthenticationMethodResolver)
|
||||
container.bind<DomainEventFactory>(TYPES.Auth_DomainEventFactory).to(DomainEventFactory)
|
||||
container.bind<AxiosInstance>(TYPES.Auth_HTTPClient).toConstantValue(axios.create())
|
||||
container.bind<CrypterInterface>(TYPES.Auth_Crypter).to(CrypterNode)
|
||||
container
|
||||
.bind<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService)
|
||||
.to(SettingsAssociationService)
|
||||
container.bind<SettingDecrypterInterface>(TYPES.Auth_SettingDecrypter).to(SettingDecrypter)
|
||||
|
||||
container
|
||||
.bind<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams)
|
||||
@@ -726,21 +783,6 @@ export class ContainerConfigLoader {
|
||||
),
|
||||
)
|
||||
|
||||
container
|
||||
.bind<SettingServiceInterface>(TYPES.Auth_SettingService)
|
||||
.toConstantValue(
|
||||
new SettingService(
|
||||
container.get<SettingFactoryInterface>(TYPES.Auth_SettingFactory),
|
||||
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
|
||||
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
|
||||
container.get<SettingInterpreterInterface>(TYPES.Auth_SettingInterpreter),
|
||||
container.get<SettingDecrypterInterface>(TYPES.Auth_SettingDecrypter),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionSettingServiceInterface>(TYPES.Auth_SubscriptionSettingService)
|
||||
.to(SubscriptionSettingService)
|
||||
container.bind<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService).to(OfflineSettingService)
|
||||
container.bind<ContentDecoderInterface>(TYPES.Auth_ContenDecoder).toConstantValue(new ContentDecoder())
|
||||
container.bind<ClientServiceInterface>(TYPES.Auth_WebSocketsClientService).to(WebSocketsClientService)
|
||||
@@ -753,7 +795,6 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector)
|
||||
.toConstantValue(new DeterministicSelector<boolean>())
|
||||
container.bind<UserSubscriptionServiceInterface>(TYPES.Auth_UserSubscriptionService).to(UserSubscriptionService)
|
||||
|
||||
// Middleware
|
||||
container.bind<SessionMiddleware>(TYPES.Auth_SessionMiddleware).to(SessionMiddleware)
|
||||
@@ -780,15 +821,6 @@ export class ContainerConfigLoader {
|
||||
container.bind<OfflineUserAuthMiddleware>(TYPES.Auth_OfflineUserAuthMiddleware).to(OfflineUserAuthMiddleware)
|
||||
|
||||
// use cases
|
||||
container
|
||||
.bind<TraceSession>(TYPES.Auth_TraceSession)
|
||||
.toConstantValue(
|
||||
new TraceSession(
|
||||
container.get(TYPES.Auth_SessionTraceRepository),
|
||||
container.get(TYPES.Auth_Timer),
|
||||
container.get(TYPES.Auth_SESSION_TRACE_DAYS_TTL),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<PersistStatistics>(TYPES.Auth_PersistStatistics)
|
||||
.toConstantValue(
|
||||
@@ -863,24 +895,65 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_FeatureService),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SetSettingValue>(TYPES.Auth_SetSettingValue)
|
||||
.toConstantValue(
|
||||
new SetSettingValue(
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes)
|
||||
.toConstantValue(
|
||||
new GenerateRecoveryCodes(
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_SettingService),
|
||||
container.get(TYPES.Auth_SetSettingValue),
|
||||
container.get(TYPES.Auth_CryptoNode),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting)
|
||||
.toConstantValue(
|
||||
new GetSubscriptionSetting(
|
||||
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
|
||||
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue)
|
||||
.toConstantValue(
|
||||
new SetSubscriptionSettingValue(
|
||||
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
|
||||
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings)
|
||||
.toConstantValue(
|
||||
new ApplyDefaultSubscriptionSettings(
|
||||
container.get<SubscriptionSettingsAssociationServiceInterface>(
|
||||
TYPES.Auth_SubscriptionSettingsAssociationService,
|
||||
),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
|
||||
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ActivatePremiumFeatures>(TYPES.Auth_ActivatePremiumFeatures)
|
||||
.toConstantValue(
|
||||
new ActivatePremiumFeatures(
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get(TYPES.Auth_SubscriptionSettingService),
|
||||
container.get(TYPES.Auth_RoleService),
|
||||
container.get(TYPES.Auth_Timer),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -894,47 +967,136 @@ export class ContainerConfigLoader {
|
||||
container.bind<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest)
|
||||
container.bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken).to(RefreshSessionToken)
|
||||
container.bind<SignIn>(TYPES.Auth_SignIn).to(SignIn)
|
||||
container.bind<VerifyMFA>(TYPES.Auth_VerifyMFA).to(VerifyMFA)
|
||||
container
|
||||
.bind<VerifyMFA>(TYPES.Auth_VerifyMFA)
|
||||
.toConstantValue(
|
||||
new VerifyMFA(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector),
|
||||
container.get<LockRepositoryInterface>(TYPES.Auth_LockRepository),
|
||||
container.get<string>(TYPES.Auth_PSEUDO_KEY_PARAMS_KEY),
|
||||
container.get<AuthenticatorRepositoryInterface>(TYPES.Auth_AuthenticatorRepository),
|
||||
container.get<VerifyAuthenticatorAuthenticationResponse>(
|
||||
TYPES.Auth_VerifyAuthenticatorAuthenticationResponse,
|
||||
),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container.bind<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts).to(ClearLoginAttempts)
|
||||
container.bind<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts).to(IncreaseLoginAttempts)
|
||||
container
|
||||
.bind<GetUserKeyParamsRecovery>(TYPES.Auth_GetUserKeyParamsRecovery)
|
||||
.toConstantValue(
|
||||
new GetUserKeyParamsRecovery(
|
||||
container.get(TYPES.Auth_KeyParamsFactory),
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_PKCERepository),
|
||||
container.get(TYPES.Auth_SettingService),
|
||||
container.get<KeyParamsFactoryInterface>(TYPES.Auth_KeyParamsFactory),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<PKCERepositoryInterface>(TYPES.Auth_PKCERepository),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
),
|
||||
)
|
||||
container.bind<UpdateUser>(TYPES.Auth_UpdateUser).to(UpdateUser)
|
||||
container.bind<Register>(TYPES.Auth_Register).to(Register)
|
||||
container
|
||||
.bind<ApplyDefaultSettings>(TYPES.Auth_ApplyDefaultSettings)
|
||||
.toConstantValue(
|
||||
new ApplyDefaultSettings(
|
||||
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
|
||||
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<Register>(TYPES.Auth_Register)
|
||||
.toConstantValue(
|
||||
new Register(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<RoleRepositoryInterface>(TYPES.Auth_RoleRepository),
|
||||
container.get<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115),
|
||||
container.get<CrypterInterface>(TYPES.Auth_Crypter),
|
||||
container.get<boolean>(TYPES.Auth_DISABLE_USER_REGISTRATION),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<ApplyDefaultSettings>(TYPES.Auth_ApplyDefaultSettings),
|
||||
),
|
||||
)
|
||||
container.bind<GetActiveSessionsForUser>(TYPES.Auth_GetActiveSessionsForUser).to(GetActiveSessionsForUser)
|
||||
container.bind<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser).to(DeleteOtherSessionsForUser)
|
||||
container.bind<DeleteSessionForUser>(TYPES.Auth_DeleteSessionForUser).to(DeleteSessionForUser)
|
||||
container.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials).to(ChangeCredentials)
|
||||
container.bind<GetSettings>(TYPES.Auth_GetSettings).to(GetSettings)
|
||||
container.bind<GetSetting>(TYPES.Auth_GetSetting).to(GetSetting)
|
||||
container
|
||||
.bind<GetSettings>(TYPES.Auth_GetSettings)
|
||||
.toConstantValue(
|
||||
new GetSettings(
|
||||
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
|
||||
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetSubscriptionSettings>(TYPES.Auth_GetSubscriptionSettings)
|
||||
.toConstantValue(
|
||||
new GetSubscriptionSettings(
|
||||
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
|
||||
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser)
|
||||
.toConstantValue(
|
||||
new GetRegularSubscriptionForUser(
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser)
|
||||
.toConstantValue(
|
||||
new GetSharedSubscriptionForUser(
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser)
|
||||
.toConstantValue(
|
||||
new GetSharedOrRegularSubscriptionForUser(
|
||||
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
|
||||
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetAllSettingsForUser>(TYPES.Auth_GetAllSettingsForUser)
|
||||
.toConstantValue(
|
||||
new GetAllSettingsForUser(
|
||||
container.get<GetSettings>(TYPES.Auth_GetSettings),
|
||||
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
|
||||
container.get<GetSubscriptionSettings>(TYPES.Auth_GetSubscriptionSettings),
|
||||
),
|
||||
)
|
||||
container.bind<GetUserFeatures>(TYPES.Auth_GetUserFeatures).to(GetUserFeatures)
|
||||
container.bind<UpdateSetting>(TYPES.Auth_UpdateSetting).to(UpdateSetting)
|
||||
container.bind<DeleteSetting>(TYPES.Auth_DeleteSetting).to(DeleteSetting)
|
||||
container
|
||||
.bind<SignInWithRecoveryCodes>(TYPES.Auth_SignInWithRecoveryCodes)
|
||||
.toConstantValue(
|
||||
new SignInWithRecoveryCodes(
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_AuthResponseFactory20200115),
|
||||
container.get(TYPES.Auth_PKCERepository),
|
||||
container.get(TYPES.Auth_Crypter),
|
||||
container.get(TYPES.Auth_SettingService),
|
||||
container.get(TYPES.Auth_GenerateRecoveryCodes),
|
||||
container.get(TYPES.Auth_IncreaseLoginAttempts),
|
||||
container.get(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get(TYPES.Auth_DeleteSetting),
|
||||
container.get(TYPES.Auth_AuthenticatorRepository),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115),
|
||||
container.get<PKCERepositoryInterface>(TYPES.Auth_PKCERepository),
|
||||
container.get<CrypterInterface>(TYPES.Auth_Crypter),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes),
|
||||
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
|
||||
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
|
||||
container.get<AuthenticatorRepositoryInterface>(TYPES.Auth_AuthenticatorRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DeleteAccount>(TYPES.Auth_DeleteAccount)
|
||||
.toConstantValue(
|
||||
new DeleteAccount(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
container.bind<DeleteAccount>(TYPES.Auth_DeleteAccount).to(DeleteAccount)
|
||||
container.bind<GetUserSubscription>(TYPES.Auth_GetUserSubscription).to(GetUserSubscription)
|
||||
container.bind<GetUserOfflineSubscription>(TYPES.Auth_GetUserOfflineSubscription).to(GetUserOfflineSubscription)
|
||||
container.bind<CreateSubscriptionToken>(TYPES.Auth_CreateSubscriptionToken).to(CreateSubscriptionToken)
|
||||
@@ -947,12 +1109,38 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<CreateOfflineSubscriptionToken>(TYPES.Auth_CreateOfflineSubscriptionToken)
|
||||
.to(CreateOfflineSubscriptionToken)
|
||||
container.bind<CreateValetToken>(TYPES.Auth_CreateValetToken).to(CreateValetToken)
|
||||
container
|
||||
.bind<CreateValetToken>(TYPES.Auth_CreateValetToken)
|
||||
.toConstantValue(
|
||||
new CreateValetToken(
|
||||
container.get<TokenEncoderInterface<ValetTokenData>>(TYPES.Auth_ValetTokenEncoder),
|
||||
container.get<SubscriptionSettingsAssociationServiceInterface>(
|
||||
TYPES.Auth_SubscriptionSettingsAssociationService,
|
||||
),
|
||||
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
|
||||
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
|
||||
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<number>(TYPES.Auth_VALET_TOKEN_TTL),
|
||||
),
|
||||
)
|
||||
container.bind<CreateListedAccount>(TYPES.Auth_CreateListedAccount).to(CreateListedAccount)
|
||||
container.bind<InviteToSharedSubscription>(TYPES.Auth_InviteToSharedSubscription).to(InviteToSharedSubscription)
|
||||
container
|
||||
.bind<AcceptSharedSubscriptionInvitation>(TYPES.Auth_AcceptSharedSubscriptionInvitation)
|
||||
.to(AcceptSharedSubscriptionInvitation)
|
||||
.toConstantValue(
|
||||
new AcceptSharedSubscriptionInvitation(
|
||||
container.get<SharedSubscriptionInvitationRepositoryInterface>(
|
||||
TYPES.Auth_SharedSubscriptionInvitationRepository,
|
||||
),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DeclineSharedSubscriptionInvitation>(TYPES.Auth_DeclineSharedSubscriptionInvitation)
|
||||
.to(DeclineSharedSubscriptionInvitation)
|
||||
@@ -963,15 +1151,32 @@ export class ContainerConfigLoader {
|
||||
.bind<ListSharedSubscriptionInvitations>(TYPES.Auth_ListSharedSubscriptionInvitations)
|
||||
.to(ListSharedSubscriptionInvitations)
|
||||
container.bind<VerifyPredicate>(TYPES.Auth_VerifyPredicate).to(VerifyPredicate)
|
||||
container.bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken).to(CreateCrossServiceToken)
|
||||
container
|
||||
.bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken)
|
||||
.toConstantValue(
|
||||
new CreateCrossServiceToken(
|
||||
container.get<ProjectorInterface<User>>(TYPES.Auth_UserProjector),
|
||||
container.get<ProjectorInterface<Session>>(TYPES.Auth_SessionProjector),
|
||||
container.get<ProjectorInterface<Role>>(TYPES.Auth_RoleProjector),
|
||||
container.get<TokenEncoderInterface<CrossServiceTokenData>>(TYPES.Auth_CrossServiceTokenEncoder),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<number>(TYPES.Auth_AUTH_JWT_TTL),
|
||||
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
|
||||
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Auth_SharedVaultUserRepository),
|
||||
),
|
||||
)
|
||||
container.bind<ProcessUserRequest>(TYPES.Auth_ProcessUserRequest).to(ProcessUserRequest)
|
||||
container
|
||||
.bind<UpdateStorageQuotaUsedForUser>(TYPES.Auth_UpdateStorageQuotaUsedForUser)
|
||||
.toConstantValue(
|
||||
new UpdateStorageQuotaUsedForUser(
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_UserSubscriptionService),
|
||||
container.get(TYPES.Auth_SubscriptionSettingService),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
|
||||
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
|
||||
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
|
||||
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -999,8 +1204,9 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
new DisableEmailSettingBasedOnEmailSubscription(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
|
||||
container.get<SettingFactoryInterface>(TYPES.Auth_SettingFactory),
|
||||
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
|
||||
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
|
||||
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1041,7 +1247,6 @@ export class ContainerConfigLoader {
|
||||
container.bind<UserRequestsController>(TYPES.Auth_UserRequestsController).to(UserRequestsController)
|
||||
|
||||
// Handlers
|
||||
container.bind<UserRegisteredEventHandler>(TYPES.Auth_UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.Auth_AccountDeletionRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
@@ -1055,7 +1260,16 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionPurchasedEventHandler>(TYPES.Auth_SubscriptionPurchasedEventHandler)
|
||||
.to(SubscriptionPurchasedEventHandler)
|
||||
.toConstantValue(
|
||||
new SubscriptionPurchasedEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
|
||||
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionCancelledEventHandler>(TYPES.Auth_SubscriptionCancelledEventHandler)
|
||||
.to(SubscriptionCancelledEventHandler)
|
||||
@@ -1070,16 +1284,42 @@ export class ContainerConfigLoader {
|
||||
.to(SubscriptionExpiredEventHandler)
|
||||
container
|
||||
.bind<SubscriptionSyncRequestedEventHandler>(TYPES.Auth_SubscriptionSyncRequestedEventHandler)
|
||||
.to(SubscriptionSyncRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new SubscriptionSyncRequestedEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
|
||||
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
|
||||
container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
|
||||
container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ExtensionKeyGrantedEventHandler>(TYPES.Auth_ExtensionKeyGrantedEventHandler)
|
||||
.to(ExtensionKeyGrantedEventHandler)
|
||||
.toConstantValue(
|
||||
new ExtensionKeyGrantedEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
|
||||
container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
|
||||
container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionReassignedEventHandler>(TYPES.Auth_SubscriptionReassignedEventHandler)
|
||||
.to(SubscriptionReassignedEventHandler)
|
||||
container
|
||||
.bind<UserEmailChangedEventHandler>(TYPES.Auth_UserEmailChangedEventHandler)
|
||||
.to(UserEmailChangedEventHandler)
|
||||
.toConstantValue(
|
||||
new SubscriptionReassignedEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
|
||||
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler)
|
||||
.toConstantValue(
|
||||
@@ -1122,10 +1362,24 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
container
|
||||
.bind<ListedAccountCreatedEventHandler>(TYPES.Auth_ListedAccountCreatedEventHandler)
|
||||
.to(ListedAccountCreatedEventHandler)
|
||||
.toConstantValue(
|
||||
new ListedAccountCreatedEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ListedAccountDeletedEventHandler>(TYPES.Auth_ListedAccountDeletedEventHandler)
|
||||
.to(ListedAccountDeletedEventHandler)
|
||||
.toConstantValue(
|
||||
new ListedAccountDeletedEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<UserDisabledSessionUserAgentLoggingEventHandler>(TYPES.Auth_UserDisabledSessionUserAgentLoggingEventHandler)
|
||||
.to(UserDisabledSessionUserAgentLoggingEventHandler)
|
||||
@@ -1182,7 +1436,6 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
|
||||
['SUBSCRIPTION_PURCHASED', container.get(TYPES.Auth_SubscriptionPurchasedEventHandler)],
|
||||
['SUBSCRIPTION_CANCELLED', container.get(TYPES.Auth_SubscriptionCancelledEventHandler)],
|
||||
@@ -1192,7 +1445,6 @@ export class ContainerConfigLoader {
|
||||
['SUBSCRIPTION_SYNC_REQUESTED', container.get(TYPES.Auth_SubscriptionSyncRequestedEventHandler)],
|
||||
['EXTENSION_KEY_GRANTED', container.get(TYPES.Auth_ExtensionKeyGrantedEventHandler)],
|
||||
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.Auth_SubscriptionReassignedEventHandler)],
|
||||
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
|
||||
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
|
||||
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
|
||||
['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)],
|
||||
@@ -1343,33 +1595,41 @@ export class ContainerConfigLoader {
|
||||
.bind<BaseSubscriptionTokensController>(TYPES.Auth_BaseSubscriptionTokensController)
|
||||
.toConstantValue(
|
||||
new BaseSubscriptionTokensController(
|
||||
container.get(TYPES.Auth_CreateSubscriptionToken),
|
||||
container.get(TYPES.Auth_AuthenticateSubscriptionToken),
|
||||
container.get(TYPES.Auth_SettingService),
|
||||
container.get(TYPES.Auth_UserProjector),
|
||||
container.get(TYPES.Auth_RoleProjector),
|
||||
container.get(TYPES.Auth_CrossServiceTokenEncoder),
|
||||
container.get(TYPES.Auth_AUTH_JWT_TTL),
|
||||
container.get(TYPES.Auth_ControllerContainer),
|
||||
container.get<CreateSubscriptionToken>(TYPES.Auth_CreateSubscriptionToken),
|
||||
container.get<AuthenticateSubscriptionToken>(TYPES.Auth_AuthenticateSubscriptionToken),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<ProjectorInterface<User>>(TYPES.Auth_UserProjector),
|
||||
container.get<ProjectorInterface<Role>>(TYPES.Auth_RoleProjector),
|
||||
container.get<TokenEncoderInterface<CrossServiceTokenData>>(TYPES.Auth_CrossServiceTokenEncoder),
|
||||
container.get<number>(TYPES.Auth_AUTH_JWT_TTL),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<BaseSubscriptionSettingsController>(TYPES.Auth_BaseSubscriptionSettingsController)
|
||||
.toConstantValue(
|
||||
new BaseSubscriptionSettingsController(
|
||||
container.get(TYPES.Auth_GetSetting),
|
||||
container.get(TYPES.Auth_ControllerContainer),
|
||||
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
|
||||
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
|
||||
container.get<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
|
||||
TYPES.Auth_SubscriptionSettingHttpMapper,
|
||||
),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<BaseSettingsController>(TYPES.Auth_BaseSettingsController)
|
||||
.toConstantValue(
|
||||
new BaseSettingsController(
|
||||
container.get(TYPES.Auth_GetSettings),
|
||||
container.get(TYPES.Auth_GetSetting),
|
||||
container.get(TYPES.Auth_UpdateSetting),
|
||||
container.get(TYPES.Auth_DeleteSetting),
|
||||
container.get(TYPES.Auth_ControllerContainer),
|
||||
container.get<GetAllSettingsForUser>(TYPES.Auth_GetAllSettingsForUser),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
|
||||
container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
|
||||
container.get<MapperInterface<Setting, SettingHttpRepresentation>>(TYPES.Auth_SettingHttpMapper),
|
||||
container.get<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
|
||||
TYPES.Auth_SubscriptionSettingHttpMapper,
|
||||
),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
container
|
||||
|
||||
@@ -5,8 +5,6 @@ import { Role } from '../Domain/Role/Role'
|
||||
import { RevokedSession } from '../Domain/Session/RevokedSession'
|
||||
import { Session } from '../Domain/Session/Session'
|
||||
import { OfflineSetting } from '../Domain/Setting/OfflineSetting'
|
||||
import { Setting } from '../Domain/Setting/Setting'
|
||||
import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
|
||||
import { SharedSubscriptionInvitation } from '../Domain/SharedSubscription/SharedSubscriptionInvitation'
|
||||
import { OfflineUserSubscription } from '../Domain/Subscription/OfflineUserSubscription'
|
||||
import { UserSubscription } from '../Domain/Subscription/UserSubscription'
|
||||
@@ -19,6 +17,8 @@ import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
|
||||
import { Env } from './Env'
|
||||
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
|
||||
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
|
||||
import { TypeORMSubscriptionSetting } from '../Infra/TypeORM/TypeORMSubscriptionSetting'
|
||||
import { TypeORMSetting } from '../Infra/TypeORM/TypeORMSetting'
|
||||
|
||||
export class AppDataSource {
|
||||
private _dataSource: DataSource | undefined
|
||||
@@ -61,10 +61,10 @@ export class AppDataSource {
|
||||
RevokedSession,
|
||||
Role,
|
||||
Permission,
|
||||
Setting,
|
||||
TypeORMSetting,
|
||||
OfflineSetting,
|
||||
SharedSubscriptionInvitation,
|
||||
SubscriptionSetting,
|
||||
TypeORMSubscriptionSetting,
|
||||
TypeORMSessionTrace,
|
||||
TypeORMAuthenticator,
|
||||
TypeORMAuthenticatorChallenge,
|
||||
|
||||
@@ -26,9 +26,11 @@ export class Service implements AuthServiceInterface {
|
||||
|
||||
async activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionId: number
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
cancelPreviousSubscription?: boolean
|
||||
}): Promise<Result<string>> {
|
||||
if (!this.container) {
|
||||
return Result.fail('Container not initialized')
|
||||
|
||||
@@ -10,6 +10,10 @@ const TYPES = {
|
||||
Auth_AuthenticatorHttpMapper: Symbol.for('Auth_AuthenticatorHttpMapper'),
|
||||
Auth_CacheEntryPersistenceMapper: Symbol.for('Auth_CacheEntryPersistenceMapper'),
|
||||
Auth_SharedVaultUserPersistenceMapper: Symbol.for('Auth_SharedVaultUserPersistenceMapper'),
|
||||
Auth_SettingHttpMapper: Symbol.for('Auth_SettingHttpMapper'),
|
||||
Auth_SubscriptionSettingHttpMapper: Symbol.for('Auth_SubscriptionSettingHttpMapper'),
|
||||
Auth_SubscriptionSettingPersistenceMapper: Symbol.for('Auth_SubscriptionSettingPersistenceMapper'),
|
||||
Auth_SettingPersistenceMapper: Symbol.for('Auth_SettingPersistenceMapper'),
|
||||
// Controller
|
||||
Auth_ControllerContainer: Symbol.for('Auth_ControllerContainer'),
|
||||
Auth_AuthController: Symbol.for('Auth_AuthController'),
|
||||
@@ -65,10 +69,6 @@ const TYPES = {
|
||||
Auth_UserProjector: Symbol.for('Auth_UserProjector'),
|
||||
Auth_RoleProjector: Symbol.for('Auth_RoleProjector'),
|
||||
Auth_PermissionProjector: Symbol.for('Auth_PermissionProjector'),
|
||||
Auth_SettingProjector: Symbol.for('Auth_SettingProjector'),
|
||||
Auth_SubscriptionSettingProjector: Symbol.for('Auth_SubscriptionSettingProjector'),
|
||||
// Factories
|
||||
Auth_SettingFactory: Symbol.for('Auth_SettingFactory'),
|
||||
// env vars
|
||||
Auth_JWT_SECRET: Symbol.for('Auth_JWT_SECRET'),
|
||||
Auth_LEGACY_JWT_SECRET: Symbol.for('Auth_LEGACY_JWT_SECRET'),
|
||||
@@ -91,12 +91,7 @@ const TYPES = {
|
||||
Auth_SNS_AWS_REGION: Symbol.for('Auth_SNS_AWS_REGION'),
|
||||
Auth_SQS_QUEUE_URL: Symbol.for('Auth_SQS_QUEUE_URL'),
|
||||
Auth_SQS_AWS_REGION: Symbol.for('Auth_SQS_AWS_REGION'),
|
||||
Auth_USER_SERVER_REGISTRATION_URL: Symbol.for('Auth_USER_SERVER_REGISTRATION_URL'),
|
||||
Auth_USER_SERVER_AUTH_KEY: Symbol.for('Auth_USER_SERVER_AUTH_KEY'),
|
||||
Auth_USER_SERVER_CHANGE_EMAIL_URL: Symbol.for('Auth_USER_SERVER_CHANGE_EMAIL_URL'),
|
||||
Auth_SYNCING_SERVER_URL: Symbol.for('Auth_SYNCING_SERVER_URL'),
|
||||
Auth_VERSION: Symbol.for('Auth_VERSION'),
|
||||
Auth_PAYMENTS_SERVER_URL: Symbol.for('Auth_PAYMENTS_SERVER_URL'),
|
||||
Auth_SESSION_TRACE_DAYS_TTL: Symbol.for('Auth_SESSION_TRACE_DAYS_TTL'),
|
||||
Auth_U2F_RELYING_PARTY_ID: Symbol.for('Auth_U2F_RELYING_PARTY_ID'),
|
||||
Auth_U2F_RELYING_PARTY_NAME: Symbol.for('Auth_U2F_RELYING_PARTY_NAME'),
|
||||
@@ -120,9 +115,12 @@ const TYPES = {
|
||||
Auth_DeleteSessionForUser: Symbol.for('Auth_DeleteSessionForUser'),
|
||||
Auth_ChangeCredentials: Symbol.for('Auth_ChangePassword'),
|
||||
Auth_GetSettings: Symbol.for('Auth_GetSettings'),
|
||||
Auth_GetSubscriptionSettings: Symbol.for('Auth_GetSubscriptionSettings'),
|
||||
Auth_GetRegularSubscriptionForUser: Symbol.for('Auth_GetRegularSubscriptionForUser'),
|
||||
Auth_GetSharedSubscriptionForUser: Symbol.for('Auth_GetSharedSubscriptionForUser'),
|
||||
Auth_GetAllSettingsForUser: Symbol.for('Auth_GetAllSettingsForUser'),
|
||||
Auth_GetSetting: Symbol.for('Auth_GetSetting'),
|
||||
Auth_GetUserFeatures: Symbol.for('Auth_GetUserFeatures'),
|
||||
Auth_UpdateSetting: Symbol.for('Auth_UpdateSetting'),
|
||||
Auth_DeleteSetting: Symbol.for('Auth_DeleteSetting'),
|
||||
Auth_DeleteAccount: Symbol.for('Auth_DeleteAccount'),
|
||||
Auth_GetUserSubscription: Symbol.for('Auth_GetUserSubscription'),
|
||||
@@ -151,7 +149,12 @@ const TYPES = {
|
||||
Auth_VerifyAuthenticatorAuthenticationResponse: Symbol.for('Auth_VerifyAuthenticatorAuthenticationResponse'),
|
||||
Auth_ListAuthenticators: Symbol.for('Auth_ListAuthenticators'),
|
||||
Auth_DeleteAuthenticator: Symbol.for('Auth_DeleteAuthenticator'),
|
||||
Auth_SetSettingValue: Symbol.for('Auth_SetSettingValue'),
|
||||
Auth_GenerateRecoveryCodes: Symbol.for('Auth_GenerateRecoveryCodes'),
|
||||
Auth_GetSubscriptionSetting: Symbol.for('Auth_GetSubscriptionSetting'),
|
||||
Auth_SetSubscriptionSettingValue: Symbol.for('Auth_SetSubscriptionSettingValue'),
|
||||
Auth_ApplyDefaultSubscriptionSettings: Symbol.for('Auth_ApplyDefaultSubscriptionSettings'),
|
||||
Auth_ApplyDefaultSettings: Symbol.for('Auth_ApplyDefaultSettings'),
|
||||
Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'),
|
||||
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
|
||||
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
|
||||
@@ -159,9 +162,9 @@ const TYPES = {
|
||||
Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'),
|
||||
Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'),
|
||||
Auth_DesignateSurvivor: Symbol.for('Auth_DesignateSurvivor'),
|
||||
Auth_GetSharedOrRegularSubscriptionForUser: Symbol.for('Auth_GetSharedOrRegularSubscriptionForUser'),
|
||||
Auth_DisableEmailSettingBasedOnEmailSubscription: Symbol.for('Auth_DisableEmailSettingBasedOnEmailSubscription'),
|
||||
// Handlers
|
||||
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
|
||||
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
|
||||
Auth_SubscriptionPurchasedEventHandler: Symbol.for('Auth_SubscriptionPurchasedEventHandler'),
|
||||
Auth_SubscriptionCancelledEventHandler: Symbol.for('Auth_SubscriptionCancelledEventHandler'),
|
||||
@@ -171,7 +174,6 @@ const TYPES = {
|
||||
Auth_SubscriptionExpiredEventHandler: Symbol.for('Auth_SubscriptionExpiredEventHandler'),
|
||||
Auth_SubscriptionSyncRequestedEventHandler: Symbol.for('Auth_SubscriptionSyncRequestedEventHandler'),
|
||||
Auth_ExtensionKeyGrantedEventHandler: Symbol.for('Auth_ExtensionKeyGrantedEventHandler'),
|
||||
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
|
||||
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
|
||||
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
|
||||
Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
|
||||
@@ -196,8 +198,6 @@ const TYPES = {
|
||||
// Services
|
||||
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
||||
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
||||
Auth_SettingService: Symbol.for('Auth_SettingService'),
|
||||
Auth_SubscriptionSettingService: Symbol.for('Auth_SubscriptionSettingService'),
|
||||
Auth_OfflineSettingService: Symbol.for('Auth_OfflineSettingService'),
|
||||
Auth_AuthResponseFactory20161215: Symbol.for('Auth_AuthResponseFactory20161215'),
|
||||
Auth_AuthResponseFactory20190520: Symbol.for('Auth_AuthResponseFactory20190520'),
|
||||
@@ -218,7 +218,6 @@ const TYPES = {
|
||||
Auth_DomainEventSubscriber: Symbol.for('Auth_DomainEventSubscriber'),
|
||||
Auth_DomainEventFactory: Symbol.for('Auth_DomainEventFactory'),
|
||||
Auth_DomainEventMessageHandler: Symbol.for('Auth_DomainEventMessageHandler'),
|
||||
Auth_HTTPClient: Symbol.for('Auth_HTTPClient'),
|
||||
Auth_Crypter: Symbol.for('Auth_Crypter'),
|
||||
Auth_CryptoNode: Symbol.for('Auth_CryptoNode'),
|
||||
Auth_Timer: Symbol.for('Auth_Timer'),
|
||||
@@ -229,11 +228,10 @@ const TYPES = {
|
||||
Auth_SettingsAssociationService: Symbol.for('Auth_SettingsAssociationService'),
|
||||
Auth_SubscriptionSettingsAssociationService: Symbol.for('Auth_SubscriptionSettingsAssociationService'),
|
||||
Auth_FeatureService: Symbol.for('Auth_FeatureService'),
|
||||
Auth_SettingDecrypter: Symbol.for('Auth_SettingDecrypter'),
|
||||
Auth_SettingCrypter: Symbol.for('Auth_SettingCrypter'),
|
||||
Auth_SettingInterpreter: Symbol.for('Auth_SettingInterpreter'),
|
||||
Auth_ProtocolVersionSelector: Symbol.for('Auth_ProtocolVersionSelector'),
|
||||
Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'),
|
||||
Auth_UserSubscriptionService: Symbol.for('Auth_UserSubscriptionService'),
|
||||
Auth_BaseAuthController: Symbol.for('Auth_BaseAuthController'),
|
||||
Auth_BaseAuthenticatorsController: Symbol.for('Auth_BaseAuthenticatorsController'),
|
||||
Auth_BaseSubscriptionInvitesController: Symbol.for('Auth_BaseSubscriptionInvitesController'),
|
||||
|
||||
@@ -111,7 +111,6 @@ describe('FeatureService', () => {
|
||||
cancelled: false,
|
||||
subscriptionId: 1,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
subscriptionSettings: Promise.resolve([]),
|
||||
}
|
||||
|
||||
subscription2 = {
|
||||
@@ -125,7 +124,6 @@ describe('FeatureService', () => {
|
||||
cancelled: false,
|
||||
subscriptionId: 2,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
subscriptionSettings: Promise.resolve([]),
|
||||
}
|
||||
|
||||
subscription3 = {
|
||||
@@ -139,7 +137,6 @@ describe('FeatureService', () => {
|
||||
cancelled: true,
|
||||
subscriptionId: 3,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
subscriptionSettings: Promise.resolve([]),
|
||||
}
|
||||
|
||||
subscription4 = {
|
||||
@@ -153,7 +150,6 @@ describe('FeatureService', () => {
|
||||
cancelled: true,
|
||||
subscriptionId: 4,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
subscriptionSettings: Promise.resolve([]),
|
||||
}
|
||||
|
||||
user = {
|
||||
@@ -329,7 +325,6 @@ describe('FeatureService', () => {
|
||||
cancelled: false,
|
||||
subscriptionId: 1,
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
subscriptionSettings: Promise.resolve([]),
|
||||
}
|
||||
|
||||
user = {
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ExtensionKeyGrantedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { ExtensionKeyGrantedEventHandler } from './ExtensionKeyGrantedEventHandler'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface'
|
||||
import { ContentDecoderInterface, SubscriptionName } from '@standardnotes/common'
|
||||
|
||||
describe('ExtensionKeyGrantedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let event: ExtensionKeyGrantedEvent
|
||||
let settingService: SettingServiceInterface
|
||||
let offlineSettingService: OfflineSettingServiceInterface
|
||||
let contentDecoder: ContentDecoderInterface
|
||||
let timestamp: number
|
||||
|
||||
const createHandler = () =>
|
||||
new ExtensionKeyGrantedEventHandler(userRepository, settingService, offlineSettingService, contentDecoder, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.createOrReplace = jest.fn()
|
||||
|
||||
offlineSettingService = {} as jest.Mocked<OfflineSettingServiceInterface>
|
||||
offlineSettingService.createOrUpdate = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
|
||||
event = {} as jest.Mocked<ExtensionKeyGrantedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userEmail: 'test@test.com',
|
||||
extensionKey: 'abc123',
|
||||
offline: false,
|
||||
offlineFeaturesToken: 'test',
|
||||
subscriptionName: SubscriptionName.ProPlan,
|
||||
origin: 'update-subscription',
|
||||
timestamp,
|
||||
payAmount: 1000,
|
||||
billingEveryNMonths: 1,
|
||||
activeUntil: new Date(10).toString(),
|
||||
}
|
||||
|
||||
contentDecoder = {} as jest.Mocked<ContentDecoderInterface>
|
||||
contentDecoder.decode = jest.fn().mockReturnValue({
|
||||
featuresUrl: 'http://features-url',
|
||||
extensionKey: 'key',
|
||||
})
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should add extension key as an user offline features token for offline user setting', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(offlineSettingService.createOrUpdate).toHaveBeenCalledWith({
|
||||
email: 'test@test.com',
|
||||
name: 'FEATURES_TOKEN',
|
||||
value: 'key',
|
||||
})
|
||||
})
|
||||
|
||||
it('should add extension key as an user offline features token if not possible to decode', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
contentDecoder.decode = jest.fn().mockReturnValue({})
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(offlineSettingService.createOrUpdate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should add extension key as user setting', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
name: 'EXTENSION_KEY',
|
||||
serverEncryptionVersion: 1,
|
||||
unencryptedValue: 'abc123',
|
||||
sensitive: true,
|
||||
},
|
||||
user: {
|
||||
uuid: '123',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should not do anything if no user is found for specified email', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if user email is invalid', async () => {
|
||||
event.payload.userEmail = ''
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,26 +1,21 @@
|
||||
import { DomainEventHandlerInterface, ExtensionKeyGrantedEvent } from '@standardnotes/domain-events'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { SettingName, Username } from '@standardnotes/domain-core'
|
||||
import { OfflineFeaturesTokenData } from '@standardnotes/security'
|
||||
import { ContentDecoderInterface } from '@standardnotes/common'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface'
|
||||
import { OfflineSettingName } from '../Setting/OfflineSettingName'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
|
||||
|
||||
@injectable()
|
||||
export class ExtensionKeyGrantedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Auth_OfflineSettingService) private offlineSettingService: OfflineSettingServiceInterface,
|
||||
@inject(TYPES.Auth_ContenDecoder) private contentDecoder: ContentDecoderInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private setSettingValue: SetSettingValue,
|
||||
private offlineSettingService: OfflineSettingServiceInterface,
|
||||
private contentDecoder: ContentDecoderInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: ExtensionKeyGrantedEvent): Promise<void> {
|
||||
@@ -58,14 +53,14 @@ export class ExtensionKeyGrantedEventHandler implements DomainEventHandlerInterf
|
||||
return
|
||||
}
|
||||
|
||||
await this.settingService.createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.ExtensionKey,
|
||||
unencryptedValue: event.payload.extensionKey,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
sensitive: true,
|
||||
},
|
||||
const result = await this.setSettingValue.execute({
|
||||
userUuid: user.uuid,
|
||||
settingName: SettingName.NAMES.ExtensionKey,
|
||||
value: event.payload.extensionKey,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not set extension key for user ${user.uuid}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { ListedAccountCreatedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ListedAccountCreatedEventHandler } from './ListedAccountCreatedEventHandler'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
|
||||
describe('ListedAccountCreatedEventHandler', () => {
|
||||
let settingService: SettingServiceInterface
|
||||
let userRepository: UserRepositoryInterface
|
||||
let event: ListedAccountCreatedEvent
|
||||
let user: User
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () => new ListedAccountCreatedEventHandler(userRepository, settingService, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
settingService.createOrReplace = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<ListedAccountCreatedEvent>
|
||||
event.payload = {
|
||||
userEmail: 'test@test.com',
|
||||
userId: 1,
|
||||
userName: 'testuser',
|
||||
secret: 'new-secret',
|
||||
hostUrl: 'https://dev.listed.to',
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should not save the listed secret if username is invalid', async () => {
|
||||
event.payload.userEmail = ''
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not save the listed secret if user is not found', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save the listed secret as a user setting', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).toHaveBeenCalledWith({
|
||||
user,
|
||||
props: {
|
||||
name: 'LISTED_AUTHOR_SECRETS',
|
||||
sensitive: false,
|
||||
unencryptedValue: '[{"authorId":1,"secret":"new-secret","hostUrl":"https://dev.listed.to"}]',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should add the listed secret as a user setting to an existing list of secrets', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: '[{"authorId":2,"secret":"old-secret","hostUrl":"https://dev.listed.to"}]',
|
||||
} as jest.Mocked<Setting>)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).toHaveBeenCalledWith({
|
||||
user,
|
||||
props: {
|
||||
name: 'LISTED_AUTHOR_SECRETS',
|
||||
sensitive: false,
|
||||
unencryptedValue:
|
||||
'[{"authorId":2,"secret":"old-secret","hostUrl":"https://dev.listed.to"},{"authorId":1,"secret":"new-secret","hostUrl":"https://dev.listed.to"}]',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,18 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { SettingName, Username } from '@standardnotes/domain-core'
|
||||
import { DomainEventHandlerInterface, ListedAccountCreatedEvent } from '@standardnotes/domain-events'
|
||||
import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { ListedAuthorSecretsData } from '@standardnotes/settings'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { GetSetting } from '../UseCase/GetSetting/GetSetting'
|
||||
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
|
||||
|
||||
@injectable()
|
||||
export class ListedAccountCreatedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private getSetting: GetSetting,
|
||||
private setSettingValue: SetSettingValue,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: ListedAccountCreatedEvent): Promise<void> {
|
||||
@@ -34,23 +33,27 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter
|
||||
|
||||
let authSecrets: ListedAuthorSecretsData = [newSecret]
|
||||
|
||||
const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({
|
||||
settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(),
|
||||
const listedAuthorSecretsSettingOrError = await this.getSetting.execute({
|
||||
settingName: SettingName.NAMES.ListedAuthorSecrets,
|
||||
userUuid: user.uuid,
|
||||
decrypted: true,
|
||||
allowSensitiveRetrieval: false,
|
||||
})
|
||||
if (listedAuthorSecretsSetting !== null) {
|
||||
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.value as string)
|
||||
if (!listedAuthorSecretsSettingOrError.isFailed()) {
|
||||
const listedAuthorSecretsSetting = listedAuthorSecretsSettingOrError.getValue()
|
||||
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.decryptedValue as string)
|
||||
existingSecrets.push(newSecret)
|
||||
authSecrets = existingSecrets
|
||||
}
|
||||
|
||||
await this.settingService.createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.ListedAuthorSecrets,
|
||||
unencryptedValue: JSON.stringify(authSecrets),
|
||||
sensitive: false,
|
||||
},
|
||||
const result = await this.setSettingValue.execute({
|
||||
userUuid: user.uuid,
|
||||
settingName: SettingName.NAMES.ListedAuthorSecrets,
|
||||
value: JSON.stringify(authSecrets),
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not update listed author secrets for user with uuid ${user.uuid}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { ListedAccountDeletedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ListedAccountDeletedEventHandler } from './ListedAccountDeletedEventHandler'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
|
||||
describe('ListedAccountDeletedEventHandler', () => {
|
||||
let settingService: SettingServiceInterface
|
||||
let userRepository: UserRepositoryInterface
|
||||
let event: ListedAccountDeletedEvent
|
||||
let user: User
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () => new ListedAccountDeletedEventHandler(userRepository, settingService, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: '[{"authorId":1,"secret":"my-secret","hostUrl":"https://dev.listed.to"}]',
|
||||
} as jest.Mocked<Setting>)
|
||||
settingService.createOrReplace = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<ListedAccountDeletedEvent>
|
||||
event.payload = {
|
||||
userEmail: 'test@test.com',
|
||||
userId: 1,
|
||||
userName: 'testuser',
|
||||
secret: 'my-secret',
|
||||
hostUrl: 'https://dev.listed.to',
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should not remove the listed secret if username is invalid', async () => {
|
||||
event.payload.userEmail = ''
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not remove the listed secret if user is not found', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not remove the listed secret if setting is not found', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should remove the listed secret from the user setting', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).toHaveBeenCalledWith({
|
||||
user,
|
||||
props: {
|
||||
name: 'LISTED_AUTHOR_SECRETS',
|
||||
sensitive: false,
|
||||
unencryptedValue: '[]',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove the listed secret from an existing list of secrets', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value:
|
||||
'[{"authorId":2,"secret":"old-secret","hostUrl":"https://dev.listed.to"},{"authorId":1,"secret":"my-secret","hostUrl":"https://dev.listed.to"},{"authorId":1,"secret":"my-secret","hostUrl":"https://local.listed.to"}]',
|
||||
} as jest.Mocked<Setting>)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).toHaveBeenCalledWith({
|
||||
user,
|
||||
props: {
|
||||
name: 'LISTED_AUTHOR_SECRETS',
|
||||
sensitive: false,
|
||||
unencryptedValue:
|
||||
'[{"authorId":2,"secret":"old-secret","hostUrl":"https://dev.listed.to"},{"authorId":1,"secret":"my-secret","hostUrl":"https://local.listed.to"}]',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,18 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { SettingName, Username } from '@standardnotes/domain-core'
|
||||
import { DomainEventHandlerInterface, ListedAccountDeletedEvent } from '@standardnotes/domain-events'
|
||||
import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { ListedAuthorSecretsData } from '@standardnotes/settings'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { GetSetting } from '../UseCase/GetSetting/GetSetting'
|
||||
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
|
||||
|
||||
@injectable()
|
||||
export class ListedAccountDeletedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private getSetting: GetSetting,
|
||||
private setSettingValue: SetSettingValue,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: ListedAccountDeletedEvent): Promise<void> {
|
||||
@@ -31,30 +30,35 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter
|
||||
return
|
||||
}
|
||||
|
||||
const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({
|
||||
settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(),
|
||||
const listedAuthorSecretsSettingOrError = await this.getSetting.execute({
|
||||
settingName: SettingName.NAMES.ListedAuthorSecrets,
|
||||
decrypted: true,
|
||||
userUuid: user.uuid,
|
||||
allowSensitiveRetrieval: false,
|
||||
})
|
||||
if (listedAuthorSecretsSetting === null) {
|
||||
this.logger.warn(`Could not find listed secrets setting for user ${user.uuid}`)
|
||||
if (listedAuthorSecretsSettingOrError.isFailed()) {
|
||||
this.logger.error(`Could not find listed secrets setting for user ${user.uuid}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.value as string)
|
||||
const listedAuthorSecretsSetting = listedAuthorSecretsSettingOrError.getValue()
|
||||
|
||||
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.decryptedValue as string)
|
||||
const filteredSecrets = existingSecrets.filter(
|
||||
(secret) =>
|
||||
secret.authorId !== event.payload.userId ||
|
||||
(secret.authorId === event.payload.userId && secret.hostUrl !== event.payload.hostUrl),
|
||||
)
|
||||
|
||||
await this.settingService.createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.ListedAuthorSecrets,
|
||||
unencryptedValue: JSON.stringify(filteredSecrets),
|
||||
sensitive: false,
|
||||
},
|
||||
const result = await this.setSettingValue.execute({
|
||||
settingName: SettingName.NAMES.ListedAuthorSecrets,
|
||||
value: JSON.stringify(filteredSecrets),
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not update listed author secrets for user with uuid ${user.uuid}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Logger } from 'winston'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { PaymentsAccountDeletedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import { DeleteAccount } from '../UseCase/DeleteAccount/DeleteAccount'
|
||||
import { PaymentsAccountDeletedEventHandler } from './PaymentsAccountDeletedEventHandler'
|
||||
|
||||
describe('PaymentsAccountDeletedEventHandler', () => {
|
||||
let deleteAccountUseCase: DeleteAccount
|
||||
let logger: Logger
|
||||
let event: PaymentsAccountDeletedEvent
|
||||
|
||||
const createHandler = () => new PaymentsAccountDeletedEventHandler(deleteAccountUseCase, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
deleteAccountUseCase = {} as jest.Mocked<DeleteAccount>
|
||||
deleteAccountUseCase.execute = jest.fn().mockResolvedValue(Result.ok('success'))
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
|
||||
event = {
|
||||
payload: {
|
||||
username: 'username',
|
||||
},
|
||||
} as jest.Mocked<PaymentsAccountDeletedEvent>
|
||||
})
|
||||
|
||||
it('should delete account', async () => {
|
||||
const handler = createHandler()
|
||||
|
||||
await handler.handle(event)
|
||||
|
||||
expect(deleteAccountUseCase.execute).toHaveBeenCalledWith({
|
||||
username: 'username',
|
||||
})
|
||||
})
|
||||
|
||||
it('should log error if delete account fails', async () => {
|
||||
const handler = createHandler()
|
||||
|
||||
deleteAccountUseCase.execute = jest.fn().mockResolvedValue(Result.fail('error'))
|
||||
|
||||
await handler.handle(event)
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith('Failed to delete account for user username: error')
|
||||
})
|
||||
})
|
||||
@@ -1,131 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
DomainEventService,
|
||||
PredicateVerificationRequestedEvent,
|
||||
PredicateVerificationRequestedEventPayload,
|
||||
PredicateVerifiedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { VerifyPredicate } from '../UseCase/VerifyPredicate/VerifyPredicate'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
import { PredicateVerificationRequestedEventHandler } from './PredicateVerificationRequestedEventHandler'
|
||||
import { User } from '../User/User'
|
||||
|
||||
describe('PredicateVerificationRequestedEventHandler', () => {
|
||||
let verifyPredicate: VerifyPredicate
|
||||
let userRepository: UserRepositoryInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let logger: Logger
|
||||
let event: PredicateVerificationRequestedEvent
|
||||
|
||||
const createHandler = () =>
|
||||
new PredicateVerificationRequestedEventHandler(
|
||||
verifyPredicate,
|
||||
userRepository,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
verifyPredicate = {} as jest.Mocked<VerifyPredicate>
|
||||
verifyPredicate.execute = jest
|
||||
.fn()
|
||||
.mockReturnValue({ predicateVerificationResult: PredicateVerificationResult.Affirmed })
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue({ uuid: '1-2-3' } as jest.Mocked<User>)
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createPredicateVerifiedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<PredicateVerifiedEvent>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
logger.info = jest.fn()
|
||||
logger.debug = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<PredicateVerificationRequestedEvent>
|
||||
event.meta = {
|
||||
correlation: {
|
||||
userIdentifier: '2-3-4',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
}
|
||||
event.payload = {
|
||||
predicate: {} as jest.Mocked<Predicate>,
|
||||
} as jest.Mocked<PredicateVerificationRequestedEventPayload>
|
||||
})
|
||||
|
||||
it('should verify a predicate by user uuid', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(verifyPredicate.execute).toHaveBeenCalledWith({
|
||||
predicate: event.payload.predicate,
|
||||
userUuid: '2-3-4',
|
||||
})
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should verify a predicate by user email', async () => {
|
||||
event.meta = {
|
||||
correlation: {
|
||||
userIdentifier: 'test@test.te',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
}
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(verifyPredicate.execute).toHaveBeenCalledWith({
|
||||
predicate: event.payload.predicate,
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should do nothing if username is invalid', async () => {
|
||||
event.meta = {
|
||||
correlation: {
|
||||
userIdentifier: ' ',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
}
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(verifyPredicate.execute).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should mark a predicate verification with undetermined result if user is missing', async () => {
|
||||
event.meta = {
|
||||
correlation: {
|
||||
userIdentifier: 'test@test.te',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
}
|
||||
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(verifyPredicate.execute).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,45 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SharedSubscriptionInvitationCreatedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||
import { AcceptSharedSubscriptionInvitation } from '../UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation'
|
||||
|
||||
import { SharedSubscriptionInvitationCreatedEventHandler } from './SharedSubscriptionInvitationCreatedEventHandler'
|
||||
|
||||
describe('SharedSubscriptionInvitationCreatedEventHandler', () => {
|
||||
let acceptSharedSubscriptionInvitation: AcceptSharedSubscriptionInvitation
|
||||
|
||||
const createHandler = () => new SharedSubscriptionInvitationCreatedEventHandler(acceptSharedSubscriptionInvitation)
|
||||
|
||||
beforeEach(() => {
|
||||
acceptSharedSubscriptionInvitation = {} as jest.Mocked<AcceptSharedSubscriptionInvitation>
|
||||
acceptSharedSubscriptionInvitation.execute = jest.fn()
|
||||
})
|
||||
|
||||
it('should accept automatically invitation for hash invitees', async () => {
|
||||
const event = {
|
||||
payload: {
|
||||
inviteeIdentifierType: InviteeIdentifierType.Hash,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
},
|
||||
} as jest.Mocked<SharedSubscriptionInvitationCreatedEvent>
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(acceptSharedSubscriptionInvitation.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not accept automatically invitation for email invitees', async () => {
|
||||
const event = {
|
||||
payload: {
|
||||
inviteeIdentifierType: InviteeIdentifierType.Email,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
},
|
||||
} as jest.Mocked<SharedSubscriptionInvitationCreatedEvent>
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(acceptSharedSubscriptionInvitation.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { SubscriptionCancelledEventHandler } from './SubscriptionCancelledEventHandler'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
|
||||
describe('SubscriptionCancelledEventHandler', () => {
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
|
||||
let event: SubscriptionCancelledEvent
|
||||
let timestamp: number
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionCancelledEventHandler(userSubscriptionRepository, offlineUserSubscriptionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.updateCancelled = jest.fn()
|
||||
|
||||
offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
|
||||
offlineUserSubscriptionRepository.updateCancelled = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
|
||||
event = {} as jest.Mocked<SubscriptionCancelledEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
subscriptionId: 1,
|
||||
userEmail: 'test@test.com',
|
||||
subscriptionName: SubscriptionName.ProPlan,
|
||||
timestamp,
|
||||
offline: false,
|
||||
replaced: false,
|
||||
subscriptionCreatedAt: 1,
|
||||
subscriptionEndsAt: 2,
|
||||
subscriptionUpdatedAt: 2,
|
||||
lastPayedAt: 1,
|
||||
userExistingSubscriptionsCount: 1,
|
||||
billingFrequency: 1,
|
||||
payAmount: 12.99,
|
||||
}
|
||||
})
|
||||
|
||||
it('should update subscription cancelled', async () => {
|
||||
event.payload.timestamp = 1642395451516000
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(userSubscriptionRepository.updateCancelled).toHaveBeenCalledWith(1, true, 1642395451516000)
|
||||
})
|
||||
|
||||
it('should update offline subscription cancelled', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(offlineUserSubscriptionRepository.updateCancelled).toHaveBeenCalledWith(1, true, timestamp)
|
||||
})
|
||||
})
|
||||
@@ -1,123 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { SubscriptionExpiredEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { SubscriptionExpiredEventHandler } from './SubscriptionExpiredEventHandler'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
|
||||
describe('SubscriptionExpiredEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let event: SubscriptionExpiredEvent
|
||||
let timestamp: number
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionExpiredEventHandler(
|
||||
userRepository,
|
||||
userSubscriptionRepository,
|
||||
offlineUserSubscriptionRepository,
|
||||
roleService,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.NAMES.ProUser,
|
||||
},
|
||||
]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.updateEndsAt = jest.fn()
|
||||
userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13)
|
||||
userSubscriptionRepository.findBySubscriptionId = jest
|
||||
.fn()
|
||||
.mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked<UserSubscription>])
|
||||
|
||||
offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
|
||||
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.removeUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
|
||||
event = {} as jest.Mocked<SubscriptionExpiredEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
subscriptionId: 1,
|
||||
userEmail: 'test@test.com',
|
||||
subscriptionName: SubscriptionName.PlusPlan,
|
||||
timestamp,
|
||||
offline: false,
|
||||
totalActiveSubscriptionsCount: 123,
|
||||
userExistingSubscriptionsCount: 2,
|
||||
billingFrequency: 1,
|
||||
payAmount: 12.99,
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
})
|
||||
|
||||
it('should update subscription ends at', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(userSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, timestamp, timestamp)
|
||||
})
|
||||
|
||||
it('should update offline subscription ends at', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(offlineUserSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, timestamp, timestamp)
|
||||
})
|
||||
|
||||
it('should not do anything if no user is found for specified email', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if username is invalid', async () => {
|
||||
event.payload.userEmail = ' '
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,177 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { SubscriptionPurchasedEventHandler } from './SubscriptionPurchasedEventHandler'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
|
||||
describe('SubscriptionPurchasedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let offlineUserSubscription: OfflineUserSubscription
|
||||
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let subscription: UserSubscription
|
||||
let event: SubscriptionPurchasedEvent
|
||||
let subscriptionExpiresAt: number
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let timestamp: number
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionPurchasedEventHandler(
|
||||
userRepository,
|
||||
userSubscriptionRepository,
|
||||
offlineUserSubscriptionRepository,
|
||||
roleService,
|
||||
subscriptionSettingService,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.NAMES.CoreUser,
|
||||
},
|
||||
]),
|
||||
} as jest.Mocked<User>
|
||||
subscription = {
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(0)
|
||||
userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13)
|
||||
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
|
||||
|
||||
offlineUserSubscription = {} as jest.Mocked<OfflineUserSubscription>
|
||||
|
||||
offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
|
||||
offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(offlineUserSubscription)
|
||||
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
roleService.setOfflineUserRole = jest.fn()
|
||||
|
||||
subscriptionExpiresAt = timestamp + 365 * 1000
|
||||
|
||||
event = {} as jest.Mocked<SubscriptionPurchasedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
subscriptionId: 1,
|
||||
userEmail: 'test@test.com',
|
||||
subscriptionName: SubscriptionName.ProPlan,
|
||||
subscriptionExpiresAt,
|
||||
timestamp: dayjs.utc().valueOf(),
|
||||
offline: false,
|
||||
discountCode: null,
|
||||
limitedDiscountPurchased: false,
|
||||
newSubscriber: true,
|
||||
totalActiveSubscriptionsCount: 123,
|
||||
userRegisteredAt: dayjs.utc().valueOf() - 23,
|
||||
billingFrequency: 12,
|
||||
payAmount: 29.99,
|
||||
}
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should update user default settings', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
subscription,
|
||||
)
|
||||
})
|
||||
|
||||
it('should update the offline user role', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.setOfflineUserRole).toHaveBeenCalledWith(offlineUserSubscription)
|
||||
})
|
||||
|
||||
it('should create subscription', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
subscription.planName = SubscriptionName.ProPlan
|
||||
subscription.endsAt = subscriptionExpiresAt
|
||||
subscription.subscriptionId = 1
|
||||
subscription.user = Promise.resolve(user)
|
||||
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalledWith({
|
||||
...subscription,
|
||||
createdAt: expect.any(Number),
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create an offline subscription', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(offlineUserSubscriptionRepository.save).toHaveBeenCalledWith({
|
||||
endsAt: subscriptionExpiresAt,
|
||||
subscriptionId: 1,
|
||||
planName: 'PRO_PLAN',
|
||||
email: 'test@test.com',
|
||||
createdAt: expect.any(Number),
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not do anything if no user is found for specified email', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if username is invalid', async () => {
|
||||
event.payload.userEmail = ' '
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,7 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
@@ -11,21 +10,16 @@ import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscri
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_UserSubscriptionRepository)
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
|
||||
private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings,
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingService)
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
private roleService: RoleServiceInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
||||
@@ -66,7 +60,15 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
|
||||
await this.addUserRole(user, event.payload.subscriptionName)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
const result = await this.applyDefaultSubscriptionSettings.execute({
|
||||
userSubscriptionUuid: userSubscription.uuid,
|
||||
userUuid: user.uuid,
|
||||
subscriptionPlanName: event.payload.subscriptionName,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not apply default subscription settings for user ${user.uuid}: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { SubscriptionReassignedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { SubscriptionReassignedEventHandler } from './SubscriptionReassignedEventHandler'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
|
||||
describe('SubscriptionReassignedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let subscription: UserSubscription
|
||||
let event: SubscriptionReassignedEvent
|
||||
let subscriptionExpiresAt: number
|
||||
let timestamp: number
|
||||
let settingService: SettingServiceInterface
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionReassignedEventHandler(
|
||||
userRepository,
|
||||
userSubscriptionRepository,
|
||||
roleService,
|
||||
settingService,
|
||||
subscriptionSettingService,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.NAMES.CoreUser,
|
||||
},
|
||||
]),
|
||||
} as jest.Mocked<User>
|
||||
subscription = {
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
subscriptionExpiresAt = timestamp + 365 * 1000
|
||||
|
||||
event = {} as jest.Mocked<SubscriptionReassignedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
subscriptionId: 1,
|
||||
offline: false,
|
||||
extensionKey: 'abc123',
|
||||
userEmail: 'test@test.com',
|
||||
subscriptionName: SubscriptionName.ProPlan,
|
||||
subscriptionExpiresAt,
|
||||
timestamp: dayjs.utc().valueOf(),
|
||||
}
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.createOrReplace = jest.fn()
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should update user default settings', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
subscription,
|
||||
)
|
||||
})
|
||||
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should create subscription', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
subscription.planName = SubscriptionName.ProPlan
|
||||
subscription.endsAt = subscriptionExpiresAt
|
||||
subscription.subscriptionId = 1
|
||||
subscription.user = Promise.resolve(user)
|
||||
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalledWith({
|
||||
...subscription,
|
||||
createdAt: expect.any(Number),
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create an extension key setting for the user', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
name: 'EXTENSION_KEY',
|
||||
serverEncryptionVersion: 1,
|
||||
unencryptedValue: 'abc123',
|
||||
sensitive: true,
|
||||
},
|
||||
user: {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.NAMES.CoreUser,
|
||||
},
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should not do anything if no user is found for specified email', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if username is invalid', async () => {
|
||||
event.payload.userEmail = ' '
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,31 +1,24 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionReassignedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { SettingName, Username } from '@standardnotes/domain-core'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionReassignedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_UserSubscriptionRepository)
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface,
|
||||
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingService)
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
private roleService: RoleServiceInterface,
|
||||
private logger: Logger,
|
||||
private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings,
|
||||
private setSettingValue: SetSettingValue,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionReassignedEvent): Promise<void> {
|
||||
@@ -53,17 +46,25 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
|
||||
|
||||
await this.addUserRole(user, event.payload.subscriptionName)
|
||||
|
||||
await this.settingService.createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.ExtensionKey,
|
||||
unencryptedValue: event.payload.extensionKey,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
sensitive: true,
|
||||
},
|
||||
const result = await this.setSettingValue.execute({
|
||||
userUuid: user.uuid,
|
||||
settingName: SettingName.NAMES.ExtensionKey,
|
||||
value: event.payload.extensionKey,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not set extension key for user ${user.uuid}`)
|
||||
}
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({
|
||||
subscriptionPlanName: event.payload.subscriptionName,
|
||||
userUuid: user.uuid,
|
||||
userSubscriptionUuid: userSubscription.uuid,
|
||||
})
|
||||
if (applyingSettingsResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`Could not apply default subscription settings for user ${user.uuid}: ${applyingSettingsResult.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { SubscriptionRefundedEventHandler } from './SubscriptionRefundedEventHandler'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
|
||||
describe('SubscriptionRefundedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let event: SubscriptionRefundedEvent
|
||||
let timestamp: number
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionRefundedEventHandler(
|
||||
userRepository,
|
||||
userSubscriptionRepository,
|
||||
offlineUserSubscriptionRepository,
|
||||
roleService,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.NAMES.ProUser,
|
||||
},
|
||||
]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.updateEndsAt = jest.fn()
|
||||
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(1)
|
||||
userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13)
|
||||
userSubscriptionRepository.findBySubscriptionId = jest
|
||||
.fn()
|
||||
.mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked<UserSubscription>])
|
||||
|
||||
offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
|
||||
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.removeUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
|
||||
event = {} as jest.Mocked<SubscriptionRefundedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
subscriptionId: 1,
|
||||
userEmail: 'test@test.com',
|
||||
subscriptionName: SubscriptionName.PlusPlan,
|
||||
timestamp,
|
||||
offline: false,
|
||||
userExistingSubscriptionsCount: 3,
|
||||
totalActiveSubscriptionsCount: 1,
|
||||
billingFrequency: 1,
|
||||
payAmount: 12.99,
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
})
|
||||
|
||||
it('should update subscription ends at', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(userSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, timestamp, timestamp)
|
||||
})
|
||||
|
||||
it('should update offline subscription ends at', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(offlineUserSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, timestamp, timestamp)
|
||||
})
|
||||
|
||||
it('should not do anything if no user is found for specified email', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if username is invalid', async () => {
|
||||
event.payload.userEmail = ' '
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,149 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
||||
import * as dayjs from 'dayjs'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { SubscriptionRenewedEventHandler } from './SubscriptionRenewedEventHandler'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
|
||||
describe('SubscriptionRenewedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let offlineUserSubscription: OfflineUserSubscription
|
||||
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let subscription: UserSubscription
|
||||
let event: SubscriptionRenewedEvent
|
||||
let subscriptionExpiresAt: number
|
||||
let timestamp: number
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionRenewedEventHandler(
|
||||
userRepository,
|
||||
userSubscriptionRepository,
|
||||
offlineUserSubscriptionRepository,
|
||||
roleService,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.NAMES.CoreUser,
|
||||
},
|
||||
]),
|
||||
} as jest.Mocked<User>
|
||||
subscription = {} as jest.Mocked<UserSubscription>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.updateEndsAt = jest.fn()
|
||||
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
|
||||
userSubscriptionRepository.findBySubscriptionId = jest
|
||||
.fn()
|
||||
.mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked<UserSubscription>])
|
||||
|
||||
offlineUserSubscription = {} as jest.Mocked<OfflineUserSubscription>
|
||||
|
||||
offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
|
||||
offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(offlineUserSubscription)
|
||||
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
roleService.setOfflineUserRole = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
subscriptionExpiresAt = dayjs.utc().valueOf() + 365 * 1000
|
||||
|
||||
event = {} as jest.Mocked<SubscriptionRenewedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
subscriptionId: 1,
|
||||
userEmail: 'test@test.com',
|
||||
subscriptionName: SubscriptionName.ProPlan,
|
||||
subscriptionExpiresAt,
|
||||
timestamp,
|
||||
offline: false,
|
||||
billingFrequency: 1,
|
||||
payAmount: 12.99,
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should update subscription ends at', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(userSubscriptionRepository.updateEndsAt).toHaveBeenCalledWith(1, subscriptionExpiresAt, timestamp)
|
||||
})
|
||||
|
||||
it('should update offline subscription ends at', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(offlineUserSubscriptionRepository.save).toHaveBeenCalledWith(offlineUserSubscription)
|
||||
})
|
||||
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should update the offline user role', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.setOfflineUserRole).toHaveBeenCalledWith(offlineUserSubscription)
|
||||
})
|
||||
|
||||
it('should not do anything if no user is found for specified email', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if username is invalid', async () => {
|
||||
event.payload.userEmail = ' '
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if no offline subscription is found for specified id', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,258 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ContentDecoderInterface, SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { SubscriptionSyncRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import * as dayjs from 'dayjs'
|
||||
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { SubscriptionSyncRequestedEventHandler } from './SubscriptionSyncRequestedEventHandler'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
|
||||
describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let offlineUserSubscription: OfflineUserSubscription
|
||||
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let subscription: UserSubscription
|
||||
let event: SubscriptionSyncRequestedEvent
|
||||
let subscriptionExpiresAt: number
|
||||
let settingService: SettingServiceInterface
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let timestamp: number
|
||||
let offlineSettingService: OfflineSettingServiceInterface
|
||||
let contentDecoder: ContentDecoderInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionSyncRequestedEventHandler(
|
||||
userRepository,
|
||||
userSubscriptionRepository,
|
||||
offlineUserSubscriptionRepository,
|
||||
roleService,
|
||||
settingService,
|
||||
subscriptionSettingService,
|
||||
offlineSettingService,
|
||||
contentDecoder,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.NAMES.CoreUser,
|
||||
},
|
||||
]),
|
||||
} as jest.Mocked<User>
|
||||
subscription = {
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([])
|
||||
|
||||
offlineUserSubscription = {} as jest.Mocked<OfflineUserSubscription>
|
||||
|
||||
offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
|
||||
offlineUserSubscriptionRepository.findOneBySubscriptionId = jest.fn().mockReturnValue(null)
|
||||
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
|
||||
|
||||
offlineSettingService = {} as jest.Mocked<OfflineSettingServiceInterface>
|
||||
offlineSettingService.createOrUpdate = jest.fn()
|
||||
|
||||
contentDecoder = {} as jest.Mocked<ContentDecoderInterface>
|
||||
contentDecoder.decode = jest.fn().mockReturnValue({
|
||||
featuresUrl: 'http://features-url',
|
||||
extensionKey: 'key',
|
||||
})
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
roleService.setOfflineUserRole = jest.fn()
|
||||
|
||||
subscriptionExpiresAt = timestamp + 365 * 1000
|
||||
|
||||
event = {} as jest.Mocked<SubscriptionSyncRequestedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
subscriptionId: 1,
|
||||
userEmail: 'test@test.com',
|
||||
subscriptionName: SubscriptionName.ProPlan,
|
||||
subscriptionExpiresAt,
|
||||
timestamp: dayjs.utc().valueOf(),
|
||||
offline: false,
|
||||
extensionKey: 'abc123',
|
||||
offlineFeaturesToken: 'test',
|
||||
canceled: false,
|
||||
}
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.createOrReplace = jest.fn()
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should update user default settings', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
subscription,
|
||||
)
|
||||
|
||||
expect(settingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
name: 'EXTENSION_KEY',
|
||||
serverEncryptionVersion: 1,
|
||||
unencryptedValue: 'abc123',
|
||||
sensitive: true,
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.NAMES.CoreUser,
|
||||
},
|
||||
]),
|
||||
uuid: '123',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the offline user role', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.setOfflineUserRole).toHaveBeenCalledWith(offlineUserSubscription)
|
||||
})
|
||||
|
||||
it('should not update the offline user features token if it is not possible to decode the extension key', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
contentDecoder.decode = jest.fn().mockReturnValue({})
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(settingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create subscription', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
subscription.planName = SubscriptionName.ProPlan
|
||||
subscription.endsAt = subscriptionExpiresAt
|
||||
subscription.subscriptionId = 1
|
||||
subscription.user = Promise.resolve(user)
|
||||
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalledWith({
|
||||
...subscription,
|
||||
createdAt: expect.any(Number),
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should update an existing subscription', async () => {
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest
|
||||
.fn()
|
||||
.mockReturnValue([{} as jest.Mocked<UserSubscription>])
|
||||
await createHandler().handle(event)
|
||||
|
||||
subscription.planName = SubscriptionName.ProPlan
|
||||
subscription.endsAt = subscriptionExpiresAt
|
||||
subscription.subscriptionId = 1
|
||||
subscription.user = Promise.resolve(user)
|
||||
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalledWith({
|
||||
...subscription,
|
||||
createdAt: expect.any(Number),
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create an offline subscription', async () => {
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(offlineUserSubscriptionRepository.save).toHaveBeenCalledWith({
|
||||
endsAt: subscriptionExpiresAt,
|
||||
subscriptionId: 1,
|
||||
planName: 'PRO_PLAN',
|
||||
email: 'test@test.com',
|
||||
createdAt: expect.any(Number),
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should update an offline subscription', async () => {
|
||||
offlineUserSubscriptionRepository.findOneBySubscriptionId = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<OfflineUserSubscription>)
|
||||
event.payload.offline = true
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(offlineUserSubscriptionRepository.save).toHaveBeenCalledWith({
|
||||
endsAt: subscriptionExpiresAt,
|
||||
subscriptionId: 1,
|
||||
planName: 'PRO_PLAN',
|
||||
email: 'test@test.com',
|
||||
createdAt: expect.any(Number),
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not do anything if no user is found for specified email', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if username is invalid', async () => {
|
||||
event.payload.userEmail = ' '
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OfflineFeaturesTokenData } from '@standardnotes/security'
|
||||
import { SettingName, Username } from '@standardnotes/domain-core'
|
||||
import { ContentDecoderInterface } from '@standardnotes/common'
|
||||
import { DomainEventHandlerInterface, SubscriptionSyncRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
@@ -11,31 +11,23 @@ import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface'
|
||||
import { ContentDecoderInterface } from '@standardnotes/common'
|
||||
import { OfflineSettingName } from '../Setting/OfflineSettingName'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionSyncRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_UserSubscriptionRepository)
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface,
|
||||
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingService)
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.Auth_OfflineSettingService) private offlineSettingService: OfflineSettingServiceInterface,
|
||||
@inject(TYPES.Auth_ContenDecoder) private contentDecoder: ContentDecoderInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
private roleService: RoleServiceInterface,
|
||||
private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings,
|
||||
private setSettingValue: SetSettingValue,
|
||||
private offlineSettingService: OfflineSettingServiceInterface,
|
||||
private contentDecoder: ContentDecoderInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionSyncRequestedEvent): Promise<void> {
|
||||
@@ -95,17 +87,26 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
await this.settingService.createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.ExtensionKey,
|
||||
unencryptedValue: event.payload.extensionKey,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
sensitive: true,
|
||||
},
|
||||
const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({
|
||||
userSubscriptionUuid: userSubscription.uuid,
|
||||
userUuid: user.uuid,
|
||||
subscriptionPlanName: event.payload.subscriptionName,
|
||||
})
|
||||
if (applyingSettingsResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`Could not apply default subscription settings for user ${user.uuid}: ${applyingSettingsResult.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.setSettingValue.execute({
|
||||
userUuid: user.uuid,
|
||||
settingName: SettingName.NAMES.ExtensionKey,
|
||||
value: event.payload.subscriptionName,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not set extension key for user ${user.uuid}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async createOrUpdateSubscription(
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { UserDisabledSessionUserAgentLoggingEvent } from '@standardnotes/domain-events'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
|
||||
import { UserDisabledSessionUserAgentLoggingEventHandler } from './UserDisabledSessionUserAgentLoggingEventHandler'
|
||||
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
|
||||
|
||||
describe('UserDisabledSessionUserAgentLoggingEventHandler', () => {
|
||||
let sessionRepository: SessionRepositoryInterface
|
||||
let revokedSessionRepository: RevokedSessionRepositoryInterface
|
||||
let event: UserDisabledSessionUserAgentLoggingEvent
|
||||
|
||||
const createHandler = () =>
|
||||
new UserDisabledSessionUserAgentLoggingEventHandler(sessionRepository, revokedSessionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
|
||||
sessionRepository.clearUserAgentByUserUuid = jest.fn()
|
||||
|
||||
revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
|
||||
revokedSessionRepository.clearUserAgentByUserUuid = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
}
|
||||
})
|
||||
|
||||
it('should clear all user agent info from all user sessions', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(sessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3')
|
||||
expect(revokedSessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3')
|
||||
})
|
||||
})
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { UserEmailChangedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { AxiosInstance } from 'axios'
|
||||
|
||||
import { UserEmailChangedEventHandler } from './UserEmailChangedEventHandler'
|
||||
|
||||
describe('UserEmailChangedEventHandler', () => {
|
||||
let httpClient: AxiosInstance
|
||||
const userServerChangeEmailUrl = 'https://user-server/change-email'
|
||||
const userServerAuthKey = 'auth-key'
|
||||
let event: UserEmailChangedEvent
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () =>
|
||||
new UserEmailChangedEventHandler(httpClient, userServerChangeEmailUrl, userServerAuthKey, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
httpClient = {} as jest.Mocked<AxiosInstance>
|
||||
httpClient.request = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<UserEmailChangedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
fromEmail: 'test@test.te',
|
||||
toEmail: 'test2@test.te',
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
})
|
||||
|
||||
it('should send a request to the user management server about an email change', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(httpClient.request).toHaveBeenCalledWith({
|
||||
method: 'POST',
|
||||
url: 'https://user-server/change-email',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
key: 'auth-key',
|
||||
user: {
|
||||
uuid: '1-2-3',
|
||||
from_email: 'test@test.te',
|
||||
to_email: 'test2@test.te',
|
||||
},
|
||||
},
|
||||
validateStatus: expect.any(Function),
|
||||
})
|
||||
})
|
||||
|
||||
it('should not send a request to the user management server about an email change if url is not defined', async () => {
|
||||
const handler = new UserEmailChangedEventHandler(httpClient, '', userServerAuthKey, logger)
|
||||
await handler.handle(event)
|
||||
|
||||
expect(httpClient.request).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,48 +0,0 @@
|
||||
import { DomainEventHandlerInterface, UserEmailChangedEvent } from '@standardnotes/domain-events'
|
||||
import { AxiosInstance } from 'axios'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class UserEmailChangedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_HTTPClient) private httpClient: AxiosInstance,
|
||||
@inject(TYPES.Auth_USER_SERVER_CHANGE_EMAIL_URL) private userServerChangeEmailUrl: string,
|
||||
@inject(TYPES.Auth_USER_SERVER_AUTH_KEY) private userServerAuthKey: string,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: UserEmailChangedEvent): Promise<void> {
|
||||
if (!this.userServerChangeEmailUrl) {
|
||||
this.logger.debug('User server change email url not defined. Skipped post email change actions.')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.debug(`Changing user email from ${event.payload.fromEmail} to ${event.payload.toEmail}`)
|
||||
|
||||
await this.httpClient.request({
|
||||
method: 'POST',
|
||||
url: this.userServerChangeEmailUrl,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
key: this.userServerAuthKey,
|
||||
user: {
|
||||
uuid: event.payload.userUuid,
|
||||
from_email: event.payload.fromEmail,
|
||||
to_email: event.payload.toEmail,
|
||||
},
|
||||
},
|
||||
validateStatus:
|
||||
/* istanbul ignore next */
|
||||
(status: number) => status >= 200 && status < 500,
|
||||
})
|
||||
|
||||
this.logger.debug(`Successfully changed user email to ${event.payload.toEmail}`)
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { UserRegisteredEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { UserRegisteredEventHandler } from './UserRegisteredEventHandler'
|
||||
import { AxiosInstance } from 'axios'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
describe('UserRegisteredEventHandler', () => {
|
||||
let httpClient: AxiosInstance
|
||||
const userServerRegistrationUrl = 'https://user-server/registration'
|
||||
const userServerAuthKey = 'auth-key'
|
||||
let event: UserRegisteredEvent
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () =>
|
||||
new UserRegisteredEventHandler(httpClient, userServerRegistrationUrl, userServerAuthKey, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
httpClient = {} as jest.Mocked<AxiosInstance>
|
||||
httpClient.request = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<UserRegisteredEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
protocolVersion: ProtocolVersion.V004,
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
})
|
||||
|
||||
it('should send a request to the user management server about a registration', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(httpClient.request).toHaveBeenCalledWith({
|
||||
method: 'POST',
|
||||
url: 'https://user-server/registration',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
key: 'auth-key',
|
||||
user: {
|
||||
created_at: new Date(1),
|
||||
email: 'test@test.te',
|
||||
},
|
||||
},
|
||||
validateStatus: expect.any(Function),
|
||||
})
|
||||
})
|
||||
|
||||
it('should not send a request to the user management server about a registration if url is not defined', async () => {
|
||||
const handler = new UserRegisteredEventHandler(httpClient, '', userServerAuthKey, logger)
|
||||
await handler.handle(event)
|
||||
|
||||
expect(httpClient.request).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
|
||||
import { AxiosInstance } from 'axios'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_HTTPClient) private httpClient: AxiosInstance,
|
||||
@inject(TYPES.Auth_USER_SERVER_REGISTRATION_URL) private userServerRegistrationUrl: string,
|
||||
@inject(TYPES.Auth_USER_SERVER_AUTH_KEY) private userServerAuthKey: string,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: UserRegisteredEvent): Promise<void> {
|
||||
if (!this.userServerRegistrationUrl) {
|
||||
this.logger.debug('User server registration url not defined. Skipped post-registration actions.')
|
||||
return
|
||||
}
|
||||
|
||||
await this.httpClient.request({
|
||||
method: 'POST',
|
||||
url: this.userServerRegistrationUrl,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
key: this.userServerAuthKey,
|
||||
user: {
|
||||
email: event.payload.email,
|
||||
created_at: event.createdAt,
|
||||
},
|
||||
},
|
||||
validateStatus:
|
||||
/* istanbul ignore next */
|
||||
(status: number) => status >= 200 && status < 500,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { EphemeralSessionRepositoryInterface } from './EphemeralSessionRepositor
|
||||
import { EphemeralSession } from './EphemeralSession'
|
||||
import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface'
|
||||
import { RevokedSession } from './RevokedSession'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { LogSessionUserAgentOption } from '@standardnotes/settings'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
import { CryptoNode } from '@standardnotes/sncrypto-node'
|
||||
@@ -19,6 +18,7 @@ import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscri
|
||||
import { TraceSession } from '../UseCase/TraceSession/TraceSession'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { GetSetting } from '../UseCase/GetSetting/GetSetting'
|
||||
|
||||
describe('SessionService', () => {
|
||||
let sessionRepository: SessionRepositoryInterface
|
||||
@@ -27,7 +27,7 @@ describe('SessionService', () => {
|
||||
let existingSession: Session
|
||||
let existingEphemeralSession: EphemeralSession
|
||||
let revokedSession: RevokedSession
|
||||
let settingService: SettingServiceInterface
|
||||
let getSetting: GetSetting
|
||||
let deviceDetector: UAParser
|
||||
let timer: TimerInterface
|
||||
let logger: winston.Logger
|
||||
@@ -46,11 +46,11 @@ describe('SessionService', () => {
|
||||
logger,
|
||||
123,
|
||||
234,
|
||||
settingService,
|
||||
cryptoNode,
|
||||
traceSession,
|
||||
userSubscriptionRepository,
|
||||
readonlyUsers,
|
||||
getSetting,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -72,8 +72,8 @@ describe('SessionService', () => {
|
||||
sessionRepository.insert = jest.fn()
|
||||
sessionRepository.update = jest.fn()
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
getSetting = {} as jest.Mocked<GetSetting>
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
|
||||
ephemeralSessionRepository = {} as jest.Mocked<EphemeralSessionRepositoryInterface>
|
||||
ephemeralSessionRepository.insert = jest.fn()
|
||||
@@ -240,9 +240,12 @@ describe('SessionService', () => {
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: LogSessionUserAgentOption.Disabled,
|
||||
} as jest.Mocked<Setting>)
|
||||
getSetting.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
setting: {} as jest.Mocked<Setting>,
|
||||
decryptedValue: LogSessionUserAgentOption.Disabled,
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import * as crypto from 'crypto'
|
||||
import * as dayjs from 'dayjs'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { UAParserInstance } from 'ua-parser-js'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
import { LogSessionUserAgentOption, SettingName } from '@standardnotes/settings'
|
||||
import { SettingName } from '@standardnotes/domain-core'
|
||||
import { LogSessionUserAgentOption } from '@standardnotes/settings'
|
||||
import { SessionBody } from '@standardnotes/responses'
|
||||
import { CryptoNode } from '@standardnotes/sncrypto-node'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { Session } from './Session'
|
||||
import { SessionRepositoryInterface } from './SessionRepositoryInterface'
|
||||
import { SessionServiceInterface } from './SessionServiceInterface'
|
||||
@@ -18,30 +17,27 @@ import { EphemeralSessionRepositoryInterface } from './EphemeralSessionRepositor
|
||||
import { EphemeralSession } from './EphemeralSession'
|
||||
import { RevokedSession } from './RevokedSession'
|
||||
import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { TraceSession } from '../UseCase/TraceSession/TraceSession'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { GetSetting } from '../UseCase/GetSetting/GetSetting'
|
||||
|
||||
@injectable()
|
||||
export class SessionService implements SessionServiceInterface {
|
||||
static readonly SESSION_TOKEN_VERSION = 1
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface,
|
||||
@inject(TYPES.Auth_EphemeralSessionRepository)
|
||||
private sessionRepository: SessionRepositoryInterface,
|
||||
private ephemeralSessionRepository: EphemeralSessionRepositoryInterface,
|
||||
@inject(TYPES.Auth_RevokedSessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
|
||||
@inject(TYPES.Auth_DeviceDetector) private deviceDetector: UAParser,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
@inject(TYPES.Auth_ACCESS_TOKEN_AGE) private accessTokenAge: number,
|
||||
@inject(TYPES.Auth_REFRESH_TOKEN_AGE) private refreshTokenAge: number,
|
||||
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Auth_CryptoNode) private cryptoNode: CryptoNode,
|
||||
@inject(TYPES.Auth_TraceSession) private traceSession: TraceSession,
|
||||
@inject(TYPES.Auth_UserSubscriptionRepository)
|
||||
private revokedSessionRepository: RevokedSessionRepositoryInterface,
|
||||
private deviceDetector: UAParserInstance,
|
||||
private timer: TimerInterface,
|
||||
private logger: Logger,
|
||||
private accessTokenAge: number,
|
||||
private refreshTokenAge: number,
|
||||
private cryptoNode: CryptoNode,
|
||||
private traceSession: TraceSession,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_READONLY_USERS) private readonlyUsers: string[],
|
||||
private readonlyUsers: string[],
|
||||
private getSetting: GetSetting,
|
||||
) {}
|
||||
|
||||
async createNewSessionForUser(dto: {
|
||||
@@ -320,15 +316,17 @@ export class SessionService implements SessionServiceInterface {
|
||||
}
|
||||
|
||||
private async isLoggingUserAgentEnabledOnSessions(user: User): Promise<boolean> {
|
||||
const loggingSetting = await this.settingService.findSettingWithDecryptedValue({
|
||||
settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(),
|
||||
const loggingSettingOrError = await this.getSetting.execute({
|
||||
settingName: SettingName.NAMES.LogSessionUserAgent,
|
||||
decrypted: true,
|
||||
userUuid: user.uuid,
|
||||
allowSensitiveRetrieval: true,
|
||||
})
|
||||
|
||||
if (loggingSetting === null) {
|
||||
if (loggingSettingOrError.isFailed()) {
|
||||
return true
|
||||
}
|
||||
const loggingSetting = loggingSettingOrError.getValue()
|
||||
|
||||
return loggingSetting.value === LogSessionUserAgentOption.Enabled
|
||||
return loggingSetting.decryptedValue === LogSessionUserAgentOption.Enabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { User } from '../User/User'
|
||||
import { SettingProps } from './SettingProps'
|
||||
|
||||
export type CreateOrReplaceSettingDto = {
|
||||
user: User
|
||||
props: SettingProps
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Setting } from './Setting'
|
||||
|
||||
export type CreateOrReplaceSettingResponse = {
|
||||
status: 'created' | 'replaced'
|
||||
setting: Setting
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { User } from '../User/User'
|
||||
import { SubscriptionSettingProps } from './SubscriptionSettingProps'
|
||||
|
||||
export type CreateOrReplaceSubscriptionSettingDTO = {
|
||||
userSubscription: UserSubscription
|
||||
user: User
|
||||
props: SubscriptionSettingProps
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
|
||||
export type CreateOrReplaceSubscriptionSettingResponse = {
|
||||
status: 'created' | 'replaced'
|
||||
subscriptionSetting: SubscriptionSetting
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
|
||||
export type FindSettingDTO = {
|
||||
userUuid: string
|
||||
settingName: SettingName
|
||||
settingUuid?: string
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
|
||||
export type FindSubscriptionSettingDTO = {
|
||||
userUuid: string
|
||||
userSubscriptionUuid: string
|
||||
subscriptionSettingName: SettingName
|
||||
settingUuid?: string
|
||||
}
|
||||
@@ -1,60 +1,13 @@
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { User } from '../User/User'
|
||||
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
@Entity({ name: 'settings' })
|
||||
@Index('index_settings_on_name_and_user_uuid', ['name', 'user'])
|
||||
export class Setting {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
declare uuid: string
|
||||
import { SettingProps } from './SettingProps'
|
||||
|
||||
@Column({
|
||||
length: 255,
|
||||
})
|
||||
declare name: string
|
||||
export class Setting extends Entity<SettingProps> {
|
||||
private constructor(props: SettingProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@Column({
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
})
|
||||
declare value: string | null
|
||||
|
||||
@Column({
|
||||
name: 'server_encryption_version',
|
||||
type: 'tinyint',
|
||||
default: EncryptionVersion.Unencrypted,
|
||||
})
|
||||
declare serverEncryptionVersion: number
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'bigint',
|
||||
})
|
||||
declare createdAt: number
|
||||
|
||||
@Column({
|
||||
name: 'updated_at',
|
||||
type: 'bigint',
|
||||
})
|
||||
@Index('index_settings_on_updated_at')
|
||||
declare updatedAt: number
|
||||
|
||||
@ManyToOne(
|
||||
/* istanbul ignore next */
|
||||
() => User,
|
||||
/* istanbul ignore next */
|
||||
(user) => user.settings,
|
||||
/* istanbul ignore next */
|
||||
{ onDelete: 'CASCADE', nullable: false, lazy: true, eager: false },
|
||||
)
|
||||
@JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' })
|
||||
declare user: Promise<User>
|
||||
|
||||
@Column({
|
||||
type: 'tinyint',
|
||||
width: 1,
|
||||
nullable: false,
|
||||
default: 0,
|
||||
})
|
||||
declare sensitive: boolean
|
||||
static create(props: SettingProps, id?: UniqueEntityId): Result<Setting> {
|
||||
return Result.ok<Setting>(new Setting(props, id))
|
||||
}
|
||||
}
|
||||
|
||||
239
packages/auth/src/Domain/Setting/SettingCrypter.spec.ts
Normal file
239
packages/auth/src/Domain/Setting/SettingCrypter.spec.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import 'reflect-metadata'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { Setting } from './Setting'
|
||||
|
||||
import { SettingCrypter } from './SettingCrypter'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
import { SettingName, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
describe('SettingCrypter', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let crypter: CrypterInterface
|
||||
let user: User
|
||||
|
||||
const createDecrypter = () => new SettingCrypter(userRepository, crypter)
|
||||
|
||||
beforeEach(() => {
|
||||
crypter = {} as jest.Mocked<CrypterInterface>
|
||||
crypter.decryptForUser = jest.fn().mockReturnValue('decrypted')
|
||||
|
||||
user = {
|
||||
uuid: '4-5-6',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
})
|
||||
|
||||
describe('setting', () => {
|
||||
it('should encrypt a string value', async () => {
|
||||
const string = 'decrypted'
|
||||
|
||||
crypter.encryptForUser = jest.fn().mockReturnValue('encrypted')
|
||||
|
||||
const encrypted = await createDecrypter().encryptValue(
|
||||
string,
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
)
|
||||
|
||||
expect(encrypted).toEqual('encrypted')
|
||||
})
|
||||
|
||||
it('should return null when trying to encrypt a null value', async () => {
|
||||
const encrypted = await createDecrypter().encryptValue(
|
||||
null,
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
)
|
||||
|
||||
expect(encrypted).toBeNull()
|
||||
})
|
||||
|
||||
it('should throw error when encrypting and user is not found', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createDecrypter().encryptValue('test', Uuid.create('00000000-0000-0000-0000-000000000000').getValue())
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should decrypt an encrypted value of a setting', async () => {
|
||||
const setting = Setting.create({
|
||||
name: SettingName.NAMES.ListedAuthorSecrets,
|
||||
value: 'encrypted',
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sensitive: false,
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual(
|
||||
'decrypted',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return null if the setting value is null', async () => {
|
||||
const setting = Setting.create({
|
||||
name: SettingName.NAMES.ListedAuthorSecrets,
|
||||
value: null,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sensitive: false,
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toBeNull()
|
||||
})
|
||||
|
||||
it('should return unencrypted value if the setting value is unencrypted', async () => {
|
||||
const setting = Setting.create({
|
||||
name: SettingName.NAMES.ListedAuthorSecrets,
|
||||
value: 'test',
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sensitive: false,
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual(
|
||||
'test',
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw if the user could not be found', async () => {
|
||||
const setting = Setting.create({
|
||||
name: SettingName.NAMES.ListedAuthorSecrets,
|
||||
value: 'encrypted',
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sensitive: false,
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should throw if the user uuid is invalid', async () => {
|
||||
const setting = Setting.create({
|
||||
name: SettingName.NAMES.ListedAuthorSecrets,
|
||||
value: 'encrypted',
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sensitive: false,
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createDecrypter().decryptSettingValue(setting, 'invalid')
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('subscription setting', () => {
|
||||
it('should decrypt an encrypted value of a setting', async () => {
|
||||
const setting = SubscriptionSetting.create({
|
||||
name: SettingName.NAMES.ExtensionKey,
|
||||
value: 'encrypted',
|
||||
sensitive: true,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
expect(
|
||||
await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000'),
|
||||
).toEqual('decrypted')
|
||||
})
|
||||
|
||||
it('should return null if the setting value is null', async () => {
|
||||
const setting = SubscriptionSetting.create({
|
||||
name: SettingName.NAMES.ExtensionKey,
|
||||
value: null,
|
||||
sensitive: true,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
expect(
|
||||
await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000'),
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('should return unencrypted value if the setting value is unencrypted', async () => {
|
||||
const setting = SubscriptionSetting.create({
|
||||
name: SettingName.NAMES.ExtensionKey,
|
||||
value: 'test',
|
||||
sensitive: true,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
expect(
|
||||
await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000'),
|
||||
).toEqual('test')
|
||||
})
|
||||
|
||||
it('should throw if the user could not be found', async () => {
|
||||
const setting = SubscriptionSetting.create({
|
||||
name: SettingName.NAMES.ExtensionKey,
|
||||
value: 'encrypted',
|
||||
sensitive: true,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000')
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should throw if the user uuid is invalid', async () => {
|
||||
const setting = SubscriptionSetting.create({
|
||||
name: SettingName.NAMES.ExtensionKey,
|
||||
value: 'encrypted',
|
||||
sensitive: true,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createDecrypter().decryptSubscriptionSettingValue(setting, 'invalid')
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
60
packages/auth/src/Domain/Setting/SettingCrypter.ts
Normal file
60
packages/auth/src/Domain/Setting/SettingCrypter.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { Setting } from './Setting'
|
||||
import { SettingCrypterInterface } from './SettingCrypterInterface'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
|
||||
export class SettingCrypter implements SettingCrypterInterface {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private crypter: CrypterInterface,
|
||||
) {}
|
||||
|
||||
async encryptValue(value: string | null, userUuid: Uuid): Promise<string | null> {
|
||||
if (value === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
|
||||
if (user === null) {
|
||||
throw new Error(`Could not find user with uuid: ${userUuid.value}`)
|
||||
}
|
||||
|
||||
return this.crypter.encryptForUser(value, user)
|
||||
}
|
||||
|
||||
async decryptSettingValue(setting: Setting, userUuidString: string): Promise<string | null> {
|
||||
return this.decrypt(setting.props.value, setting.props.serverEncryptionVersion, userUuidString)
|
||||
}
|
||||
|
||||
async decryptSubscriptionSettingValue(setting: SubscriptionSetting, userUuidString: string): Promise<string | null> {
|
||||
return this.decrypt(setting.props.value, setting.props.serverEncryptionVersion, userUuidString)
|
||||
}
|
||||
|
||||
private async decrypt(
|
||||
value: string | null,
|
||||
serverEncryptionVersion: number,
|
||||
userUuidString: string,
|
||||
): Promise<string | null> {
|
||||
if (value !== null && serverEncryptionVersion === EncryptionVersion.Default) {
|
||||
const userUuidOrError = Uuid.create(userUuidString)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
throw new Error(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
|
||||
if (user === null) {
|
||||
throw new Error(`Could not find user with uuid: ${userUuid.value}`)
|
||||
}
|
||||
|
||||
return this.crypter.decryptForUser(value, user)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
10
packages/auth/src/Domain/Setting/SettingCrypterInterface.ts
Normal file
10
packages/auth/src/Domain/Setting/SettingCrypterInterface.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Setting } from './Setting'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
|
||||
export interface SettingCrypterInterface {
|
||||
encryptValue(value: string | null, userUuid: Uuid): Promise<string | null>
|
||||
decryptSettingValue(value: Setting, userUuid: string): Promise<string | null>
|
||||
decryptSubscriptionSettingValue(setting: SubscriptionSetting, userUuid: string): Promise<string | null>
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { Setting } from './Setting'
|
||||
|
||||
import { SettingDecrypter } from './SettingDecrypter'
|
||||
|
||||
describe('SettingDecrypter', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let crypter: CrypterInterface
|
||||
let user: User
|
||||
|
||||
const createDecrypter = () => new SettingDecrypter(userRepository, crypter)
|
||||
|
||||
beforeEach(() => {
|
||||
crypter = {} as jest.Mocked<CrypterInterface>
|
||||
crypter.decryptForUser = jest.fn().mockReturnValue('decrypted')
|
||||
|
||||
user = {
|
||||
uuid: '4-5-6',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
})
|
||||
|
||||
it('should decrypt an encrypted value of a setting', async () => {
|
||||
const setting = {
|
||||
value: 'encrypted',
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual(
|
||||
'decrypted',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return null if the setting value is null', async () => {
|
||||
const setting = {
|
||||
value: null,
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toBeNull()
|
||||
})
|
||||
|
||||
it('should return unencrypted value if the setting value is unencrypted', async () => {
|
||||
const setting = {
|
||||
value: 'test',
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual('test')
|
||||
})
|
||||
|
||||
it('should throw if the user could not be found', async () => {
|
||||
const setting = {
|
||||
value: 'encrypted',
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
} as jest.Mocked<Setting>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should throw if the user uuid is invalid', async () => {
|
||||
const setting = {
|
||||
value: 'encrypted',
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createDecrypter().decryptSettingValue(setting, 'invalid')
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { Setting } from './Setting'
|
||||
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
@injectable()
|
||||
export class SettingDecrypter implements SettingDecrypterInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_Crypter) private crypter: CrypterInterface,
|
||||
) {}
|
||||
|
||||
async decryptSettingValue(setting: Setting | SubscriptionSetting, userUuidString: string): Promise<string | null> {
|
||||
if (setting.value !== null && setting.serverEncryptionVersion === EncryptionVersion.Default) {
|
||||
const userUuidOrError = Uuid.create(userUuidString)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
throw new Error(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
|
||||
if (user === null) {
|
||||
throw new Error(`Could not find user with uuid: ${userUuid.value}`)
|
||||
}
|
||||
|
||||
return this.crypter.decryptForUser(setting.value, user)
|
||||
}
|
||||
|
||||
return setting.value
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Setting } from './Setting'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
|
||||
export interface SettingDecrypterInterface {
|
||||
decryptSettingValue(setting: Setting | SubscriptionSetting, userUuid: string): Promise<string | null>
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
|
||||
export type SettingDescription = {
|
||||
value: string
|
||||
sensitive: boolean
|
||||
serverEncryptionVersion: EncryptionVersion
|
||||
replaceable: boolean
|
||||
}
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { User } from '../User/User'
|
||||
import { Setting } from './Setting'
|
||||
import { SettingFactory } from './SettingFactory'
|
||||
import { SettingProps } from './SettingProps'
|
||||
import { SubscriptionSettingProps } from './SubscriptionSettingProps'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
|
||||
describe('SettingFactory', () => {
|
||||
let crypter: CrypterInterface
|
||||
let timer: TimerInterface
|
||||
let user: User
|
||||
let userSubscription: UserSubscription
|
||||
|
||||
const createFactory = () => new SettingFactory(crypter, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
crypter = {} as jest.Mocked<CrypterInterface>
|
||||
crypter.encryptForUser = jest.fn().mockReturnValue('encrypted')
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userSubscription = {
|
||||
user: Promise.resolve(user),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
})
|
||||
|
||||
it('should create a Setting', async () => {
|
||||
const props: SettingProps = {
|
||||
name: 'name',
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
sensitive: false,
|
||||
}
|
||||
const actual = await createFactory().create(props, user)
|
||||
|
||||
expect(actual).toEqual({
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
name: 'name',
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: 0,
|
||||
user: Promise.resolve(user),
|
||||
uuid: expect.any(String),
|
||||
value: 'value',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a SubscriptionSetting', async () => {
|
||||
const props: SubscriptionSettingProps = {
|
||||
name: 'name',
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
sensitive: false,
|
||||
}
|
||||
const actual = await createFactory().createSubscriptionSetting(props, userSubscription)
|
||||
|
||||
expect(actual).toEqual({
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
name: 'name',
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: 0,
|
||||
userSubscription: Promise.resolve(userSubscription),
|
||||
uuid: expect.any(String),
|
||||
value: 'value',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create an encrypted SubscriptionSetting', async () => {
|
||||
const value = 'value'
|
||||
const props: SettingProps = {
|
||||
name: 'name',
|
||||
unencryptedValue: value,
|
||||
sensitive: false,
|
||||
}
|
||||
|
||||
const actual = await createFactory().createSubscriptionSetting(props, userSubscription)
|
||||
|
||||
expect(actual).toEqual({
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
name: 'name',
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: 1,
|
||||
userSubscription: Promise.resolve(userSubscription),
|
||||
uuid: expect.any(String),
|
||||
value: 'encrypted',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a SubscriptionSetting replacement', async () => {
|
||||
const original = {
|
||||
userSubscription: Promise.resolve(userSubscription),
|
||||
} as jest.Mocked<SubscriptionSetting>
|
||||
original.uuid = '2-3-4'
|
||||
|
||||
const props: SettingProps = {
|
||||
name: 'name',
|
||||
unencryptedValue: 'value2',
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
sensitive: true,
|
||||
}
|
||||
|
||||
const actual = await createFactory().createSubscriptionSettingReplacement(original, props)
|
||||
|
||||
expect(actual).toEqual({
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
name: 'name',
|
||||
sensitive: true,
|
||||
serverEncryptionVersion: 0,
|
||||
userSubscription: Promise.resolve(userSubscription),
|
||||
uuid: '2-3-4',
|
||||
value: 'value2',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a Setting replacement', async () => {
|
||||
const original = {} as jest.Mocked<Setting>
|
||||
original.uuid = '2-3-4'
|
||||
|
||||
const props: SettingProps = {
|
||||
name: 'name',
|
||||
unencryptedValue: 'value2',
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
sensitive: true,
|
||||
}
|
||||
|
||||
const actual = await createFactory().createReplacement(original, props)
|
||||
|
||||
expect(actual).toEqual({
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
name: 'name',
|
||||
sensitive: true,
|
||||
serverEncryptionVersion: 0,
|
||||
user: Promise.resolve(user),
|
||||
uuid: '2-3-4',
|
||||
value: 'value2',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create an encrypted Setting', async () => {
|
||||
const value = 'value'
|
||||
const props: SettingProps = {
|
||||
name: 'name',
|
||||
unencryptedValue: value,
|
||||
sensitive: false,
|
||||
}
|
||||
|
||||
const actual = await createFactory().create(props, user)
|
||||
|
||||
expect(actual).toEqual({
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
name: 'name',
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: 1,
|
||||
user: Promise.resolve(user),
|
||||
uuid: expect.any(String),
|
||||
value: 'encrypted',
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw for unrecognized encryption version', async () => {
|
||||
const value = 'value'
|
||||
const props: SettingProps = {
|
||||
name: 'name',
|
||||
unencryptedValue: value,
|
||||
serverEncryptionVersion: 99999999999,
|
||||
sensitive: false,
|
||||
}
|
||||
|
||||
await expect(async () => await createFactory().create(props, user)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -1,114 +0,0 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { User } from '../User/User'
|
||||
import { Setting } from './Setting'
|
||||
import { SettingProps } from './SettingProps'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { SettingFactoryInterface } from './SettingFactoryInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
import { SubscriptionSettingProps } from './SubscriptionSettingProps'
|
||||
|
||||
@injectable()
|
||||
export class SettingFactory implements SettingFactoryInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_Crypter) private crypter: CrypterInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async createSubscriptionSetting(
|
||||
props: SubscriptionSettingProps,
|
||||
userSubscription: UserSubscription,
|
||||
): Promise<SubscriptionSetting> {
|
||||
const uuid = props.uuid ?? uuidv4()
|
||||
const now = this.timer.getTimestampInMicroseconds()
|
||||
const createdAt = props.createdAt ?? now
|
||||
const updatedAt = props.updatedAt ?? now
|
||||
|
||||
const { name, unencryptedValue, serverEncryptionVersion = EncryptionVersion.Default, sensitive } = props
|
||||
|
||||
const subscriptionSetting = {
|
||||
uuid,
|
||||
userSubscription: Promise.resolve(userSubscription),
|
||||
name,
|
||||
value: await this.createValue({
|
||||
unencryptedValue,
|
||||
serverEncryptionVersion,
|
||||
user: await userSubscription.user,
|
||||
}),
|
||||
serverEncryptionVersion,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
sensitive,
|
||||
}
|
||||
|
||||
return Object.assign(new SubscriptionSetting(), subscriptionSetting)
|
||||
}
|
||||
|
||||
async createSubscriptionSettingReplacement(
|
||||
original: SubscriptionSetting,
|
||||
props: SubscriptionSettingProps,
|
||||
): Promise<SubscriptionSetting> {
|
||||
const { uuid, userSubscription } = original
|
||||
|
||||
return Object.assign(await this.createSubscriptionSetting(props, await userSubscription), {
|
||||
uuid,
|
||||
})
|
||||
}
|
||||
|
||||
async create(props: SettingProps, user: User): Promise<Setting> {
|
||||
const uuid = props.uuid ?? uuidv4()
|
||||
const now = this.timer.getTimestampInMicroseconds()
|
||||
const createdAt = props.createdAt ?? now
|
||||
const updatedAt = props.updatedAt ?? now
|
||||
|
||||
const { name, unencryptedValue, serverEncryptionVersion = EncryptionVersion.Default, sensitive } = props
|
||||
|
||||
const setting = {
|
||||
uuid,
|
||||
user: Promise.resolve(user),
|
||||
name,
|
||||
value: await this.createValue({
|
||||
unencryptedValue,
|
||||
serverEncryptionVersion,
|
||||
user,
|
||||
}),
|
||||
serverEncryptionVersion,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
sensitive,
|
||||
}
|
||||
|
||||
return Object.assign(new Setting(), setting)
|
||||
}
|
||||
|
||||
async createReplacement(original: Setting, props: SettingProps): Promise<Setting> {
|
||||
const { uuid, user } = original
|
||||
|
||||
return Object.assign(await this.create(props, await user), {
|
||||
uuid,
|
||||
})
|
||||
}
|
||||
|
||||
async createValue({
|
||||
unencryptedValue,
|
||||
serverEncryptionVersion,
|
||||
user,
|
||||
}: {
|
||||
unencryptedValue: string | null
|
||||
serverEncryptionVersion: number
|
||||
user: User
|
||||
}): Promise<string | null> {
|
||||
switch (serverEncryptionVersion) {
|
||||
case EncryptionVersion.Unencrypted:
|
||||
return unencryptedValue
|
||||
case EncryptionVersion.Default:
|
||||
return this.crypter.encryptForUser(unencryptedValue as string, user)
|
||||
default:
|
||||
throw Error(`Unrecognized encryption version: ${serverEncryptionVersion}!`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { User } from '../User/User'
|
||||
import { Setting } from './Setting'
|
||||
import { SettingProps } from './SettingProps'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
import { SubscriptionSettingProps } from './SubscriptionSettingProps'
|
||||
|
||||
export interface SettingFactoryInterface {
|
||||
create(props: SettingProps, user: User): Promise<Setting>
|
||||
createSubscriptionSetting(
|
||||
props: SubscriptionSettingProps,
|
||||
userSubscription: UserSubscription,
|
||||
): Promise<SubscriptionSetting>
|
||||
createReplacement(original: Setting, props: SettingProps): Promise<Setting>
|
||||
createSubscriptionSettingReplacement(
|
||||
original: SubscriptionSetting,
|
||||
props: SubscriptionSettingProps,
|
||||
): Promise<SubscriptionSetting>
|
||||
}
|
||||
@@ -4,30 +4,26 @@ import {
|
||||
MuteEmailsSettingChangedEvent,
|
||||
UserDisabledSessionUserAgentLoggingEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import {
|
||||
EmailBackupFrequency,
|
||||
LogSessionUserAgentOption,
|
||||
MuteMarketingEmailsOption,
|
||||
SettingName,
|
||||
} from '@standardnotes/settings'
|
||||
import { EmailBackupFrequency, LogSessionUserAgentOption, MuteMarketingEmailsOption } from '@standardnotes/settings'
|
||||
import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { Setting } from './Setting'
|
||||
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
|
||||
import { SettingCrypterInterface } from './SettingCrypterInterface'
|
||||
|
||||
import { SettingInterpreter } from './SettingInterpreter'
|
||||
import { SettingRepositoryInterface } from './SettingRepositoryInterface'
|
||||
import { GetUserKeyParams } from '../UseCase/GetUserKeyParams/GetUserKeyParams'
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
import { Uuid, Timestamps, UniqueEntityId, SettingName } from '@standardnotes/domain-core'
|
||||
|
||||
describe('SettingInterpreter', () => {
|
||||
let user: User
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let settingRepository: SettingRepositoryInterface
|
||||
let settingDecrypter: SettingDecrypterInterface
|
||||
let settingCrypter: SettingCrypterInterface
|
||||
let logger: Logger
|
||||
let getUserKeyParams: GetUserKeyParams
|
||||
|
||||
@@ -44,8 +40,8 @@ describe('SettingInterpreter', () => {
|
||||
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
settingDecrypter = {} as jest.Mocked<SettingDecrypterInterface>
|
||||
settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
|
||||
settingCrypter = {} as jest.Mocked<SettingCrypterInterface>
|
||||
settingCrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
@@ -96,11 +92,19 @@ describe('SettingInterpreter', () => {
|
||||
})
|
||||
|
||||
it('should trigger backup if email backup setting is created - emails muted', async () => {
|
||||
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({
|
||||
name: SettingName.NAMES.MuteFailedBackupsEmails,
|
||||
uuid: '6-7-8',
|
||||
value: 'muted',
|
||||
} as jest.Mocked<Setting>)
|
||||
const setting = Setting.create(
|
||||
{
|
||||
name: SettingName.NAMES.MuteFailedBackupsEmails,
|
||||
value: 'muted',
|
||||
serverEncryptionVersion: 0,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sensitive: false,
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
},
|
||||
new UniqueEntityId('7fb54003-1dd2-40bd-8900-2bacd6cf629c'),
|
||||
).getValue()
|
||||
|
||||
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting)
|
||||
|
||||
await createInterpreter().interpretSettingUpdated(
|
||||
SettingName.NAMES.EmailBackupFrequency,
|
||||
@@ -109,7 +113,12 @@ describe('SettingInterpreter', () => {
|
||||
)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '6-7-8', true, {})
|
||||
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith(
|
||||
'4-5-6',
|
||||
'7fb54003-1dd2-40bd-8900-2bacd6cf629c',
|
||||
true,
|
||||
{},
|
||||
)
|
||||
})
|
||||
|
||||
it('should not trigger backup if email backup setting is disabled', async () => {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { EmailLevel } from '@standardnotes/domain-core'
|
||||
import {
|
||||
EmailBackupFrequency,
|
||||
LogSessionUserAgentOption,
|
||||
MuteFailedBackupsEmailsOption,
|
||||
SettingName,
|
||||
} from '@standardnotes/settings'
|
||||
import { EmailLevel, SettingName } from '@standardnotes/domain-core'
|
||||
import { EmailBackupFrequency, LogSessionUserAgentOption, MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
|
||||
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { User } from '../User/User'
|
||||
@@ -54,8 +49,8 @@ export class SettingInterpreter implements SettingInterpreterInterface {
|
||||
userUuid,
|
||||
)
|
||||
if (muteFailedEmailsBackupSetting !== null) {
|
||||
userHasEmailsMuted = muteFailedEmailsBackupSetting.value === MuteFailedBackupsEmailsOption.Muted
|
||||
muteEmailsSettingUuid = muteFailedEmailsBackupSetting.uuid
|
||||
userHasEmailsMuted = muteFailedEmailsBackupSetting.props.value === MuteFailedBackupsEmailsOption.Muted
|
||||
muteEmailsSettingUuid = muteFailedEmailsBackupSetting.id.toString()
|
||||
}
|
||||
|
||||
const keyParamsResponse = await this.getUserKeyParams.execute({
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Setting } from './Setting'
|
||||
import { Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export type SettingProps = Omit<
|
||||
Setting,
|
||||
'uuid' | 'user' | 'createdAt' | 'updatedAt' | 'serverEncryptionVersion' | 'value'
|
||||
> & {
|
||||
uuid?: string
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
unencryptedValue: string | null
|
||||
serverEncryptionVersion?: number
|
||||
export interface SettingProps {
|
||||
name: string
|
||||
value: string | null
|
||||
serverEncryptionVersion: number
|
||||
timestamps: Timestamps
|
||||
sensitive: boolean
|
||||
userUuid: Uuid
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReadStream } from 'fs'
|
||||
import { SettingName } from '@standardnotes/domain-core'
|
||||
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { DeleteSettingDto } from '../UseCase/DeleteSetting/DeleteSettingDto'
|
||||
import { Setting } from './Setting'
|
||||
|
||||
@@ -13,5 +13,6 @@ export interface SettingRepositoryInterface {
|
||||
streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream>
|
||||
streamAllByName(name: SettingName): Promise<ReadStream>
|
||||
deleteByUserUuid(dto: DeleteSettingDto): Promise<void>
|
||||
save(setting: Setting): Promise<Setting>
|
||||
insert(setting: Setting): Promise<void>
|
||||
update(setting: Setting): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { LogSessionUserAgentOption, MuteSignInEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { Logger } from 'winston'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { User } from '../User/User'
|
||||
import { Setting } from './Setting'
|
||||
import { SettingRepositoryInterface } from './SettingRepositoryInterface'
|
||||
|
||||
import { SettingService } from './SettingService'
|
||||
import { SettingsAssociationServiceInterface } from './SettingsAssociationServiceInterface'
|
||||
import { SettingInterpreterInterface } from './SettingInterpreterInterface'
|
||||
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
|
||||
import { SettingFactoryInterface } from './SettingFactoryInterface'
|
||||
|
||||
describe('SettingService', () => {
|
||||
let setting: Setting
|
||||
let user: User
|
||||
let factory: SettingFactoryInterface
|
||||
let settingRepository: SettingRepositoryInterface
|
||||
let settingsAssociationService: SettingsAssociationServiceInterface
|
||||
let settingInterpreter: SettingInterpreterInterface
|
||||
let settingDecrypter: SettingDecrypterInterface
|
||||
let logger: Logger
|
||||
|
||||
const createService = () =>
|
||||
new SettingService(
|
||||
factory,
|
||||
settingRepository,
|
||||
settingsAssociationService,
|
||||
settingInterpreter,
|
||||
settingDecrypter,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '4-5-6',
|
||||
} as jest.Mocked<User>
|
||||
user.isPotentiallyAPrivateUsernameAccount = jest.fn().mockReturnValue(false)
|
||||
|
||||
setting = {
|
||||
name: SettingName.NAMES.DropboxBackupToken,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
factory = {} as jest.Mocked<SettingFactoryInterface>
|
||||
factory.create = jest.fn().mockReturnValue(setting)
|
||||
factory.createReplacement = jest.fn().mockReturnValue(setting)
|
||||
|
||||
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
|
||||
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
settingRepository.save = jest.fn().mockImplementation((setting) => setting)
|
||||
|
||||
settingsAssociationService = {} as jest.Mocked<SettingsAssociationServiceInterface>
|
||||
settingsAssociationService.getDefaultSettingsAndValuesForNewUser = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.MuteSignInEmails,
|
||||
{
|
||||
value: MuteSignInEmailsOption.NotMuted,
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
},
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
||||
settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.LogSessionUserAgent,
|
||||
{
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
value: LogSessionUserAgentOption.Disabled,
|
||||
},
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
||||
settingInterpreter = {} as jest.Mocked<SettingInterpreterInterface>
|
||||
settingInterpreter.interpretSettingUpdated = jest.fn()
|
||||
|
||||
settingDecrypter = {} as jest.Mocked<SettingDecrypterInterface>
|
||||
settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should create default settings for a newly registered user', async () => {
|
||||
await createService().applyDefaultSettingsUponRegistration(user)
|
||||
|
||||
expect(settingRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
|
||||
it('should create default settings for a newly registered vault account', async () => {
|
||||
user.isPotentiallyAPrivateUsernameAccount = jest.fn().mockReturnValue(true)
|
||||
|
||||
await createService().applyDefaultSettingsUponRegistration(user)
|
||||
|
||||
expect(settingRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
|
||||
it("should create setting if it doesn't exist", async () => {
|
||||
const result = await createService().createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.MuteFailedBackupsEmails,
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: 1,
|
||||
sensitive: false,
|
||||
},
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
const result = await createService().createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
uuid: '1-2-3',
|
||||
name: SettingName.NAMES.MuteFailedBackupsEmails,
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: 1,
|
||||
sensitive: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toEqual('created')
|
||||
})
|
||||
|
||||
it('should replace setting if it does exist', async () => {
|
||||
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting)
|
||||
|
||||
const result = await createService().createOrReplace({
|
||||
user: user,
|
||||
props: {
|
||||
...setting,
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: 1,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toEqual('replaced')
|
||||
})
|
||||
|
||||
it('should replace setting with a given uuid if it does exist', async () => {
|
||||
settingRepository.findOneByUuid = jest.fn().mockReturnValue(setting)
|
||||
|
||||
const result = await createService().createOrReplace({
|
||||
user: user,
|
||||
props: {
|
||||
...setting,
|
||||
uuid: '1-2-3',
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: 1,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toEqual('replaced')
|
||||
})
|
||||
|
||||
it('should find and decrypt the value of a setting for user', async () => {
|
||||
setting = {
|
||||
value: 'encrypted',
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createService().findSettingWithDecryptedValue({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(),
|
||||
}),
|
||||
).toEqual({
|
||||
serverEncryptionVersion: 1,
|
||||
value: 'decrypted',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,110 +0,0 @@
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { CreateOrReplaceSettingDto } from './CreateOrReplaceSettingDto'
|
||||
import { CreateOrReplaceSettingResponse } from './CreateOrReplaceSettingResponse'
|
||||
import { FindSettingDTO } from './FindSettingDTO'
|
||||
import { Setting } from './Setting'
|
||||
import { SettingRepositoryInterface } from './SettingRepositoryInterface'
|
||||
import { SettingServiceInterface } from './SettingServiceInterface'
|
||||
import { SettingsAssociationServiceInterface } from './SettingsAssociationServiceInterface'
|
||||
import { SettingInterpreterInterface } from './SettingInterpreterInterface'
|
||||
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
|
||||
import { SettingFactoryInterface } from './SettingFactoryInterface'
|
||||
|
||||
export class SettingService implements SettingServiceInterface {
|
||||
constructor(
|
||||
private factory: SettingFactoryInterface,
|
||||
private settingRepository: SettingRepositoryInterface,
|
||||
private settingsAssociationService: SettingsAssociationServiceInterface,
|
||||
private settingInterpreter: SettingInterpreterInterface,
|
||||
private settingDecrypter: SettingDecrypterInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async applyDefaultSettingsUponRegistration(user: User): Promise<void> {
|
||||
let defaultSettingsWithValues = this.settingsAssociationService.getDefaultSettingsAndValuesForNewUser()
|
||||
if (user.isPotentiallyAPrivateUsernameAccount()) {
|
||||
defaultSettingsWithValues =
|
||||
this.settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount()
|
||||
}
|
||||
|
||||
for (const settingName of defaultSettingsWithValues.keys()) {
|
||||
this.logger.debug(`Creating setting ${settingName} for user ${user.uuid}`)
|
||||
|
||||
const setting = defaultSettingsWithValues.get(settingName) as {
|
||||
value: string
|
||||
sensitive: boolean
|
||||
serverEncryptionVersion: number
|
||||
}
|
||||
|
||||
await this.createOrReplace({
|
||||
user,
|
||||
props: {
|
||||
name: settingName,
|
||||
unencryptedValue: setting.value,
|
||||
serverEncryptionVersion: setting.serverEncryptionVersion,
|
||||
sensitive: setting.sensitive,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async findSettingWithDecryptedValue(dto: FindSettingDTO): Promise<Setting | null> {
|
||||
let setting: Setting | null
|
||||
if (dto.settingUuid !== undefined) {
|
||||
setting = await this.settingRepository.findOneByUuid(dto.settingUuid)
|
||||
} else {
|
||||
setting = await this.settingRepository.findLastByNameAndUserUuid(dto.settingName.value, dto.userUuid)
|
||||
}
|
||||
|
||||
if (setting === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
setting.value = await this.settingDecrypter.decryptSettingValue(setting, dto.userUuid)
|
||||
|
||||
return setting
|
||||
}
|
||||
|
||||
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,
|
||||
settingUuid: props.uuid,
|
||||
})
|
||||
|
||||
if (existing === null) {
|
||||
const setting = await this.settingRepository.save(await this.factory.create(props, user))
|
||||
|
||||
this.logger.debug('[%s] Created setting %s: %O', user.uuid, props.name, setting)
|
||||
|
||||
await this.settingInterpreter.interpretSettingUpdated(setting.name, user, props.unencryptedValue)
|
||||
|
||||
return {
|
||||
status: 'created',
|
||||
setting,
|
||||
}
|
||||
}
|
||||
|
||||
const setting = await this.settingRepository.save(await this.factory.createReplacement(existing, props))
|
||||
|
||||
this.logger.debug('[%s] Replaced existing setting %s with: %O', user.uuid, props.name, setting)
|
||||
|
||||
await this.settingInterpreter.interpretSettingUpdated(setting.name, user, props.unencryptedValue)
|
||||
|
||||
return {
|
||||
status: 'replaced',
|
||||
setting,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { User } from '../User/User'
|
||||
import { CreateOrReplaceSettingDto } from './CreateOrReplaceSettingDto'
|
||||
import { CreateOrReplaceSettingResponse } from './CreateOrReplaceSettingResponse'
|
||||
import { FindSettingDTO } from './FindSettingDTO'
|
||||
import { Setting } from './Setting'
|
||||
|
||||
export interface SettingServiceInterface {
|
||||
applyDefaultSettingsUponRegistration(user: User): Promise<void>
|
||||
createOrReplace(dto: CreateOrReplaceSettingDto): Promise<CreateOrReplaceSettingResponse>
|
||||
findSettingWithDecryptedValue(dto: FindSettingDTO): Promise<Setting | null>
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
|
||||
import { SettingsAssociationService } from './SettingsAssociationService'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { SettingDescription } from './SettingDescription'
|
||||
import { SettingName } from '@standardnotes/domain-core'
|
||||
|
||||
describe('SettingsAssociationService', () => {
|
||||
const createService = () => new SettingsAssociationService()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { LogSessionUserAgentOption, MuteMarketingEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { SettingName } from '@standardnotes/domain-core'
|
||||
import { LogSessionUserAgentOption, MuteMarketingEmailsOption } from '@standardnotes/settings'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
@@ -32,6 +33,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
|
||||
SettingName.NAMES.MuteMarketingEmails,
|
||||
SettingName.NAMES.ListedAuthorSecrets,
|
||||
SettingName.NAMES.LogSessionUserAgent,
|
||||
SettingName.NAMES.RecoveryCodes,
|
||||
]
|
||||
|
||||
private readonly CLIENT_IMMUTABLE_SETTINGS = [
|
||||
@@ -49,8 +51,6 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
|
||||
[
|
||||
SettingName.NAMES.MuteMarketingEmails,
|
||||
{
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
value: MuteMarketingEmailsOption.NotMuted,
|
||||
replaceable: false,
|
||||
},
|
||||
@@ -58,8 +58,6 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
|
||||
[
|
||||
SettingName.NAMES.LogSessionUserAgent,
|
||||
{
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
value: LogSessionUserAgentOption.Enabled,
|
||||
replaceable: false,
|
||||
},
|
||||
@@ -70,8 +68,6 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
|
||||
[
|
||||
SettingName.NAMES.LogSessionUserAgent,
|
||||
{
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
value: LogSessionUserAgentOption.Disabled,
|
||||
replaceable: false,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { SettingName } from '@standardnotes/domain-core'
|
||||
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { SettingDescription } from './SettingDescription'
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Setting } from './Setting'
|
||||
|
||||
export type SimpleSetting = Omit<Setting, 'user' | 'serverEncryptionVersion'>
|
||||
@@ -1,3 +0,0 @@
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
|
||||
export type SimpleSubscriptionSetting = Omit<SubscriptionSetting, 'userSubscription' | 'serverEncryptionVersion'>
|
||||
@@ -1,60 +1,13 @@
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
@Entity({ name: 'subscription_settings' })
|
||||
@Index('index_settings_on_name_and_user_subscription_uuid', ['name', 'userSubscription'])
|
||||
export class SubscriptionSetting {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
declare uuid: string
|
||||
import { SubscriptionSettingProps } from './SubscriptionSettingProps'
|
||||
|
||||
@Column({
|
||||
length: 255,
|
||||
})
|
||||
declare name: string
|
||||
export class SubscriptionSetting extends Entity<SubscriptionSettingProps> {
|
||||
private constructor(props: SubscriptionSettingProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@Column({
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
})
|
||||
declare value: string | null
|
||||
|
||||
@Column({
|
||||
name: 'server_encryption_version',
|
||||
type: 'tinyint',
|
||||
default: EncryptionVersion.Unencrypted,
|
||||
})
|
||||
declare serverEncryptionVersion: number
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'bigint',
|
||||
})
|
||||
declare createdAt: number
|
||||
|
||||
@Column({
|
||||
name: 'updated_at',
|
||||
type: 'bigint',
|
||||
})
|
||||
@Index('index_subcsription_settings_on_updated_at')
|
||||
declare updatedAt: number
|
||||
|
||||
@ManyToOne(
|
||||
/* istanbul ignore next */
|
||||
() => UserSubscription,
|
||||
/* istanbul ignore next */
|
||||
(userSubscription) => userSubscription.subscriptionSettings,
|
||||
/* istanbul ignore next */
|
||||
{ onDelete: 'CASCADE', nullable: false, lazy: true, eager: false },
|
||||
)
|
||||
@JoinColumn({ name: 'user_subscription_uuid', referencedColumnName: 'uuid' })
|
||||
declare userSubscription: Promise<UserSubscription>
|
||||
|
||||
@Column({
|
||||
type: 'tinyint',
|
||||
width: 1,
|
||||
nullable: false,
|
||||
default: 0,
|
||||
})
|
||||
declare sensitive: boolean
|
||||
static create(props: SubscriptionSettingProps, id?: UniqueEntityId): Result<SubscriptionSetting> {
|
||||
return Result.ok<SubscriptionSetting>(new SubscriptionSetting(props, id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
import { Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export type SubscriptionSettingProps = Omit<
|
||||
SubscriptionSetting,
|
||||
'uuid' | 'userSubscription' | 'createdAt' | 'updatedAt' | 'serverEncryptionVersion' | 'value'
|
||||
> & {
|
||||
uuid?: string
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
unencryptedValue: string | null
|
||||
serverEncryptionVersion?: number
|
||||
export interface SubscriptionSettingProps {
|
||||
name: string
|
||||
value: string | null
|
||||
serverEncryptionVersion: number
|
||||
timestamps: Timestamps
|
||||
sensitive: boolean
|
||||
userSubscriptionUuid: Uuid
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
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>
|
||||
findOneByUuid(uuid: Uuid): Promise<SubscriptionSetting | null>
|
||||
findLastByNameAndUserSubscriptionUuid(name: string, userSubscriptionUuid: Uuid): Promise<SubscriptionSetting | null>
|
||||
findAllBySubscriptionUuid(userSubscriptionUuid: Uuid): Promise<SubscriptionSetting[]>
|
||||
insert(subscriptionSetting: SubscriptionSetting): Promise<void>
|
||||
update(subscriptionSetting: SubscriptionSetting): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
|
||||
import { SubscriptionSettingService } from './SubscriptionSettingService'
|
||||
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
|
||||
import { SubscriptionSettingRepositoryInterface } from './SubscriptionSettingRepositoryInterface'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
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
|
||||
let user: User
|
||||
let userSubscription: UserSubscription
|
||||
let factory: SettingFactoryInterface
|
||||
let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface
|
||||
let subscriptionSettingsAssociationService: SubscriptionSettingsAssociationServiceInterface
|
||||
let settingInterpreter: SettingInterpreterInterface
|
||||
let settingDecrypter: SettingDecrypterInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createService = () =>
|
||||
new SubscriptionSettingService(
|
||||
factory,
|
||||
subscriptionSettingRepository,
|
||||
subscriptionSettingsAssociationService,
|
||||
settingInterpreter,
|
||||
settingDecrypter,
|
||||
userSubscriptionRepository,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userSubscription = {
|
||||
uuid: '1-2-3',
|
||||
user: Promise.resolve(user),
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
setting = {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
} as jest.Mocked<SubscriptionSetting>
|
||||
|
||||
factory = {} as jest.Mocked<SettingFactoryInterface>
|
||||
factory.createSubscriptionSetting = jest.fn().mockReturnValue(setting)
|
||||
factory.createSubscriptionSettingReplacement = jest.fn().mockReturnValue(setting)
|
||||
|
||||
subscriptionSettingRepository = {} as jest.Mocked<SubscriptionSettingRepositoryInterface>
|
||||
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null)
|
||||
subscriptionSettingRepository.save = jest.fn().mockImplementation((setting) => setting)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: 's-1-2-3',
|
||||
} as jest.Mocked<UserSubscription>,
|
||||
{
|
||||
uuid: 's-2-3-4',
|
||||
} as jest.Mocked<UserSubscription>,
|
||||
])
|
||||
|
||||
subscriptionSettingsAssociationService = {} as jest.Mocked<SubscriptionSettingsAssociationServiceInterface>
|
||||
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesUsed,
|
||||
{
|
||||
value: '0',
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
replaceable: true,
|
||||
},
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
||||
settingInterpreter = {} as jest.Mocked<SettingInterpreterInterface>
|
||||
settingInterpreter.interpretSettingUpdated = jest.fn()
|
||||
|
||||
settingDecrypter = {} as jest.Mocked<SettingDecrypterInterface>
|
||||
settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should create default settings for a subscription', async () => {
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
|
||||
it('should create default settings for a subscription with overrides', async () => {
|
||||
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesUsed,
|
||||
{
|
||||
value: '0',
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
replaceable: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesLimit,
|
||||
{
|
||||
value: '345',
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
replaceable: true,
|
||||
},
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, '123']]),
|
||||
)
|
||||
|
||||
expect(factory.createSubscriptionSetting).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
unencryptedValue: '0',
|
||||
},
|
||||
{
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
user: Promise.resolve(user),
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
)
|
||||
expect(factory.createSubscriptionSetting).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{
|
||||
name: SettingName.NAMES.FileUploadBytesLimit,
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
unencryptedValue: '123',
|
||||
},
|
||||
{
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
user: Promise.resolve(user),
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
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)).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)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should reassign existing default settings for a subscription if it is not replaceable', async () => {
|
||||
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesUsed,
|
||||
{
|
||||
value: '0',
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
replaceable: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
)
|
||||
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(setting)
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create default settings for a subscription if it is not replaceable and not existing', async () => {
|
||||
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesUsed,
|
||||
{
|
||||
value: '0',
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
replaceable: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
)
|
||||
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
|
||||
it('should create default settings for a subscription if it is not replaceable and no previous subscription existed', async () => {
|
||||
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesUsed,
|
||||
{
|
||||
value: '0',
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
replaceable: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
)
|
||||
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null)
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
} as jest.Mocked<UserSubscription>,
|
||||
])
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
|
||||
it('should not create default settings for a subscription if subscription has no defaults', async () => {
|
||||
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest
|
||||
.fn()
|
||||
.mockReturnValue(undefined)
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should create setting if it doesn't exist", async () => {
|
||||
const result = await createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesLimit,
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: 1,
|
||||
sensitive: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toEqual('created')
|
||||
})
|
||||
|
||||
it('should throw error if the setting name is not valid', async () => {
|
||||
await expect(
|
||||
createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
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,
|
||||
user,
|
||||
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)
|
||||
|
||||
const result = await createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
uuid: '1-2-3',
|
||||
name: SettingName.NAMES.FileUploadBytesLimit,
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: 1,
|
||||
sensitive: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toEqual('created')
|
||||
})
|
||||
|
||||
it('should replace setting if it does exist', async () => {
|
||||
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(setting)
|
||||
|
||||
const result = await createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
...setting,
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: 1,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toEqual('replaced')
|
||||
})
|
||||
|
||||
it('should replace setting with a given uuid if it does exist', async () => {
|
||||
subscriptionSettingRepository.findOneByUuid = jest.fn().mockReturnValue(setting)
|
||||
|
||||
const result = await createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
...setting,
|
||||
uuid: '1-2-3',
|
||||
unencryptedValue: 'value',
|
||||
serverEncryptionVersion: 1,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toEqual('replaced')
|
||||
})
|
||||
|
||||
it('should find and decrypt the value of a setting for user', async () => {
|
||||
setting = {
|
||||
value: 'encrypted',
|
||||
serverEncryptionVersion: EncryptionVersion.Default,
|
||||
} as jest.Mocked<SubscriptionSetting>
|
||||
|
||||
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createService().findSubscriptionSettingWithDecryptedValue({
|
||||
userSubscriptionUuid: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
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,192 +0,0 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
|
||||
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
|
||||
import { SettingDescription } from './SettingDescription'
|
||||
import { SubscriptionSettingServiceInterface } from './SubscriptionSettingServiceInterface'
|
||||
import { CreateOrReplaceSubscriptionSettingDTO } from './CreateOrReplaceSubscriptionSettingDTO'
|
||||
import { CreateOrReplaceSubscriptionSettingResponse } from './CreateOrReplaceSubscriptionSettingResponse'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
import { FindSubscriptionSettingDTO } from './FindSubscriptionSettingDTO'
|
||||
import { SubscriptionSettingRepositoryInterface } from './SubscriptionSettingRepositoryInterface'
|
||||
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 {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_SettingFactory) private factory: SettingFactoryInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingRepository)
|
||||
private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingsAssociationService)
|
||||
private subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface,
|
||||
@inject(TYPES.Auth_SettingInterpreter) private settingInterpreter: SettingInterpreterInterface,
|
||||
@inject(TYPES.Auth_SettingDecrypter) private settingDecrypter: SettingDecrypterInterface,
|
||||
@inject(TYPES.Auth_UserSubscriptionRepository)
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription: UserSubscription,
|
||||
overrides?: Map<string, string>,
|
||||
): Promise<void> {
|
||||
const defaultSettingsWithValues =
|
||||
await this.subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName(
|
||||
userSubscription.planName,
|
||||
)
|
||||
if (defaultSettingsWithValues === undefined) {
|
||||
this.logger.warn(`Could not find settings for subscription: ${userSubscription.planName}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const user = await userSubscription.user
|
||||
|
||||
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,
|
||||
user.uuid,
|
||||
)
|
||||
if (existingSetting !== null) {
|
||||
existingSetting.userSubscription = Promise.resolve(userSubscription)
|
||||
await this.subscriptionSettingRepository.save(existingSetting)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let unencryptedValue = setting.value
|
||||
if (overrides && overrides.has(settingName.value)) {
|
||||
unencryptedValue = overrides.get(settingName.value) as string
|
||||
}
|
||||
|
||||
await this.createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
name: settingName.value,
|
||||
unencryptedValue,
|
||||
serverEncryptionVersion: setting.serverEncryptionVersion,
|
||||
sensitive: setting.sensitive,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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.value,
|
||||
dto.userSubscriptionUuid,
|
||||
)
|
||||
}
|
||||
|
||||
if (setting === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
setting.value = await this.settingDecrypter.decryptSettingValue(setting, dto.userUuid)
|
||||
|
||||
return setting
|
||||
}
|
||||
|
||||
async createOrReplace(
|
||||
dto: CreateOrReplaceSubscriptionSettingDTO,
|
||||
): Promise<CreateOrReplaceSubscriptionSettingResponse> {
|
||||
const { userSubscription, user, 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 existing = await this.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: user.uuid,
|
||||
userSubscriptionUuid: userSubscription.uuid,
|
||||
subscriptionSettingName: settingName,
|
||||
settingUuid: props.uuid,
|
||||
})
|
||||
|
||||
if (existing === null) {
|
||||
const subscriptionSetting = await this.subscriptionSettingRepository.save(
|
||||
await this.factory.createSubscriptionSetting(props, userSubscription),
|
||||
)
|
||||
|
||||
this.logger.debug('Created subscription setting %s: %O', props.name, subscriptionSetting)
|
||||
|
||||
await this.settingInterpreter.interpretSettingUpdated(settingName.value, user, props.unencryptedValue)
|
||||
|
||||
return {
|
||||
status: 'created',
|
||||
subscriptionSetting,
|
||||
}
|
||||
}
|
||||
|
||||
const subscriptionSetting = await this.subscriptionSettingRepository.save(
|
||||
await this.factory.createSubscriptionSettingReplacement(existing, props),
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
private async findPreviousSubscriptionSetting(
|
||||
settingName: SettingName,
|
||||
currentUserSubscriptionUuid: string,
|
||||
userUuid: string,
|
||||
): Promise<SubscriptionSetting | null> {
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findByUserUuid(userUuid)
|
||||
const previousSubscriptions = userSubscriptions.filter(
|
||||
(subscription) => subscription.uuid !== currentUserSubscriptionUuid,
|
||||
)
|
||||
const lastSubscription = previousSubscriptions.shift()
|
||||
|
||||
if (!lastSubscription) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid(
|
||||
settingName.value,
|
||||
lastSubscription.uuid,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
|
||||
import { CreateOrReplaceSubscriptionSettingDTO } from './CreateOrReplaceSubscriptionSettingDTO'
|
||||
import { CreateOrReplaceSubscriptionSettingResponse } from './CreateOrReplaceSubscriptionSettingResponse'
|
||||
import { FindSubscriptionSettingDTO } from './FindSubscriptionSettingDTO'
|
||||
import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
|
||||
export interface SubscriptionSettingServiceInterface {
|
||||
applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription: UserSubscription,
|
||||
overrides?: Map<string, string>,
|
||||
): Promise<void>
|
||||
createOrReplace(dto: CreateOrReplaceSubscriptionSettingDTO): Promise<CreateOrReplaceSubscriptionSettingResponse>
|
||||
findSubscriptionSettingWithDecryptedValue(dto: FindSubscriptionSettingDTO): Promise<SubscriptionSetting | null>
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { RoleName, SettingName } from '@standardnotes/domain-core'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
|
||||
import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface'
|
||||
import { Role } from '../Role/Role'
|
||||
import { Permission } from '../Permission/Permission'
|
||||
import { SubscriptionSettingsAssociationService } from './SubscriptionSettingsAssociationService'
|
||||
import { SettingDescription } from './SettingDescription'
|
||||
|
||||
describe('SubscriptionSettingsAssociationService', () => {
|
||||
let roleToSubscriptionMap: RoleToSubscriptionMapInterface
|
||||
@@ -49,15 +48,9 @@ describe('SubscriptionSettingsAssociationService', () => {
|
||||
|
||||
expect(settings).not.toBeUndefined()
|
||||
|
||||
const flatSettings = [
|
||||
...(
|
||||
settings as Map<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
|
||||
).keys(),
|
||||
]
|
||||
const flatSettings = [...(settings as Map<string, SettingDescription>).keys()]
|
||||
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',
|
||||
replaceable: true,
|
||||
})
|
||||
@@ -74,15 +67,9 @@ describe('SubscriptionSettingsAssociationService', () => {
|
||||
|
||||
expect(settings).not.toBeUndefined()
|
||||
|
||||
const flatSettings = [
|
||||
...(
|
||||
settings as Map<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
|
||||
).keys(),
|
||||
]
|
||||
const flatSettings = [...(settings as Map<string, SettingDescription>).keys()]
|
||||
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',
|
||||
replaceable: true,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { SettingName } from '@standardnotes/domain-core'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { Permission } from '../Permission/Permission'
|
||||
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
|
||||
import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface'
|
||||
@@ -23,15 +22,10 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
|
||||
[
|
||||
SubscriptionName.PlusPlan,
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesUsed,
|
||||
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false },
|
||||
],
|
||||
[SettingName.NAMES.FileUploadBytesUsed, { value: '0', replaceable: false }],
|
||||
[
|
||||
SettingName.NAMES.MuteSignInEmails,
|
||||
{
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
value: 'not_muted',
|
||||
replaceable: false,
|
||||
},
|
||||
@@ -41,15 +35,10 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
|
||||
[
|
||||
SubscriptionName.ProPlan,
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesUsed,
|
||||
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false },
|
||||
],
|
||||
[SettingName.NAMES.FileUploadBytesUsed, { value: '0', replaceable: false }],
|
||||
[
|
||||
SettingName.NAMES.MuteSignInEmails,
|
||||
{
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
value: 'not_muted',
|
||||
replaceable: false,
|
||||
},
|
||||
@@ -68,8 +57,6 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
|
||||
}
|
||||
|
||||
defaultSettings.set(SettingName.NAMES.FileUploadBytesLimit, {
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
value: (await this.getFileUploadLimit(subscriptionName)).toString(),
|
||||
replaceable: true,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { SubscriptionSetting } from '../Setting/SubscriptionSetting'
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { User } from '../User/User'
|
||||
import { UserSubscriptionType } from './UserSubscriptionType'
|
||||
|
||||
@@ -74,14 +73,4 @@ export class UserSubscription {
|
||||
)
|
||||
@JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' })
|
||||
declare user: Promise<User>
|
||||
|
||||
@OneToMany(
|
||||
/* istanbul ignore next */
|
||||
() => SubscriptionSetting,
|
||||
/* istanbul ignore next */
|
||||
(subscriptionSetting) => subscriptionSetting.userSubscription,
|
||||
/* istanbul ignore next */
|
||||
{ lazy: true, eager: false },
|
||||
)
|
||||
declare subscriptionSettings: Promise<SubscriptionSetting[]>
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface UserSubscriptionRepositoryInterface {
|
||||
findOneByUuid(uuid: string): Promise<UserSubscription | null>
|
||||
countByUserUuid(userUuid: string): Promise<number>
|
||||
findOneByUserUuid(userUuid: string): Promise<UserSubscription | null>
|
||||
findOneByUserUuidAndType(userUuid: string, type: UserSubscriptionType): Promise<UserSubscription | null>
|
||||
findByUserUuid(userUuid: string): Promise<UserSubscription[]>
|
||||
findOneByUserUuidAndSubscriptionId(userUuid: string, subscriptionId: number): Promise<UserSubscription | null>
|
||||
findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise<UserSubscription[]>
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { User } from '../User/User'
|
||||
import { UserSubscription } from './UserSubscription'
|
||||
import { UserSubscriptionRepositoryInterface } from './UserSubscriptionRepositoryInterface'
|
||||
|
||||
import { UserSubscriptionService } from './UserSubscriptionService'
|
||||
import { UserSubscriptionType } from './UserSubscriptionType'
|
||||
|
||||
describe('UserSubscriptionService', () => {
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let regularSubscription: UserSubscription
|
||||
let sharedSubscription: UserSubscription
|
||||
let user: User
|
||||
|
||||
const createService = () => new UserSubscriptionService(userSubscriptionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
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>
|
||||
sharedSubscription = {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
user: Promise.resolve(user),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
||||
userSubscriptionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([])
|
||||
})
|
||||
|
||||
describe('by uuid', () => {
|
||||
it('should return undefined if there is no user subscription', async () => {
|
||||
expect(await createService().findRegularSubscriptionForUuid('1-2-3')).toEqual({
|
||||
regularSubscription: null,
|
||||
sharedSubscription: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a regular subscription if the uuid corresponds to a regular subscription', async () => {
|
||||
userSubscriptionRepository.findOneByUuid = jest.fn().mockReturnValue(regularSubscription)
|
||||
|
||||
expect(await createService().findRegularSubscriptionForUuid('1-2-3')).toEqual({
|
||||
regularSubscription,
|
||||
sharedSubscription: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a regular subscription if the uuid corresponds to a shared subscription', async () => {
|
||||
userSubscriptionRepository.findOneByUuid = jest.fn().mockReturnValue(sharedSubscription)
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([regularSubscription])
|
||||
|
||||
expect(await createService().findRegularSubscriptionForUuid('1-2-3')).toEqual({
|
||||
regularSubscription,
|
||||
sharedSubscription,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return undefined if a regular subscription is not found corresponding to the shared subscription', async () => {
|
||||
userSubscriptionRepository.findOneByUuid = jest.fn().mockReturnValue(sharedSubscription)
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([])
|
||||
|
||||
expect(await createService().findRegularSubscriptionForUuid('1-2-3')).toEqual({
|
||||
regularSubscription: null,
|
||||
sharedSubscription,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('by user uuid', () => {
|
||||
it('should return undefined if there is no user subscription', async () => {
|
||||
expect(await createService().findRegularSubscriptionForUserUuid('1-2-3')).toEqual({
|
||||
regularSubscription: null,
|
||||
sharedSubscription: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a regular subscription if the uuid corresponds to a regular subscription', async () => {
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(regularSubscription)
|
||||
|
||||
expect(await createService().findRegularSubscriptionForUserUuid('1-2-3')).toEqual({
|
||||
regularSubscription,
|
||||
sharedSubscription: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a regular subscription if the uuid corresponds to a shared subscription', async () => {
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(sharedSubscription)
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([regularSubscription])
|
||||
|
||||
expect(await createService().findRegularSubscriptionForUserUuid('1-2-3')).toEqual({
|
||||
regularSubscription,
|
||||
sharedSubscription,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return undefined if a regular subscription is not found corresponding to the shared subscription', async () => {
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(sharedSubscription)
|
||||
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([])
|
||||
|
||||
expect(await createService().findRegularSubscriptionForUserUuid('1-2-3')).toEqual({
|
||||
regularSubscription: null,
|
||||
sharedSubscription,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,63 +0,0 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { FindRegularSubscriptionResponse } from './FindRegularSubscriptionResponse'
|
||||
|
||||
import { UserSubscription } from './UserSubscription'
|
||||
import { UserSubscriptionRepositoryInterface } from './UserSubscriptionRepositoryInterface'
|
||||
import { UserSubscriptionServiceInterface } from './UserSubscriptionServiceInterface'
|
||||
import { UserSubscriptionType } from './UserSubscriptionType'
|
||||
|
||||
@injectable()
|
||||
export class UserSubscriptionService implements UserSubscriptionServiceInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserSubscriptionRepository)
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async findRegularSubscriptionForUserUuid(userUuid: string): Promise<FindRegularSubscriptionResponse> {
|
||||
const userSubscription = await this.userSubscriptionRepository.findOneByUserUuid(userUuid)
|
||||
|
||||
return this.findRegularSubscription(userSubscription)
|
||||
}
|
||||
|
||||
async findRegularSubscriptionForUuid(uuid: string): Promise<FindRegularSubscriptionResponse> {
|
||||
const userSubscription = await this.userSubscriptionRepository.findOneByUuid(uuid)
|
||||
|
||||
return this.findRegularSubscription(userSubscription)
|
||||
}
|
||||
|
||||
private async findRegularSubscription(
|
||||
userSubscription: UserSubscription | null,
|
||||
): Promise<FindRegularSubscriptionResponse> {
|
||||
if (userSubscription === null) {
|
||||
return {
|
||||
regularSubscription: null,
|
||||
sharedSubscription: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (userSubscription.subscriptionType === UserSubscriptionType.Regular) {
|
||||
return {
|
||||
regularSubscription: userSubscription,
|
||||
sharedSubscription: null,
|
||||
}
|
||||
}
|
||||
|
||||
const regularSubscriptions = await this.userSubscriptionRepository.findBySubscriptionIdAndType(
|
||||
userSubscription.subscriptionId as number,
|
||||
UserSubscriptionType.Regular,
|
||||
)
|
||||
if (regularSubscriptions.length === 0) {
|
||||
return {
|
||||
regularSubscription: null,
|
||||
sharedSubscription: userSubscription,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
regularSubscription: regularSubscriptions[0],
|
||||
sharedSubscription: userSubscription,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { FindRegularSubscriptionResponse } from './FindRegularSubscriptionResponse'
|
||||
|
||||
export interface UserSubscriptionServiceInterface {
|
||||
findRegularSubscriptionForUuid(uuid: string): Promise<FindRegularSubscriptionResponse>
|
||||
findRegularSubscriptionForUserUuid(userUuid: string): Promise<FindRegularSubscriptionResponse>
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { Result, RoleName } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
@@ -13,19 +13,21 @@ import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { AcceptSharedSubscriptionInvitation } from './AcceptSharedSubscriptionInvitation'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
let sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings
|
||||
let timer: TimerInterface
|
||||
let invitee: User
|
||||
let inviterSubscription: UserSubscription
|
||||
let inviteeSubscription: UserSubscription
|
||||
let invitation: SharedSubscriptionInvitation
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () =>
|
||||
new AcceptSharedSubscriptionInvitation(
|
||||
@@ -33,8 +35,9 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
userRepository,
|
||||
userSubscriptionRepository,
|
||||
roleService,
|
||||
subscriptionSettingService,
|
||||
applyDefaultSubscriptionSettings,
|
||||
timer,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -71,11 +74,14 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
|
||||
applyDefaultSubscriptionSettings = {} as jest.Mocked<ApplyDefaultSubscriptionSettings>
|
||||
applyDefaultSubscriptionSettings.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should create a shared subscription upon accepting the invitation', async () => {
|
||||
@@ -104,9 +110,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
user: Promise.resolve(invitee),
|
||||
})
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
inviteeSubscription,
|
||||
)
|
||||
expect(applyDefaultSubscriptionSettings.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create a shared subscription upon accepting the invitation if inviter has a second subscription', async () => {
|
||||
@@ -144,9 +148,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
user: Promise.resolve(invitee),
|
||||
})
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
inviteeSubscription,
|
||||
)
|
||||
expect(applyDefaultSubscriptionSettings.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a shared subscription if invitiation is not found', async () => {
|
||||
@@ -163,7 +165,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a shared subscription if invitee is not found', async () => {
|
||||
@@ -181,7 +183,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a shared subscription if invitee email is invalid', async () => {
|
||||
@@ -203,7 +205,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a shared subscription if inviter subscription is not found', async () => {
|
||||
@@ -220,7 +222,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a shared subscription if inviter subscriptions are not active', async () => {
|
||||
@@ -245,6 +247,6 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
expect(applyDefaultSubscriptionSettings.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { Logger } from 'winston'
|
||||
import { Email } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
|
||||
import { SharedSubscriptionInvitationRepositoryInterface } from '../../SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
@@ -17,19 +15,17 @@ import { UseCaseInterface } from '../UseCaseInterface'
|
||||
|
||||
import { AcceptSharedSubscriptionInvitationDTO } from './AcceptSharedSubscriptionInvitationDTO'
|
||||
import { AcceptSharedSubscriptionInvitationResponse } from './AcceptSharedSubscriptionInvitationResponse'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
|
||||
@injectable()
|
||||
export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_SharedSubscriptionInvitationRepository)
|
||||
private sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface,
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_UserSubscriptionRepository)
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingService)
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
private roleService: RoleServiceInterface,
|
||||
private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings,
|
||||
private timer: TimerInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: AcceptSharedSubscriptionInvitationDTO): Promise<AcceptSharedSubscriptionInvitationResponse> {
|
||||
@@ -92,7 +88,15 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
|
||||
await this.addUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(inviteeSubscription)
|
||||
const result = await this.applyDefaultSubscriptionSettings.execute({
|
||||
subscriptionPlanName: inviterUserSubscription.planName,
|
||||
userSubscriptionUuid: inviteeSubscription.uuid,
|
||||
userUuid: invitee.uuid,
|
||||
})
|
||||
/* istanbul ignore next */
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not apply default subscription settings for user with uuid ${invitee.uuid}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -5,12 +5,14 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { ActivatePremiumFeatures } from './ActivatePremiumFeatures'
|
||||
import { User } from '../../User/User'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('ActivatePremiumFeatures', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let subscriptionSettingsService: SubscriptionSettingServiceInterface
|
||||
let applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings
|
||||
let roleService: RoleServiceInterface
|
||||
let timer: TimerInterface
|
||||
let user: User
|
||||
@@ -19,7 +21,7 @@ describe('ActivatePremiumFeatures', () => {
|
||||
new ActivatePremiumFeatures(
|
||||
userRepository,
|
||||
userSubscriptionRepository,
|
||||
subscriptionSettingsService,
|
||||
applyDefaultSubscriptionSettings,
|
||||
roleService,
|
||||
timer,
|
||||
)
|
||||
@@ -31,6 +33,7 @@ describe('ActivatePremiumFeatures', () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockResolvedValue(user)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockResolvedValue(null)
|
||||
userSubscriptionRepository.save = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
@@ -41,14 +44,14 @@ describe('ActivatePremiumFeatures', () => {
|
||||
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.getUTCDateNDaysAhead = jest.fn().mockReturnValue(new Date('2024-01-01T00:00:00.000Z'))
|
||||
|
||||
subscriptionSettingsService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingsService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
|
||||
applyDefaultSubscriptionSettings = {} as jest.Mocked<ApplyDefaultSubscriptionSettings>
|
||||
applyDefaultSubscriptionSettings.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
})
|
||||
|
||||
it('should return error when username is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ username: '' })
|
||||
const result = await useCase.execute({ username: '', subscriptionId: 1 })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Username cannot be empty')
|
||||
@@ -59,7 +62,7 @@ describe('ActivatePremiumFeatures', () => {
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ username: 'test@test.te' })
|
||||
const result = await useCase.execute({ username: 'test@test.te', subscriptionId: 1 })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('User not found with username: test@test.te')
|
||||
@@ -68,7 +71,24 @@ describe('ActivatePremiumFeatures', () => {
|
||||
it('should save a subscription and add role to user', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({ username: 'test@test.te' })
|
||||
const result = await useCase.execute({ username: 'test@test.te', subscriptionId: 1 })
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should cancel previous subscription if cancelPreviousSubscription is true', async () => {
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockResolvedValue({} as jest.Mocked<UserSubscription>)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
username: 'test@test.te',
|
||||
subscriptionId: 1,
|
||||
cancelPreviousSubscription: true,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
|
||||
@@ -81,6 +101,7 @@ describe('ActivatePremiumFeatures', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
username: 'test@test.te',
|
||||
subscriptionId: 1,
|
||||
subscriptionPlanName: 'PRO_PLAN',
|
||||
endsAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||
})
|
||||
@@ -93,6 +114,7 @@ describe('ActivatePremiumFeatures', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
username: 'test@test.te',
|
||||
subscriptionId: 1,
|
||||
subscriptionPlanName: 'some invalid plan name',
|
||||
endsAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Result, SubscriptionPlanName, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||
import { Result, SettingName, SubscriptionPlanName, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
|
||||
@@ -7,14 +7,13 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { ActivatePremiumFeaturesDTO } from './ActivatePremiumFeaturesDTO'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
|
||||
export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings,
|
||||
private roleService: RoleServiceInterface,
|
||||
private timer: TimerInterface,
|
||||
) {}
|
||||
@@ -30,6 +29,17 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
if (user === null) {
|
||||
return Result.fail(`User not found with username: ${username.value}`)
|
||||
}
|
||||
|
||||
if (dto.cancelPreviousSubscription) {
|
||||
const previousSubscription = await this.userSubscriptionRepository.findOneByUserUuid(user.uuid)
|
||||
if (previousSubscription) {
|
||||
previousSubscription.cancelled = true
|
||||
previousSubscription.updatedAt = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
await this.userSubscriptionRepository.save(previousSubscription)
|
||||
}
|
||||
}
|
||||
|
||||
const subscriptionPlanNameString = dto.subscriptionPlanName ?? SubscriptionPlanName.NAMES.ProPlan
|
||||
const subscriptionPlanNameOrError = SubscriptionPlanName.create(subscriptionPlanNameString)
|
||||
if (subscriptionPlanNameOrError.isFailed()) {
|
||||
@@ -48,17 +58,19 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
subscription.updatedAt = timestamp
|
||||
subscription.endsAt = this.timer.convertDateToMicroseconds(endsAt)
|
||||
subscription.cancelled = false
|
||||
subscription.subscriptionId = 1
|
||||
subscription.subscriptionId = dto.subscriptionId
|
||||
subscription.subscriptionType = UserSubscriptionType.Regular
|
||||
|
||||
await this.userSubscriptionRepository.save(subscription)
|
||||
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionPlanName.value)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
|
||||
subscription,
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
|
||||
)
|
||||
await this.applyDefaultSubscriptionSettings.execute({
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
userUuid: user.uuid,
|
||||
subscriptionPlanName: subscriptionPlanName.value,
|
||||
overrides: new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
|
||||
})
|
||||
|
||||
return Result.ok('Premium features activated.')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export interface ActivatePremiumFeaturesDTO {
|
||||
username: string
|
||||
subscriptionId: number
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
cancelPreviousSubscription?: boolean
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface'
|
||||
import { SetSettingValue } from '../SetSettingValue/SetSettingValue'
|
||||
import { ApplyDefaultSettings } from './ApplyDefaultSettings'
|
||||
|
||||
describe('ApplyDefaultSettings', () => {
|
||||
let settingsAssociationService: SettingsAssociationServiceInterface
|
||||
let setSettingValue: SetSettingValue
|
||||
|
||||
const createUseCase = () => new ApplyDefaultSettings(settingsAssociationService, setSettingValue)
|
||||
|
||||
beforeEach(() => {
|
||||
settingsAssociationService = {} as jest.Mocked<SettingsAssociationServiceInterface>
|
||||
settingsAssociationService.getDefaultSettingsAndValuesForNewUser = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
['setting1', { value: 'value1', sensitive: false, serverEncryptionVersion: 0 }],
|
||||
['setting2', { value: 'value2', sensitive: false, serverEncryptionVersion: 0 }],
|
||||
]),
|
||||
)
|
||||
|
||||
setSettingValue = {} as jest.Mocked<SetSettingValue>
|
||||
setSettingValue.execute = jest.fn().mockReturnValue(Promise.resolve())
|
||||
})
|
||||
|
||||
it('should set default settings for a new user', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userName: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(setSettingValue.execute).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should set default settings for a new private username account', async () => {
|
||||
settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
['setting1', { value: 'value1', sensitive: false, serverEncryptionVersion: 0 }],
|
||||
['setting2', { value: 'value2', sensitive: false, serverEncryptionVersion: 0 }],
|
||||
]),
|
||||
)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userName: 'a75a31ce95365904ef0e0a8e6cefc1f5e99adfef81bbdb6d4499eeb10ae0ff67',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(setSettingValue.execute).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should fail if user uuid is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
userName: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fail if user name is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userName: '',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Result, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface'
|
||||
import { ApplyDefaultSettingsDTO } from './ApplyDefaultSettingsDTO'
|
||||
import { SetSettingValue } from '../SetSettingValue/SetSettingValue'
|
||||
import { SettingDescription } from '../../Setting/SettingDescription'
|
||||
|
||||
export class ApplyDefaultSettings implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private settingsAssociationService: SettingsAssociationServiceInterface,
|
||||
private setSettingValue: SetSettingValue,
|
||||
) {}
|
||||
|
||||
async execute(dto: ApplyDefaultSettingsDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const userNameOrError = Username.create(dto.userName)
|
||||
if (userNameOrError.isFailed()) {
|
||||
return Result.fail(userNameOrError.getError())
|
||||
}
|
||||
const userName = userNameOrError.getValue()
|
||||
|
||||
let defaultSettingsWithValues = this.settingsAssociationService.getDefaultSettingsAndValuesForNewUser()
|
||||
if (userName.isPotentiallyAPrivateUsernameAccount()) {
|
||||
defaultSettingsWithValues =
|
||||
this.settingsAssociationService.getDefaultSettingsAndValuesForNewPrivateUsernameAccount()
|
||||
}
|
||||
|
||||
for (const settingName of defaultSettingsWithValues.keys()) {
|
||||
const setting = defaultSettingsWithValues.get(settingName) as SettingDescription
|
||||
|
||||
await this.setSettingValue.execute({
|
||||
settingName: settingName,
|
||||
userUuid: userUuid.value,
|
||||
value: setting.value,
|
||||
})
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ApplyDefaultSettingsDTO {
|
||||
userUuid: string
|
||||
userName: string
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Result, SettingName, SubscriptionPlanName, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
|
||||
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting'
|
||||
import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue'
|
||||
import { ApplyDefaultSubscriptionSettings } from './ApplyDefaultSubscriptionSettings'
|
||||
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
|
||||
|
||||
describe('ApplyDefaultSubscriptionSettings', () => {
|
||||
let subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let getSubscriptionSetting: GetSubscriptionSetting
|
||||
let setSubscriptionSettingValue: SetSubscriptionSettingValue
|
||||
|
||||
const createUseCase = () =>
|
||||
new ApplyDefaultSubscriptionSettings(
|
||||
subscriptionSettingAssociationService,
|
||||
userSubscriptionRepository,
|
||||
getSubscriptionSetting,
|
||||
setSubscriptionSettingValue,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionSettingAssociationService = {} as jest.Mocked<SubscriptionSettingsAssociationServiceInterface>
|
||||
subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.MuteSignInEmails,
|
||||
{ value: 'value1', sensitive: false, serverEncryptionVersion: 0, replaceable: true },
|
||||
],
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesLimit,
|
||||
{ value: 'value2', sensitive: false, serverEncryptionVersion: 0, replaceable: false },
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
} as jest.Mocked<UserSubscription>,
|
||||
])
|
||||
|
||||
getSubscriptionSetting = {} as jest.Mocked<GetSubscriptionSetting>
|
||||
getSubscriptionSetting.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
setting: SubscriptionSetting.create({
|
||||
sensitive: false,
|
||||
name: SettingName.NAMES.FileUploadBytesLimit,
|
||||
value: '100',
|
||||
timestamps: Timestamps.create(123456789, 123456789).getValue(),
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
}).getValue(),
|
||||
}),
|
||||
)
|
||||
|
||||
setSubscriptionSettingValue = {} as jest.Mocked<SetSubscriptionSettingValue>
|
||||
setSubscriptionSettingValue.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
})
|
||||
|
||||
it('should set default settings for a subscription', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(setSubscriptionSettingValue.execute).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should fail if user uuid is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fail if user subscription uuid is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userSubscriptionUuid: 'invalid',
|
||||
subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fail if subscription plan name is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionPlanName: 'invalid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fail if subscription setting for plan name are not found', async () => {
|
||||
subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest
|
||||
.fn()
|
||||
.mockReturnValue(undefined)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shold fail if subscription setting name is invalid', async () => {
|
||||
subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest
|
||||
.fn()
|
||||
.mockReturnValue(new Map([['invalid', { value: 'value1', sensitive: false, serverEncryptionVersion: 0 }]]))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fail if subscription setting name is not a subscription setting', async () => {
|
||||
subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.MuteFailedCloudBackupsEmails,
|
||||
{ value: 'value1', sensitive: false, serverEncryptionVersion: 0 },
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fail if setting the subcription setting value fails', async () => {
|
||||
setSubscriptionSettingValue.execute = jest.fn().mockReturnValue(Result.fail('error'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should create new setting if cannot find previous subscription for a non replacable setting', async () => {
|
||||
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([])
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(setSubscriptionSettingValue.execute).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should allow to override setting values if setting is replacable', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionPlanName: SubscriptionPlanName.NAMES.ProPlan,
|
||||
overrides: new Map([
|
||||
[SettingName.NAMES.MuteSignInEmails, '000'],
|
||||
[SettingName.NAMES.FileUploadBytesLimit, '000'],
|
||||
]),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(setSubscriptionSettingValue.execute).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Result, SettingName, SubscriptionPlanName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { ApplyDefaultSubscriptionSettingsDTO } from './ApplyDefaultSubscriptionSettingsDTO'
|
||||
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||
import { SettingDescription } from '../../Setting/SettingDescription'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting'
|
||||
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
|
||||
import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue'
|
||||
|
||||
export class ApplyDefaultSubscriptionSettings implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
private getSubscriptionSetting: GetSubscriptionSetting,
|
||||
private setSubscriptionSettingValue: SetSubscriptionSettingValue,
|
||||
) {}
|
||||
|
||||
async execute(dto: ApplyDefaultSubscriptionSettingsDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const userSubscriptionUuidOrError = Uuid.create(dto.userSubscriptionUuid)
|
||||
if (userSubscriptionUuidOrError.isFailed()) {
|
||||
return Result.fail(userSubscriptionUuidOrError.getError())
|
||||
}
|
||||
const userSubscriptionUuid = userSubscriptionUuidOrError.getValue()
|
||||
|
||||
const subscriptionPlanNameOrError = SubscriptionPlanName.create(dto.subscriptionPlanName)
|
||||
if (subscriptionPlanNameOrError.isFailed()) {
|
||||
return Result.fail(subscriptionPlanNameOrError.getError())
|
||||
}
|
||||
const subscriptionPlanName = subscriptionPlanNameOrError.getValue()
|
||||
|
||||
const defaultSettingsWithValues =
|
||||
await this.subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName(
|
||||
subscriptionPlanName.value,
|
||||
)
|
||||
if (defaultSettingsWithValues === undefined) {
|
||||
return Result.fail(`Could not find default settings for subscription plan ${subscriptionPlanName.value}.`)
|
||||
}
|
||||
|
||||
for (const settingNameString of defaultSettingsWithValues.keys()) {
|
||||
const settingNameOrError = SettingName.create(settingNameString)
|
||||
if (settingNameOrError.isFailed()) {
|
||||
return Result.fail(settingNameOrError.getError())
|
||||
}
|
||||
const settingName = settingNameOrError.getValue()
|
||||
if (!settingName.isASubscriptionSetting()) {
|
||||
return Result.fail(`Setting ${settingName.value} is not a subscription setting!`)
|
||||
}
|
||||
|
||||
const setting = defaultSettingsWithValues.get(settingName.value) as SettingDescription
|
||||
if (!setting.replaceable) {
|
||||
const existingSettingOrError = await this.findPreviousSubscriptionSetting(
|
||||
settingName,
|
||||
userSubscriptionUuid.value,
|
||||
userUuid.value,
|
||||
)
|
||||
if (!existingSettingOrError.isFailed()) {
|
||||
const existingSetting = existingSettingOrError.getValue()
|
||||
const result = await this.setSubscriptionSettingValue.execute({
|
||||
userSubscriptionUuid: existingSetting.setting.props.userSubscriptionUuid.value,
|
||||
settingName: existingSetting.setting.props.name,
|
||||
value: existingSetting.setting.props.value,
|
||||
newUserSubscriptionUuid: userSubscriptionUuid.value,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let unencryptedValue = setting.value
|
||||
if (dto.overrides && dto.overrides.has(settingName.value)) {
|
||||
unencryptedValue = dto.overrides.get(settingName.value) as string
|
||||
}
|
||||
|
||||
await this.setSubscriptionSettingValue.execute({
|
||||
userSubscriptionUuid: userSubscriptionUuid.value,
|
||||
settingName: settingName.value,
|
||||
value: unencryptedValue,
|
||||
})
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async findPreviousSubscriptionSetting(
|
||||
settingName: SettingName,
|
||||
currentUserSubscriptionUuid: string,
|
||||
userUuid: string,
|
||||
): Promise<Result<{ setting: SubscriptionSetting; decryptedValue?: string | null }>> {
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findByUserUuid(userUuid)
|
||||
const previousSubscriptions = userSubscriptions.filter(
|
||||
(subscription) => subscription.uuid !== currentUserSubscriptionUuid,
|
||||
)
|
||||
const lastSubscription = previousSubscriptions.shift()
|
||||
|
||||
if (!lastSubscription) {
|
||||
return Result.fail(`Could not find previous subscription for user ${userUuid}.`)
|
||||
}
|
||||
|
||||
return this.getSubscriptionSetting.execute({
|
||||
userSubscriptionUuid: lastSubscription.uuid,
|
||||
settingName: settingName.value,
|
||||
allowSensitiveRetrieval: true,
|
||||
decryptWith: {
|
||||
userUuid,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ApplyDefaultSubscriptionSettingsDTO {
|
||||
userUuid: string
|
||||
userSubscriptionUuid: string
|
||||
subscriptionPlanName: string
|
||||
overrides?: Map<string, string>
|
||||
}
|
||||
@@ -8,9 +8,20 @@ import { Role } from '../../Role/Role'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
|
||||
import { GetSetting } from '../GetSetting/GetSetting'
|
||||
import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import {
|
||||
Result,
|
||||
SettingName,
|
||||
SharedVaultUser,
|
||||
SharedVaultUserPermission,
|
||||
Timestamps,
|
||||
Uuid,
|
||||
} from '@standardnotes/domain-core'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface'
|
||||
import { GetSubscriptionSetting } from '../GetSubscriptionSetting/GetSubscriptionSetting'
|
||||
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
|
||||
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
|
||||
|
||||
describe('CreateCrossServiceToken', () => {
|
||||
let userProjector: ProjectorInterface<User>
|
||||
@@ -18,7 +29,8 @@ describe('CreateCrossServiceToken', () => {
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
let userRepository: UserRepositoryInterface
|
||||
let getSettingUseCase: GetSetting
|
||||
let getRegularSubscription: GetRegularSubscriptionForUser
|
||||
let getSubscriptionSetting: GetSubscriptionSetting
|
||||
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
|
||||
const jwtTTL = 60
|
||||
|
||||
@@ -34,7 +46,8 @@ describe('CreateCrossServiceToken', () => {
|
||||
tokenEncoder,
|
||||
userRepository,
|
||||
jwtTTL,
|
||||
getSettingUseCase,
|
||||
getRegularSubscription,
|
||||
getSubscriptionSetting,
|
||||
sharedVaultUserRepository,
|
||||
)
|
||||
|
||||
@@ -65,8 +78,22 @@ describe('CreateCrossServiceToken', () => {
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
|
||||
getSettingUseCase = {} as jest.Mocked<GetSetting>
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } }))
|
||||
getSubscriptionSetting = {} as jest.Mocked<GetSubscriptionSetting>
|
||||
getSubscriptionSetting.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
setting: SubscriptionSetting.create({
|
||||
sensitive: false,
|
||||
name: SettingName.NAMES.FileUploadBytesLimit,
|
||||
value: '100',
|
||||
timestamps: Timestamps.create(123456789, 123456789).getValue(),
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
}).getValue(),
|
||||
}),
|
||||
)
|
||||
|
||||
getRegularSubscription = {} as jest.Mocked<GetRegularSubscriptionForUser>
|
||||
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findByUserUuid = jest.fn().mockReturnValue([
|
||||
@@ -188,6 +215,9 @@ describe('CreateCrossServiceToken', () => {
|
||||
|
||||
describe('shared vault context', () => {
|
||||
it('should add shared vault context if shared vault owner uuid is provided', async () => {
|
||||
const regularSubscription = {} as jest.Mocked<UserSubscription>
|
||||
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription))
|
||||
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
@@ -223,9 +253,7 @@ describe('CreateCrossServiceToken', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if shared vault owner context is sensitive', async () => {
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sensitive: true }))
|
||||
|
||||
it('should return an error if it fails to retrieve shared vault owner subscription', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
@@ -235,8 +263,11 @@ describe('CreateCrossServiceToken', () => {
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw an error if it fails to retrieve shared vault owner setting', async () => {
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
it('should return an error if it fails to retrieve shared vault owner setting', async () => {
|
||||
const regularSubscription = {} as jest.Mocked<UserSubscription>
|
||||
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription))
|
||||
|
||||
getSubscriptionSetting.execute = jest.fn().mockReturnValue(Result.fail('error'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
user,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user