mirror of
https://github.com/standardnotes/server
synced 2026-01-21 08:04:27 -05:00
Compare commits
47 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2742075edc | ||
|
|
7f16232f8b | ||
|
|
0b0703e6d1 | ||
|
|
3e376c44e3 | ||
|
|
bfe2d4bb4a | ||
|
|
7253a0a1d9 | ||
|
|
f2c5810023 | ||
|
|
2e5b9105b8 | ||
|
|
d14411d72e | ||
|
|
5226513b26 | ||
|
|
334449f8aa | ||
|
|
7f43d0c69d | ||
|
|
6f18276e7a | ||
|
|
9ff18a18a5 | ||
|
|
999e72fb1f | ||
|
|
4733e663a3 | ||
|
|
b48eeb16c3 | ||
|
|
0aa2584e82 | ||
|
|
eb8c704d84 | ||
|
|
e93fa14703 | ||
|
|
16a6815b69 | ||
|
|
b08e9731b8 | ||
|
|
9bd4fb2d79 | ||
|
|
647aeda1de | ||
|
|
78ff748d91 | ||
|
|
31f8cf1169 | ||
|
|
14bcf7b6c9 | ||
|
|
74adddd1e7 | ||
|
|
0e43bc0042 | ||
|
|
b40d539611 | ||
|
|
654663d17f | ||
|
|
75830c3a98 | ||
|
|
1b5078eb96 | ||
|
|
a5e019e290 | ||
|
|
a812f3400a | ||
|
|
15af5635f0 | ||
|
|
cee6d62791 | ||
|
|
6aee51bd45 | ||
|
|
599a84e634 | ||
|
|
1c3d19cca4 | ||
|
|
9986e8e7ce | ||
|
|
e19f7a7b7f | ||
|
|
d570146378 | ||
|
|
8a9e4370e5 | ||
|
|
ce357679e9 | ||
|
|
acab402747 | ||
|
|
e385926046 |
4
.github/workflows/common-docker-image.yml
vendored
4
.github/workflows/common-docker-image.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
run: yarn build
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
|
||||
2
.github/workflows/common-self-hosting.yml
vendored
2
.github/workflows/common-self-hosting.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
6
.github/workflows/publish.yml
vendored
6
.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
|
||||
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
git config --global user.email "ci@standardnotes.com"
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v5
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
|
||||
13
.pnp.cjs
generated
13
.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"],\
|
||||
@@ -5567,7 +5566,7 @@ const RAW_RUNTIME_STATE =
|
||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
|
||||
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.17"],\
|
||||
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
|
||||
["ua-parser-js", "npm:1.0.35"],\
|
||||
["ua-parser-js", "npm:1.0.37"],\
|
||||
["uuid", "npm:9.0.0"],\
|
||||
["winston", "npm:3.9.0"]\
|
||||
],\
|
||||
@@ -5786,7 +5785,6 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
|
||||
["@types/cors", "npm:2.8.13"],\
|
||||
["@types/express", "npm:4.17.17"],\
|
||||
["@types/prettyjson", "npm:0.0.30"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
@@ -5799,7 +5797,6 @@ const RAW_RUNTIME_STATE =
|
||||
["inversify", "npm:6.0.1"],\
|
||||
["inversify-express-utils", "npm:6.4.3"],\
|
||||
["prettier", "npm:3.0.3"],\
|
||||
["prettyjson", "npm:1.2.5"],\
|
||||
["reflect-metadata", "npm:0.1.13"],\
|
||||
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
|
||||
["winston", "npm:3.9.0"]\
|
||||
@@ -6081,7 +6078,7 @@ const RAW_RUNTIME_STATE =
|
||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
|
||||
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.17"],\
|
||||
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
|
||||
["ua-parser-js", "npm:1.0.35"],\
|
||||
["ua-parser-js", "npm:1.0.37"],\
|
||||
["uuid", "npm:9.0.0"],\
|
||||
["winston", "npm:3.9.0"]\
|
||||
],\
|
||||
@@ -16150,10 +16147,10 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["ua-parser-js", [\
|
||||
["npm:1.0.35", {\
|
||||
"packageLocation": "./.yarn/cache/ua-parser-js-npm-1.0.35-38ecdb7612-b69c99c20f.zip/node_modules/ua-parser-js/",\
|
||||
["npm:1.0.37", {\
|
||||
"packageLocation": "./.yarn/cache/ua-parser-js-npm-1.0.37-b79655e1b5-56508f2428.zip/node_modules/ua-parser-js/",\
|
||||
"packageDependencies": [\
|
||||
["ua-parser-js", "npm:1.0.35"]\
|
||||
["ua-parser-js", "npm:1.0.37"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
|
||||
Binary file not shown.
BIN
.yarn/cache/ua-parser-js-npm-1.0.37-b79655e1b5-56508f2428.zip
vendored
Normal file
BIN
.yarn/cache/ua-parser-js-npm-1.0.37-b79655e1b5-56508f2428.zip
vendored
Normal file
Binary file not shown.
@@ -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,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.81.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.7...@standardnotes/api-gateway@1.81.8) (2023-11-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* retry attempts on session validation and more verbose logs ([#898](https://github.com/standardnotes/api-gateway/issues/898)) ([3e376c4](https://github.com/standardnotes/api-gateway/commit/3e376c44e3a6c336dcff3d8ef5eb3ab040d9a561))
|
||||
|
||||
## [1.81.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.6...@standardnotes/api-gateway@1.81.7) (2023-10-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add fallback methods for 404 requests ([#893](https://github.com/standardnotes/api-gateway/issues/893)) ([16a6815](https://github.com/standardnotes/api-gateway/commit/16a6815b69e344573ae07682f3bac1d44d715d79))
|
||||
|
||||
## [1.81.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.5...@standardnotes/api-gateway@1.81.6) (2023-10-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** logs for errors reaching service ([14bcf7b](https://github.com/standardnotes/api-gateway/commit/14bcf7b6c9403c3413e7579f58ea17168d14dce7))
|
||||
|
||||
## [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.8",
|
||||
"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
|
||||
|
||||
@@ -74,13 +74,16 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
|
||||
response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
let detailedErrorMessage = (error as Error).message
|
||||
if (error instanceof AxiosError) {
|
||||
detailedErrorMessage = `Status: ${error.status}, code: ${error.code}, message: ${error.message}`
|
||||
}
|
||||
|
||||
this.logger.error(`Could not pass the request to sessions/validate on underlying service: ${errorMessage}`)
|
||||
this.logger.error(
|
||||
`Could not pass the request to sessions/validate on underlying service: ${detailedErrorMessage}`,
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -91,7 +94,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
? +((error as AxiosError).code as string)
|
||||
: 500
|
||||
|
||||
response.status(errorCode).send(errorMessage)
|
||||
const responseErrorMessage = (error as AxiosError).response?.data
|
||||
|
||||
response
|
||||
.status(errorCode)
|
||||
.send(
|
||||
responseErrorMessage ??
|
||||
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { BaseHttpController, all, controller, results } from 'inversify-express-utils'
|
||||
|
||||
@controller('')
|
||||
export class FallbackController extends BaseHttpController {
|
||||
@all('*')
|
||||
public async fallback(): Promise<results.NotFoundResult> {
|
||||
return this.notFound()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './AuthMiddleware'
|
||||
export * from './FallbackController'
|
||||
export * from './HealthCheckController'
|
||||
export * from './SubscriptionTokenAuthMiddleware'
|
||||
export * from './TokenAuthenticationMethod'
|
||||
|
||||
@@ -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,49 @@ 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 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
|
||||
|
||||
return this.validateSession(headers, nextRetryAttempt)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +188,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 +231,47 @@ 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 errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
let detailedErrorMessage = (error as Error).message
|
||||
if (error instanceof AxiosError) {
|
||||
detailedErrorMessage = `Status: ${error.status}, code: ${error.code}, message: ${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: ${detailedErrorMessage}`,
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -232,7 +282,14 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
? +((error as AxiosError).code as string)
|
||||
: 500
|
||||
|
||||
response.status(errorCode).send(errorMessage)
|
||||
const responseErrorMessage = (error as AxiosError).response?.data
|
||||
|
||||
response
|
||||
.status(errorCode)
|
||||
.send(
|
||||
responseErrorMessage ??
|
||||
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -363,4 +420,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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,13 @@ export interface ServiceProxyInterface {
|
||||
endpointOrMethodIdentifier: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
|
||||
validateSession(
|
||||
headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
retryAttempt?: number,
|
||||
): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
|
||||
@@ -9,10 +9,13 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
private filesServerUrl: string,
|
||||
) {}
|
||||
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(
|
||||
headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
_retryAttempt?: number,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
|
||||
if (!authService) {
|
||||
throw new Error('Auth service not found')
|
||||
|
||||
@@ -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,74 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.165.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.1...@standardnotes/auth-server@1.165.2) (2023-11-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** change log severity on user authentication ([7f16232](https://github.com/standardnotes/server/commit/7f16232f8b13e3736801b6dc0af799e0559a3cfa))
|
||||
|
||||
## [1.165.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.0...@standardnotes/auth-server@1.165.1) (2023-11-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* retry attempts on session validation and more verbose logs ([#898](https://github.com/standardnotes/server/issues/898)) ([3e376c4](https://github.com/standardnotes/server/commit/3e376c44e3a6c336dcff3d8ef5eb3ab040d9a561))
|
||||
|
||||
# [1.165.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.164.2...@standardnotes/auth-server@1.165.0) (2023-11-02)
|
||||
|
||||
### Features
|
||||
|
||||
* add shared vault invitation email notifications ([#897](https://github.com/standardnotes/server/issues/897)) ([7253a0a](https://github.com/standardnotes/server/commit/7253a0a1d92099df844c9baf6541b440bbcb0a68))
|
||||
|
||||
## [1.164.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.164.1...@standardnotes/auth-server@1.164.2) (2023-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.164.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.164.0...@standardnotes/auth-server@1.164.1) (2023-11-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** creating valet tokens for shared subscription users ([#895](https://github.com/standardnotes/server/issues/895)) ([b48eeb1](https://github.com/standardnotes/server/commit/b48eeb16c32031e73e9757e34c4b50ca0a3a773d))
|
||||
|
||||
# [1.164.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.163.2...@standardnotes/auth-server@1.164.0) (2023-11-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add sending email to old email address when the address is changed ([#894](https://github.com/standardnotes/server/issues/894)) ([eb8c704](https://github.com/standardnotes/server/commit/eb8c704d84277130dc0dc51c1fe475a7220612cd))
|
||||
|
||||
## [1.163.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.163.1...@standardnotes/auth-server@1.163.2) (2023-10-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** checking permissions to update setting only when directly performed by user ([#892](https://github.com/standardnotes/server/issues/892)) ([9bd4fb2](https://github.com/standardnotes/server/commit/9bd4fb2d794dae032286c68f23d3896b68735bdd))
|
||||
|
||||
## [1.163.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.163.0...@standardnotes/auth-server@1.163.1) (2023-10-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add more information on the listed creation error ([78ff748](https://github.com/standardnotes/server/commit/78ff748d911a5a4063903847ef761822bbb8f4e2))
|
||||
|
||||
# [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.165.2",
|
||||
"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,29 @@ 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'
|
||||
import { AuthResponseFactoryResolverInterface } from '../Domain/Auth/AuthResponseFactoryResolverInterface'
|
||||
import { UserInvitedToSharedVaultEventHandler } from '../Domain/Handler/UserInvitedToSharedVaultEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
@@ -401,6 +406,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 +438,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 +472,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 +552,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 +590,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,16 +673,61 @@ 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)
|
||||
container.bind<AuthResponseFactoryResolver>(TYPES.Auth_AuthResponseFactoryResolver).to(AuthResponseFactoryResolver)
|
||||
container
|
||||
.bind<AuthResponseFactoryResolverInterface>(TYPES.Auth_AuthResponseFactoryResolver)
|
||||
.to(AuthResponseFactoryResolver)
|
||||
container.bind<KeyParamsFactory>(TYPES.Auth_KeyParamsFactory).to(KeyParamsFactory)
|
||||
container
|
||||
.bind<TokenDecoderInterface<SessionTokenData>>(TYPES.Auth_SessionTokenDecoder)
|
||||
@@ -698,12 +762,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 +787,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 +799,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 +825,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 +899,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 +971,148 @@ 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<ChangeCredentials>(TYPES.Auth_ChangeCredentials)
|
||||
.toConstantValue(
|
||||
new ChangeCredentials(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<AuthResponseFactoryResolverInterface>(TYPES.Auth_AuthResponseFactoryResolver),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
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 +1125,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 +1167,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 +1220,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 +1263,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 +1276,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 +1300,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 +1378,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)
|
||||
@@ -1180,9 +1450,17 @@ export class ContainerConfigLoader {
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<UserInvitedToSharedVaultEventHandler>(TYPES.Auth_UserInvitedToSharedVaultEventHandler)
|
||||
.toConstantValue(
|
||||
new UserInvitedToSharedVaultEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
|
||||
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 +1470,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)],
|
||||
@@ -1217,6 +1494,7 @@ export class ContainerConfigLoader {
|
||||
'USER_DESIGNATED_AS_SURVIVOR_IN_SHARED_VAULT',
|
||||
container.get(TYPES.Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler),
|
||||
],
|
||||
['USER_INVITED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserInvitedToSharedVaultEventHandler)],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
@@ -1343,33 +1621,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'),
|
||||
@@ -193,11 +195,10 @@ const TYPES = {
|
||||
Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler: Symbol.for(
|
||||
'Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler',
|
||||
),
|
||||
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
|
||||
// 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 +219,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 +229,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'),
|
||||
|
||||
9
packages/auth/src/Domain/Email/UserEmailChanged.ts
Normal file
9
packages/auth/src/Domain/Email/UserEmailChanged.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { html } from './user-email-changed.html'
|
||||
|
||||
export function getSubject(): string {
|
||||
return 'Confirmation: Your Email Address Has Been Successfully Updated'
|
||||
}
|
||||
|
||||
export function getBody(newEmail: string): string {
|
||||
return html(newEmail)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { html } from './user-invited-to-shared-vault.html'
|
||||
|
||||
export function getSubject(): string {
|
||||
return "You're Invited to a Shared Vault!"
|
||||
}
|
||||
|
||||
export function getBody(): string {
|
||||
return html()
|
||||
}
|
||||
14
packages/auth/src/Domain/Email/user-email-changed.html.ts
Normal file
14
packages/auth/src/Domain/Email/user-email-changed.html.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const html = (newEmail: string) => `
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>We are writing to inform you that your request to update your email address has been successfully processed. The email address associated with your Standard Notes account has now been changed to the following:</p>
|
||||
<p>New Email Address: ${newEmail}</p>
|
||||
|
||||
<p>From now on, you can log in to your account using your new email address. Your password and all other account details remain the same. If you did not initiate this change or have any concerns about this update, please contact our support team by visiting our <a href="https://standardnotes.com/help">help page</a>
|
||||
or by replying directly to this email.</p>
|
||||
|
||||
<p>Best regards,</p>
|
||||
<p>
|
||||
Standard Notes
|
||||
</p>
|
||||
`
|
||||
@@ -0,0 +1,21 @@
|
||||
export const html = () => `
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>You've been invited to join a shared vault. This shared workspace will help you collaborate and securely manage notes and files.</p>
|
||||
|
||||
<p>To accept this invitation and access the shared vault, please follow these steps:</p>
|
||||
|
||||
<ol>
|
||||
<li>Go to your account settings.</li>
|
||||
<li>Navigate to the "Vaults" section.</li>
|
||||
<li>You will find the invitation there — simply click to accept.</li>
|
||||
</ol>
|
||||
|
||||
<p>If you have any questions, please contact our support team by visiting our <a href="https://standardnotes.com/help">help page</a>
|
||||
or by replying directly to this email.</p>
|
||||
|
||||
<p>Best regards,</p>
|
||||
<p>
|
||||
Standard Notes
|
||||
</p>
|
||||
`
|
||||
@@ -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}: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
UserInvitedToSharedVaultEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { EmailLevel, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { getBody, getSubject } from '../Email/UserInvitedToSharedVault'
|
||||
|
||||
export class UserInvitedToSharedVaultEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: UserInvitedToSharedVaultEvent): Promise<void> {
|
||||
const userUuidOrError = Uuid.create(event.payload.invite.user_uuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createEmailRequestedEvent({
|
||||
body: getBody(),
|
||||
level: EmailLevel.LEVELS.System,
|
||||
subject: getSubject(),
|
||||
messageIdentifier: 'USER_INVITED_TO_SHARED_VAULT',
|
||||
userEmail: user.email,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user