Compare commits

...

19 Commits

Author SHA1 Message Date
standardci
75830c3a98 chore(release): publish new version
- @standardnotes/analytics@2.32.3
 - @standardnotes/api-gateway@1.81.3
 - @standardnotes/auth-server@1.162.0
 - @standardnotes/domain-core@1.39.0
 - @standardnotes/event-store@1.13.16
 - @standardnotes/files-server@1.32.1
 - @standardnotes/home-server@1.18.7
 - @standardnotes/revisions-server@1.47.2
 - @standardnotes/scheduler-server@1.26.3
 - @standardnotes/settings@1.21.47
 - @standardnotes/syncing-server@1.119.2
 - @standardnotes/websockets-server@1.17.3
2023-10-26 11:13:35 +00:00
Karol Sójko
1b5078eb96 feat: refactor settings (#890)
* feat: refactor settings

* fix verify mfa specs and data source metadata

* fix: compound index field names

* fix metadata binding for typeorm repository

* fix responses to preserve e2e

* fixes for e2e

* fix recovery codes e2e

* add missing specs
2023-10-26 12:42:30 +02:00
standardci
a5e019e290 chore(release): publish new version
- @standardnotes/auth-server@1.161.0
 - @standardnotes/home-server@1.18.6
2023-10-23 12:29:33 +00:00
Karol Sójko
a812f3400a feat(auth): remove axios http calls to payments server (#889)
* feat(auth): remove axios http calls to payments server

* fix: remove unused variable

* fix: remove another unused variable
2023-10-23 14:01:22 +02:00
Karol Sójko
15af5635f0 fix: allow to cancel previous subscription when activating premium features in e2e tests 2023-10-23 11:44:35 +02:00
standardci
cee6d62791 chore(release): publish new version
- @standardnotes/api-gateway@1.81.2
 - @standardnotes/home-server@1.18.5
2023-10-20 10:13:15 +00:00
Karol Sójko
6aee51bd45 fix(api-gateway): add session validation retry attempts on timedout requests 2023-10-20 11:46:29 +02:00
standardci
599a84e634 chore(release): publish new version
- @standardnotes/api-gateway@1.81.1
 - @standardnotes/home-server@1.18.4
2023-10-20 08:52:56 +00:00
Karol Sójko
1c3d19cca4 fix(api-gateway): logs severity on retry attempts 2023-10-20 10:21:10 +02:00
standardci
9986e8e7ce chore(release): publish new version
- @standardnotes/home-server@1.18.3
 - @standardnotes/revisions-server@1.47.1
 - @standardnotes/syncing-server@1.119.1
2023-10-20 08:12:46 +00:00
Karol Sójko
e19f7a7b7f fix: merge mysql and mysql-legacy together (#883) 2023-10-20 09:42:10 +02:00
Karol Sójko
d570146378 fix: publishing workflow 2023-10-20 09:40:54 +02:00
standardci
8a9e4370e5 chore(release): publish new version
- @standardnotes/api-gateway@1.81.0
 - @standardnotes/home-server@1.18.2
2023-10-20 07:40:07 +00:00
Karol Sójko
ce357679e9 feat(api-gateway): add retry attempts on timedout requests (#885) 2023-10-20 09:24:01 +02:00
standardci
acab402747 chore(release): publish new version
- @standardnotes/api-gateway@1.80.1
 - @standardnotes/home-server@1.18.1
2023-10-19 18:17:41 +00:00
Karol Sójko
e385926046 fix(api-gateway): stringify error in service proxy 2023-10-19 20:01:18 +02:00
standardci
e9b8d0ceb7 chore(release): publish new version
- @standardnotes/analytics@2.32.2
 - @standardnotes/api-gateway@1.80.0
 - @standardnotes/auth-server@1.160.0
 - @standardnotes/domain-core@1.38.0
 - @standardnotes/domain-events-infra@1.20.2
 - @standardnotes/domain-events@2.133.0
 - @standardnotes/event-store@1.13.15
 - @standardnotes/files-server@1.32.0
 - @standardnotes/home-server@1.18.0
 - @standardnotes/revisions-server@1.47.0
 - @standardnotes/scheduler-server@1.26.2
 - @standardnotes/security@1.16.0
 - @standardnotes/settings@1.21.46
 - @standardnotes/syncing-server@1.119.0
 - @standardnotes/websockets-server@1.17.2
2023-10-19 09:56:55 +00:00
Karol Sójko
a2c1ebe675 feat: remove transition state (#882)
* fix: Dockerfile build deps

* feat: remove transition state
2023-10-19 11:38:16 +02:00
Karol Sójko
3ef8e9ea24 fix: re-enable vault tests (#875) 2023-10-18 11:14:24 +02:00
400 changed files with 5159 additions and 13816 deletions

View File

@@ -103,10 +103,10 @@ jobs:
snjs_image_tag: 'latest'
suite: 'base'
# e2e-vaults:
# needs: build
# name: E2E Vaults Suite
# uses: standardnotes/server/.github/workflows/common-e2e.yml@main
# with:
# snjs_image_tag: 'latest'
# suite: 'vaults'
e2e-vaults:
needs: build
name: E2E Vaults Suite
uses: standardnotes/server/.github/workflows/common-e2e.yml@main
with:
snjs_image_tag: 'latest'
suite: 'vaults'

View File

@@ -103,22 +103,22 @@ jobs:
snjs_image_tag: 'latest'
suite: 'base'
# e2e-vaults:
# needs: build
# name: E2E Vaults Suite
# uses: standardnotes/server/.github/workflows/common-e2e.yml@main
# with:
# snjs_image_tag: 'latest'
# suite: 'vaults'
e2e-vaults:
needs: build
name: E2E Vaults Suite
uses: standardnotes/server/.github/workflows/common-e2e.yml@main
with:
snjs_image_tag: 'latest'
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

241
.pnp.cjs generated
View File

@@ -3065,16 +3065,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@mongodb-js/saslprep", [\
["npm:1.1.0", {\
"packageLocation": "./.yarn/cache/@mongodb-js-saslprep-npm-1.1.0-3906c025b8-1a631b92d2.zip/node_modules/@mongodb-js/saslprep/",\
"packageDependencies": [\
["@mongodb-js/saslprep", "npm:1.1.0"],\
["sparse-bitfield", "npm:3.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["@nodelib/fs.scandir", [\
["npm:2.1.5", {\
"packageLocation": "./.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-6ab2a9b8a1.zip/node_modules/@nodelib/fs.scandir/",\
@@ -5556,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"],\
@@ -5897,13 +5886,12 @@ const RAW_RUNTIME_STATE =
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.3.2"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
["mongodb", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0"],\
["mysql2", "npm:3.3.3"],\
["prettier", "npm:3.0.3"],\
["reflect-metadata", "npm:0.1.13"],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.17"],\
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.17"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["winston", "npm:3.9.0"]\
],\
@@ -6083,7 +6071,6 @@ const RAW_RUNTIME_STATE =
["ioredis", "npm:5.3.2"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
["jsonwebtoken", "npm:9.0.0"],\
["mongodb", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0"],\
["mysql2", "npm:3.3.3"],\
["prettier", "npm:3.0.3"],\
["prettyjson", "npm:1.2.5"],\
@@ -6091,7 +6078,7 @@ const RAW_RUNTIME_STATE =
["semver", "npm:7.5.4"],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.17"],\
["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"],\
["uuid", "npm:9.0.0"],\
@@ -6729,26 +6716,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@types/webidl-conversions", [\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/@types-webidl-conversions-npm-7.0.0-0903313151-60142c7ddd.zip/node_modules/@types/webidl-conversions/",\
"packageDependencies": [\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/whatwg-url", [\
["npm:8.2.2", {\
"packageLocation": "./.yarn/cache/@types-whatwg-url-npm-8.2.2-54c5c24e6c-5dc5afe078.zip/node_modules/@types/whatwg-url/",\
"packageDependencies": [\
["@types/whatwg-url", "npm:8.2.2"],\
["@types/node", "npm:20.2.5"],\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/yargs", [\
["npm:17.0.24", {\
"packageLocation": "./.yarn/cache/@types-yargs-npm-17.0.24-b034cf1d8b-03d9a985cb.zip/node_modules/@types/yargs/",\
@@ -7949,15 +7916,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["bson", [\
["npm:6.0.0", {\
"packageLocation": "./.yarn/cache/bson-npm-6.0.0-7b3cba060e-e7614bdc53.zip/node_modules/bson/",\
"packageDependencies": [\
["bson", "npm:6.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["buffer", [\
["npm:5.7.1", {\
"packageLocation": "./.yarn/cache/buffer-npm-5.7.1-513ef8259e-997434d3c6.zip/node_modules/buffer/",\
@@ -12561,15 +12519,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["memory-pager", [\
["npm:1.5.0", {\
"packageLocation": "./.yarn/cache/memory-pager-npm-1.5.0-46e20e6c81-ffe3461b6a.zip/node_modules/memory-pager/",\
"packageDependencies": [\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["meow", [\
["npm:8.1.2", {\
"packageLocation": "./.yarn/cache/meow-npm-8.1.2-bcfe48d4f3-d4770f9013.zip/node_modules/meow/",\
@@ -12914,66 +12863,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["mongodb", [\
["npm:6.0.0", {\
"packageLocation": "./.yarn/cache/mongodb-npm-6.0.0-7c1e74de91-501feaecb7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "npm:6.0.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0", {\
"packageLocation": "./.yarn/__virtual__/mongodb-virtual-789f2eaaac/0/cache/mongodb-npm-6.0.0-7c1e74de91-501feaecb7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0"],\
["@aws-sdk/credential-providers", null],\
["@mongodb-js/saslprep", "npm:1.1.0"],\
["@mongodb-js/zstd", null],\
["@types/aws-sdk__credential-providers", null],\
["@types/gcp-metadata", null],\
["@types/kerberos", null],\
["@types/mongodb-client-encryption", null],\
["@types/mongodb-js__zstd", null],\
["@types/snappy", null],\
["@types/socks", null],\
["bson", "npm:6.0.0"],\
["gcp-metadata", null],\
["kerberos", null],\
["mongodb-client-encryption", null],\
["mongodb-connection-string-url", "npm:2.6.0"],\
["snappy", null],\
["socks", null]\
],\
"packagePeers": [\
"@aws-sdk/credential-providers",\
"@mongodb-js/zstd",\
"@types/aws-sdk__credential-providers",\
"@types/gcp-metadata",\
"@types/kerberos",\
"@types/mongodb-client-encryption",\
"@types/mongodb-js__zstd",\
"@types/snappy",\
"@types/socks",\
"gcp-metadata",\
"kerberos",\
"mongodb-client-encryption",\
"snappy",\
"socks"\
],\
"linkType": "HARD"\
}]\
]],\
["mongodb-connection-string-url", [\
["npm:2.6.0", {\
"packageLocation": "./.yarn/cache/mongodb-connection-string-url-npm-2.6.0-af011ba17f-d0903b9824.zip/node_modules/mongodb-connection-string-url/",\
"packageDependencies": [\
["mongodb-connection-string-url", "npm:2.6.0"],\
["@types/whatwg-url", "npm:8.2.2"],\
["whatwg-url", "npm:11.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["ms", [\
["npm:2.0.0", {\
"packageLocation": "./.yarn/cache/ms-npm-2.0.0-9e1101a471-0e6a22b8b7.zip/node_modules/ms/",\
@@ -15055,16 +14944,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["sparse-bitfield", [\
["npm:3.0.3", {\
"packageLocation": "./.yarn/cache/sparse-bitfield-npm-3.0.3-cb80d0c89f-174da88dbb.zip/node_modules/sparse-bitfield/",\
"packageDependencies": [\
["sparse-bitfield", "npm:3.0.3"],\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["spdx-correct", [\
["npm:3.2.0", {\
"packageLocation": "./.yarn/cache/spdx-correct-npm-3.2.0-ffae008484-cc2e4dbef8.zip/node_modules/spdx-correct/",\
@@ -15714,14 +15593,6 @@ const RAW_RUNTIME_STATE =
["tr46", "npm:0.0.3"]\
],\
"linkType": "HARD"\
}],\
["npm:3.0.0", {\
"packageLocation": "./.yarn/cache/tr46-npm-3.0.0-e1ae1ea7c9-b09a15886c.zip/node_modules/tr46/",\
"packageDependencies": [\
["tr46", "npm:3.0.0"],\
["punycode", "npm:2.3.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["treeverse", [\
@@ -16175,98 +16046,6 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.17", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-bfb7ebf128/0/cache/typeorm-npm-0.3.17-f8c2578e7f-3a7fe2a5e9.zip/node_modules/typeorm/",\
"packageDependencies": [\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.17"],\
["@google-cloud/spanner", null],\
["@sap/hana-client", null],\
["@sqltools/formatter", "npm:1.2.5"],\
["@types/better-sqlite3", null],\
["@types/google-cloud__spanner", null],\
["@types/hdb-pool", null],\
["@types/ioredis", "npm:5.0.0"],\
["@types/mongodb", null],\
["@types/mssql", null],\
["@types/mysql2", null],\
["@types/oracledb", null],\
["@types/pg", null],\
["@types/pg-native", null],\
["@types/pg-query-stream", null],\
["@types/redis", null],\
["@types/sap__hana-client", null],\
["@types/sql.js", null],\
["@types/sqlite3", null],\
["@types/ts-node", null],\
["@types/typeorm-aurora-data-api-driver", null],\
["app-root-path", "npm:3.1.0"],\
["better-sqlite3", null],\
["buffer", "npm:6.0.3"],\
["chalk", "npm:4.1.2"],\
["cli-highlight", "npm:2.1.11"],\
["date-fns", "npm:2.30.0"],\
["debug", "virtual:ac3d8e680759ce54399273724d44e041d6c9b73454d191d411a8c44bb27e22f02aaf6ed9d3ad0ac1c298eac4833cff369c9c7b84c573016112c4f84be2cd8543#npm:4.3.4"],\
["dotenv", "npm:16.1.3"],\
["glob", "npm:8.1.0"],\
["hdb-pool", null],\
["ioredis", "npm:5.3.2"],\
["mkdirp", "npm:2.1.6"],\
["mongodb", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0"],\
["mssql", null],\
["mysql2", "npm:3.3.3"],\
["oracledb", null],\
["pg", null],\
["pg-native", null],\
["pg-query-stream", null],\
["redis", null],\
["reflect-metadata", "npm:0.1.13"],\
["sha.js", "npm:2.4.11"],\
["sql.js", null],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-node", null],\
["tslib", "npm:2.5.2"],\
["typeorm-aurora-data-api-driver", null],\
["uuid", "npm:9.0.0"],\
["yargs", "npm:17.7.2"]\
],\
"packagePeers": [\
"@google-cloud/spanner",\
"@sap/hana-client",\
"@types/better-sqlite3",\
"@types/google-cloud__spanner",\
"@types/hdb-pool",\
"@types/ioredis",\
"@types/mongodb",\
"@types/mssql",\
"@types/mysql2",\
"@types/oracledb",\
"@types/pg-native",\
"@types/pg-query-stream",\
"@types/pg",\
"@types/redis",\
"@types/sap__hana-client",\
"@types/sql.js",\
"@types/sqlite3",\
"@types/ts-node",\
"@types/typeorm-aurora-data-api-driver",\
"better-sqlite3",\
"hdb-pool",\
"ioredis",\
"mongodb",\
"mssql",\
"mysql2",\
"oracledb",\
"pg-native",\
"pg-query-stream",\
"pg",\
"redis",\
"sql.js",\
"sqlite3",\
"ts-node",\
"typeorm-aurora-data-api-driver"\
],\
"linkType": "HARD"\
}],\
["virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.17", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-bfa664706d/0/cache/typeorm-npm-0.3.17-f8c2578e7f-3a7fe2a5e9.zip/node_modules/typeorm/",\
"packageDependencies": [\
@@ -16659,13 +16438,6 @@ const RAW_RUNTIME_STATE =
["webidl-conversions", "npm:3.0.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/webidl-conversions-npm-7.0.0-e8c8e30c68-4c4f65472c.zip/node_modules/webidl-conversions/",\
"packageDependencies": [\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["webpack", [\
@@ -16724,15 +16496,6 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["whatwg-url", [\
["npm:11.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-11.0.0-073529d93a-dfcd51c6f4.zip/node_modules/whatwg-url/",\
"packageDependencies": [\
["whatwg-url", "npm:11.0.0"],\
["tr46", "npm:3.0.0"],\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}],\
["npm:5.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-f95adbc1e8.zip/node_modules/whatwg-url/",\
"packageDependencies": [\

View File

@@ -3,6 +3,8 @@ FROM node:20.6.1-alpine
ENV NODE_ENV production
RUN apk add --update --no-cache \
g++ \
make \
openssl \
curl \
bash \

View File

@@ -57,9 +57,6 @@ fi
if [ -z "$CACHE_TYPE" ]; then
export CACHE_TYPE="redis"
fi
if [ -z "$SECONDARY_DB_ENABLED" ]; then
export SECONDARY_DB_ENABLED=false
fi
export DB_MIGRATIONS_PATH="dist/migrations/*.js"
#########

View File

@@ -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.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
## [2.32.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.0...@standardnotes/analytics@2.32.1) (2023-10-18)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.32.1",
"version": "2.32.3",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
* remove transition state ([#882](https://github.com/standardnotes/api-gateway/issues/882)) ([a2c1ebe](https://github.com/standardnotes/api-gateway/commit/a2c1ebe675cd5678c923715056a6966f465a15d6))
## [1.79.14](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.79.13...@standardnotes/api-gateway@1.79.14) (2023-10-18)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.79.14",
"version": "1.81.3",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -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

View File

@@ -39,7 +39,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
}
if (this.crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken)) {
if (crossServiceToken === null) {
const authResponse = await this.serviceProxy.validateSession({
authorization: authHeaderValue,
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
@@ -129,14 +129,4 @@ export abstract class AuthMiddleware extends BaseMiddleware {
return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration)
}
private crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken: string | null) {
if (crossServiceToken === null) {
return true
}
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
return decodedToken.ongoing_transition === true
}
}

View File

@@ -34,16 +34,6 @@ export class ItemsController extends BaseHttpController {
)
}
@httpPost('/transition')
async transition(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callSyncingServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'items/transition'),
request.body,
)
}
@httpGet('/:uuid')
async getItem(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callSyncingServer(

View File

@@ -1,6 +1,6 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
import { BaseHttpController, controller, httpDelete, httpGet } from 'inversify-express-utils'
import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
@@ -55,14 +55,4 @@ export class RevisionsControllerV2 extends BaseHttpController {
),
)
}
@httpPost('/revisions/transition')
async transition(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callRevisionsServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'revisions/transition'),
request.body,
)
}
}

View File

@@ -7,6 +7,7 @@ import { Logger } from 'winston'
import { TYPES } from '../../Bootstrap/Types'
import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from './ServiceProxyInterface'
import { TimerInterface } from '@standardnotes/time'
@injectable()
export class HttpServiceProxy implements ServiceProxyInterface {
@@ -22,31 +23,50 @@ export class HttpServiceProxy implements ServiceProxyInterface {
@inject(TYPES.ApiGateway_HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
@inject(TYPES.ApiGateway_CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
@inject(TYPES.ApiGateway_Timer) private timer: TimerInterface,
) {}
async validateSession(headers: {
authorization: string
sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authResponse = await this.httpClient.request({
method: 'POST',
headers: {
Authorization: headers.authorization,
Accept: 'application/json',
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
validateStatus: (status: number) => {
return status >= 200 && status < 500
},
url: `${this.authServerUrl}/sessions/validate`,
})
async validateSession(
headers: {
authorization: string
sharedVaultOwnerContext?: string
},
retryAttempt?: number,
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
try {
const authResponse = await this.httpClient.request({
method: 'POST',
headers: {
Authorization: headers.authorization,
Accept: 'application/json',
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
validateStatus: (status: number) => {
return status >= 200 && status < 500
},
url: `${this.authServerUrl}/sessions/validate`,
})
return {
status: authResponse.status,
data: authResponse.data,
headers: {
contentType: authResponse.headers['content-type'] as string,
},
return {
status: authResponse.status,
data: authResponse.data,
headers: {
contentType: authResponse.headers['content-type'] as string,
},
}
} catch (error) {
const requestTimedOut =
'code' in (error as Record<string, unknown>) && (error as Record<string, unknown>).code === 'ETIMEDOUT'
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
if (!tooManyRetryAttempts && requestTimedOut) {
await this.timer.sleep(50)
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
return this.validateSession(headers, nextRetryAttempt)
}
throw error
}
}
@@ -169,6 +189,7 @@ export class HttpServiceProxy implements ServiceProxyInterface {
response: Response,
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
retryAttempt?: number,
): Promise<AxiosResponse | undefined> {
try {
const headers: Record<string, string> = {}
@@ -211,14 +232,44 @@ 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 requestTimedOut =
'code' in (error as Record<string, unknown>) && (error as Record<string, unknown>).code === 'ETIMEDOUT'
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
if (!tooManyRetryAttempts && requestTimedOut) {
await this.timer.sleep(50)
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
this.logger.debug(
`Retrying request to ${serverUrl}/${endpointOrMethodIdentifier} for the ${nextRetryAttempt} time`,
)
return this.getServerResponse(
serverUrl,
request,
response,
endpointOrMethodIdentifier,
payload,
nextRetryAttempt,
)
}
const errorMessage = (error as AxiosError).isAxiosError
? JSON.stringify((error as AxiosError).response?.data)
: (error as Error).message
this.logger.error(
`Could not pass the request to ${serverUrl}/${endpointOrMethodIdentifier} on underlying service: ${errorMessage}`,
`Could not pass the request to ${serverUrl}/${endpointOrMethodIdentifier} on underlying service: ${JSON.stringify(
error,
)}`,
)
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)

View File

@@ -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=

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.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
* remove transition state ([#882](https://github.com/standardnotes/server/issues/882)) ([a2c1ebe](https://github.com/standardnotes/server/commit/a2c1ebe675cd5678c923715056a6966f465a15d6))
## [1.159.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.159.1...@standardnotes/auth-server@1.159.2) (2023-10-18)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -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,
),

View File

@@ -1,170 +0,0 @@
import 'reflect-metadata'
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
import { ServiceIdentifier, RoleName, TransitionStatus } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
sdk.start()
import { Logger } from 'winston'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { TimerInterface } from '@standardnotes/time'
import { TransitionStatusRepositoryInterface } from '../src/Domain/Transition/TransitionStatusRepositoryInterface'
const inputArgs = process.argv.slice(2)
const startDateString = inputArgs[0]
const endDateString = inputArgs[1]
const forceRunParam = inputArgs[2]
const requestTransition = async (
transitionStatusRepository: TransitionStatusRepositoryInterface,
userRepository: UserRepositoryInterface,
logger: Logger,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
timer: TimerInterface,
): Promise<void> => {
const startDate = new Date(startDateString)
const endDate = new Date(endDateString)
const usersCount = await userRepository.countAllCreatedBetween(startDate, endDate)
const timestamp = timer.getTimestampInMicroseconds()
logger.info(
`[TRANSITION ${timestamp}] Found ${usersCount} users created between ${startDateString} and ${endDateString}`,
)
let itemTransitionsTriggered = 0
let revisionTransitionsTriggered = 0
const forceRun = forceRunParam === 'true'
const pageLimit = 100
const totalPages = Math.ceil(usersCount / pageLimit)
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
const users = await userRepository.findAllCreatedBetween({
start: startDate,
end: endDate,
offset: (currentPage - 1) * pageLimit,
limit: pageLimit,
})
for (const user of users) {
const itemsTransitionStatus = await transitionStatusRepository.getStatus(user.uuid, 'items')
const revisionsTransitionStatus = await transitionStatusRepository.getStatus(user.uuid, 'revisions')
const userRoles = await user.roles
const userHasTransitionRole = userRoles.some((role) => role.name === RoleName.NAMES.TransitionUser)
const bothTransitionStatusesAreVerified =
itemsTransitionStatus?.value === TransitionStatus.STATUSES.Verified &&
revisionsTransitionStatus?.value === TransitionStatus.STATUSES.Verified
if (!userHasTransitionRole && bothTransitionStatusesAreVerified) {
continue
}
logger.info(
`[TRANSITION ${timestamp}] Transition status for user ${user.uuid} - items status: ${itemsTransitionStatus?.value}, revisions status: ${revisionsTransitionStatus?.value}, has transition role: ${userHasTransitionRole}`,
)
if (
itemsTransitionStatus === null ||
itemsTransitionStatus.value === TransitionStatus.STATUSES.Failed ||
(itemsTransitionStatus.value === TransitionStatus.STATUSES.InProgress && forceRun)
) {
await transitionStatusRepository.remove(user.uuid, 'items')
await domainEventPublisher.publish(
domainEventFactory.createTransitionRequestedEvent({
userUuid: user.uuid,
type: 'items',
timestamp,
}),
)
itemTransitionsTriggered++
}
if (
revisionsTransitionStatus === null ||
revisionsTransitionStatus.value === TransitionStatus.STATUSES.Failed ||
(revisionsTransitionStatus.value === TransitionStatus.STATUSES.InProgress && forceRun)
) {
await transitionStatusRepository.remove(user.uuid, 'revisions')
await domainEventPublisher.publish(
domainEventFactory.createTransitionRequestedEvent({
userUuid: user.uuid,
type: 'revisions',
timestamp,
}),
)
revisionTransitionsTriggered++
}
}
}
logger.info(
`[TRANSITION ${timestamp}] Triggered ${itemTransitionsTriggered} item transitions and ${revisionTransitionsTriggered} revision transitions for users created between ${startDateString} and ${endDateString}`,
)
}
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
dayjs.extend(utc)
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Auth_Logger)
logger.info(`Starting transition request for users created between ${startDateString} and ${endDateString}`)
const userRepository: UserRepositoryInterface = container.get(TYPES.Auth_UserRepository)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.Auth_DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher)
const timer = container.get<TimerInterface>(TYPES.Auth_Timer)
const transitionStatusRepository = container.get<TransitionStatusRepositoryInterface>(
TYPES.Auth_TransitionStatusRepository,
)
const tracer = new OpenTelemetryTracer()
tracer.startSpan(ServiceIdentifier.NAMES.AuthScheduledTask, 'transition')
Promise.resolve(
requestTransition(
transitionStatusRepository,
userRepository,
logger,
domainEventFactory,
domainEventPublisher,
timer,
),
)
.then(() => {
logger.info(`Finished transition request for users created between ${startDateString} and ${endDateString}`)
tracer.stopSpan()
process.exit(0)
})
.catch((error) => {
logger.error(
`Error while requesting transition for users created between ${startDateString} and ${endDateString}: ${error}`,
)
tracer.stopSpanWithError(error)
process.exit(1)
})
})

View File

@@ -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,
),

View File

@@ -1,11 +0,0 @@
'use strict'
const path = require('path')
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/transition.js')))
Object.defineProperty(exports, '__esModule', { value: true })
exports.default = index

View File

@@ -55,13 +55,6 @@ case "$COMMAND" in
node docker/entrypoint-backup.js one_drive daily
;;
'transition' )
START_DATE=$1 && shift 1
END_DATE=$1 && shift 1
echo "[Docker] Starting Transition..."
node docker/entrypoint-transition.js $START_DATE $END_DATE
;;
* )
echo "[Docker] Unknown command"
;;

View File

@@ -5,6 +5,7 @@ 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 { Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
@@ -32,19 +33,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()

View File

@@ -1,8 +1,8 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class UpdateUnknownContent1690975361562 implements MigrationInterface {
export class RemoveTransitionRole1697704066569 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.manager.query('UPDATE items SET content_type = "Note" WHERE content_type = "Unknown"')
await queryRunner.query('DELETE FROM `roles` WHERE name = "TRANSITION_USER"')
}
public async down(): Promise<void> {

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.159.2",
"version": "1.162.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -54,7 +54,6 @@
"@standardnotes/sncrypto-common": "^1.13.4",
"@standardnotes/sncrypto-node": "workspace:*",
"@standardnotes/time": "workspace:*",
"axios": "^1.1.3",
"bcryptjs": "2.4.3",
"cors": "2.8.5",
"dayjs": "^1.11.6",

View File

@@ -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>>
}

View File

@@ -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'
@@ -258,11 +240,6 @@ import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQu
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
import { TransitionStatusRepositoryInterface } from '../Domain/Transition/TransitionStatusRepositoryInterface'
import { RedisTransitionStatusRepository } from '../Infra/Redis/RedisTransitionStatusRepository'
import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTransitionStatusRepository'
import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
import { SharedVaultUserPersistenceMapper } from '../Mapping/SharedVaultUserPersistenceMapper'
import { SharedVaultUserRepositoryInterface } from '../Domain/SharedVault/SharedVaultUserRepositoryInterface'
@@ -276,6 +253,27 @@ import { UserDesignatedAsSurvivorInSharedVaultEventHandler } from '../Domain/Han
import { DisableEmailSettingBasedOnEmailSubscription } from '../Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription'
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
import { KeyParamsFactoryInterface } from '../Domain/User/KeyParamsFactoryInterface'
import { TypeORMSubscriptionSetting } from '../Infra/TypeORM/TypeORMSubscriptionSetting'
import { SetSettingValue } from '../Domain/UseCase/SetSettingValue/SetSettingValue'
import { ApplyDefaultSubscriptionSettings } from '../Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting'
import { SetSubscriptionSettingValue } from '../Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue'
import { GetSettings } from '../Domain/UseCase/GetSettings/GetSettings'
import { GetSubscriptionSettings } from '../Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings'
import { GetAllSettingsForUser } from '../Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser'
import { GetRegularSubscriptionForUser } from '../Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
import { GetSharedSubscriptionForUser } from '../Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
import { GetSharedOrRegularSubscriptionForUser } from '../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser'
import { ProjectorInterface } from '../Projection/ProjectorInterface'
import { SettingHttpRepresentation } from '../Mapping/Http/SettingHttpRepresentation'
import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
import { SubscriptionSettingHttpRepresentation } from '../Mapping/Http/SubscriptionSettingHttpRepresentation'
import { SettingHttpMapper } from '../Mapping/Http/SettingHttpMapper'
import { SubscriptionSettingHttpMapper } from '../Mapping/Http/SubscriptionSettingHttpMapper'
import { TypeORMSetting } from '../Infra/TypeORM/TypeORMSetting'
import { SettingPersistenceMapper } from '../Mapping/Persistence/SettingPersistenceMapper'
import { SubscriptionSettingPersistenceMapper } from '../Mapping/Persistence/SubscriptionSettingPersistenceMapper'
import { ApplyDefaultSettings } from '../Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -406,6 +404,22 @@ export class ContainerConfigLoader {
container
.bind<MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>>(TYPES.Auth_SharedVaultUserPersistenceMapper)
.toConstantValue(new SharedVaultUserPersistenceMapper())
container
.bind<MapperInterface<Setting, SettingHttpRepresentation>>(TYPES.Auth_SettingHttpMapper)
.toConstantValue(new SettingHttpMapper())
container
.bind<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
TYPES.Auth_SubscriptionSettingHttpMapper,
)
.toConstantValue(new SubscriptionSettingHttpMapper())
container
.bind<MapperInterface<Setting, TypeORMSetting>>(TYPES.Auth_SettingPersistenceMapper)
.toConstantValue(new SettingPersistenceMapper())
container
.bind<MapperInterface<SubscriptionSetting, TypeORMSubscriptionSetting>>(
TYPES.Auth_SubscriptionSettingPersistenceMapper,
)
.toConstantValue(new SubscriptionSettingPersistenceMapper())
// ORM
container
@@ -422,14 +436,14 @@ export class ContainerConfigLoader {
.bind<Repository<Session>>(TYPES.Auth_ORMSessionRepository)
.toConstantValue(appDataSource.getRepository(Session))
container
.bind<Repository<Setting>>(TYPES.Auth_ORMSettingRepository)
.toConstantValue(appDataSource.getRepository(Setting))
.bind<Repository<TypeORMSetting>>(TYPES.Auth_ORMSettingRepository)
.toConstantValue(appDataSource.getRepository(TypeORMSetting))
container
.bind<Repository<SharedSubscriptionInvitation>>(TYPES.Auth_ORMSharedSubscriptionInvitationRepository)
.toConstantValue(appDataSource.getRepository(SharedSubscriptionInvitation))
container
.bind<Repository<SubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository)
.toConstantValue(appDataSource.getRepository(SubscriptionSetting))
.bind<Repository<TypeORMSubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository)
.toConstantValue(appDataSource.getRepository(TypeORMSubscriptionSetting))
container.bind<Repository<User>>(TYPES.Auth_ORMUserRepository).toConstantValue(appDataSource.getRepository(User))
container
.bind<Repository<UserSubscription>>(TYPES.Auth_ORMUserSubscriptionRepository)
@@ -456,10 +470,24 @@ export class ContainerConfigLoader {
.bind<RevokedSessionRepositoryInterface>(TYPES.Auth_RevokedSessionRepository)
.to(TypeORMRevokedSessionRepository)
container.bind<UserRepositoryInterface>(TYPES.Auth_UserRepository).to(TypeORMUserRepository)
container.bind<SettingRepositoryInterface>(TYPES.Auth_SettingRepository).to(TypeORMSettingRepository)
container
.bind<SettingRepositoryInterface>(TYPES.Auth_SettingRepository)
.toConstantValue(
new TypeORMSettingRepository(
container.get<Repository<TypeORMSetting>>(TYPES.Auth_ORMSettingRepository),
container.get<MapperInterface<Setting, TypeORMSetting>>(TYPES.Auth_SettingPersistenceMapper),
),
)
container
.bind<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository)
.to(TypeORMSubscriptionSettingRepository)
.toConstantValue(
new TypeORMSubscriptionSettingRepository(
container.get<Repository<TypeORMSubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository),
container.get<MapperInterface<SubscriptionSetting, TypeORMSubscriptionSetting>>(
TYPES.Auth_SubscriptionSettingPersistenceMapper,
),
),
)
container
.bind<OfflineSettingRepositoryInterface>(TYPES.Auth_OfflineSettingRepository)
.to(TypeORMOfflineSettingRepository)
@@ -522,13 +550,6 @@ export class ContainerConfigLoader {
container.bind<UserProjector>(TYPES.Auth_UserProjector).to(UserProjector)
container.bind<RoleProjector>(TYPES.Auth_RoleProjector).to(RoleProjector)
container.bind<PermissionProjector>(TYPES.Auth_PermissionProjector).to(PermissionProjector)
container.bind<SettingProjector>(TYPES.Auth_SettingProjector).to(SettingProjector)
container
.bind<SubscriptionSettingProjector>(TYPES.Auth_SubscriptionSettingProjector)
.to(SubscriptionSettingProjector)
// Factories
container.bind<SettingFactoryInterface>(TYPES.Auth_SettingFactory).to(SettingFactory)
// env vars
container.bind(TYPES.Auth_JWT_SECRET).toConstantValue(env.get('JWT_SECRET'))
@@ -567,16 +588,7 @@ export class ContainerConfigLoader {
.toConstantValue(env.get('DISABLE_USER_REGISTRATION', true) === 'true')
container.bind(TYPES.Auth_SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
container.bind(TYPES.Auth_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
container
.bind(TYPES.Auth_USER_SERVER_REGISTRATION_URL)
.toConstantValue(env.get('USER_SERVER_REGISTRATION_URL', true))
container.bind(TYPES.Auth_USER_SERVER_AUTH_KEY).toConstantValue(env.get('USER_SERVER_AUTH_KEY', true))
container
.bind(TYPES.Auth_USER_SERVER_CHANGE_EMAIL_URL)
.toConstantValue(env.get('USER_SERVER_CHANGE_EMAIL_URL', true))
container.bind(TYPES.Auth_SYNCING_SERVER_URL).toConstantValue(env.get('SYNCING_SERVER_URL', true))
container.bind(TYPES.Auth_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
container.bind(TYPES.Auth_PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
container
.bind(TYPES.Auth_SESSION_TRACE_DAYS_TTL)
.toConstantValue(env.get('SESSION_TRACE_DAYS_TTL', true) ? +env.get('SESSION_TRACE_DAYS_TTL', true) : 90)
@@ -645,9 +657,6 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Timer),
),
)
container
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
.toConstantValue(new InMemoryTransitionStatusRepository())
} else {
container.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository).to(RedisPKCERepository)
container.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository).to(LockRepository)
@@ -660,17 +669,57 @@ export class ContainerConfigLoader {
container
.bind<SubscriptionTokenRepositoryInterface>(TYPES.Auth_SubscriptionTokenRepository)
.to(RedisSubscriptionTokenRepository)
container
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
.toConstantValue(new RedisTransitionStatusRepository(container.get<Redis>(TYPES.Auth_Redis)))
}
// 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)
@@ -709,12 +758,9 @@ export class ContainerConfigLoader {
.bind<AuthenticationMethodResolver>(TYPES.Auth_AuthenticationMethodResolver)
.to(AuthenticationMethodResolver)
container.bind<DomainEventFactory>(TYPES.Auth_DomainEventFactory).to(DomainEventFactory)
container.bind<AxiosInstance>(TYPES.Auth_HTTPClient).toConstantValue(axios.create())
container.bind<CrypterInterface>(TYPES.Auth_Crypter).to(CrypterNode)
container
.bind<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService)
.to(SettingsAssociationService)
container.bind<SettingDecrypterInterface>(TYPES.Auth_SettingDecrypter).to(SettingDecrypter)
container
.bind<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams)
@@ -737,21 +783,6 @@ export class ContainerConfigLoader {
),
)
container
.bind<SettingServiceInterface>(TYPES.Auth_SettingService)
.toConstantValue(
new SettingService(
container.get<SettingFactoryInterface>(TYPES.Auth_SettingFactory),
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
container.get<SettingInterpreterInterface>(TYPES.Auth_SettingInterpreter),
container.get<SettingDecrypterInterface>(TYPES.Auth_SettingDecrypter),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<SubscriptionSettingServiceInterface>(TYPES.Auth_SubscriptionSettingService)
.to(SubscriptionSettingService)
container.bind<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService).to(OfflineSettingService)
container.bind<ContentDecoderInterface>(TYPES.Auth_ContenDecoder).toConstantValue(new ContentDecoder())
container.bind<ClientServiceInterface>(TYPES.Auth_WebSocketsClientService).to(WebSocketsClientService)
@@ -764,7 +795,6 @@ export class ContainerConfigLoader {
container
.bind<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector)
.toConstantValue(new DeterministicSelector<boolean>())
container.bind<UserSubscriptionServiceInterface>(TYPES.Auth_UserSubscriptionService).to(UserSubscriptionService)
// Middleware
container.bind<SessionMiddleware>(TYPES.Auth_SessionMiddleware).to(SessionMiddleware)
@@ -791,15 +821,6 @@ export class ContainerConfigLoader {
container.bind<OfflineUserAuthMiddleware>(TYPES.Auth_OfflineUserAuthMiddleware).to(OfflineUserAuthMiddleware)
// use cases
container
.bind<TraceSession>(TYPES.Auth_TraceSession)
.toConstantValue(
new TraceSession(
container.get(TYPES.Auth_SessionTraceRepository),
container.get(TYPES.Auth_Timer),
container.get(TYPES.Auth_SESSION_TRACE_DAYS_TTL),
),
)
container
.bind<PersistStatistics>(TYPES.Auth_PersistStatistics)
.toConstantValue(
@@ -874,24 +895,65 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_FeatureService),
),
)
container
.bind<SetSettingValue>(TYPES.Auth_SetSettingValue)
.toConstantValue(
new SetSettingValue(
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
),
)
container
.bind<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes)
.toConstantValue(
new GenerateRecoveryCodes(
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_SettingService),
container.get(TYPES.Auth_SetSettingValue),
container.get(TYPES.Auth_CryptoNode),
),
)
container
.bind<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting)
.toConstantValue(
new GetSubscriptionSetting(
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
),
)
container
.bind<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue)
.toConstantValue(
new SetSubscriptionSettingValue(
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
container.get<TimerInterface>(TYPES.Auth_Timer),
),
)
container
.bind<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings)
.toConstantValue(
new ApplyDefaultSubscriptionSettings(
container.get<SubscriptionSettingsAssociationServiceInterface>(
TYPES.Auth_SubscriptionSettingsAssociationService,
),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
),
)
container
.bind<ActivatePremiumFeatures>(TYPES.Auth_ActivatePremiumFeatures)
.toConstantValue(
new ActivatePremiumFeatures(
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_UserSubscriptionRepository),
container.get(TYPES.Auth_SubscriptionSettingService),
container.get(TYPES.Auth_RoleService),
container.get(TYPES.Auth_Timer),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<TimerInterface>(TYPES.Auth_Timer),
),
)
@@ -905,47 +967,136 @@ export class ContainerConfigLoader {
container.bind<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest)
container.bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken).to(RefreshSessionToken)
container.bind<SignIn>(TYPES.Auth_SignIn).to(SignIn)
container.bind<VerifyMFA>(TYPES.Auth_VerifyMFA).to(VerifyMFA)
container
.bind<VerifyMFA>(TYPES.Auth_VerifyMFA)
.toConstantValue(
new VerifyMFA(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector),
container.get<LockRepositoryInterface>(TYPES.Auth_LockRepository),
container.get<string>(TYPES.Auth_PSEUDO_KEY_PARAMS_KEY),
container.get<AuthenticatorRepositoryInterface>(TYPES.Auth_AuthenticatorRepository),
container.get<VerifyAuthenticatorAuthenticationResponse>(
TYPES.Auth_VerifyAuthenticatorAuthenticationResponse,
),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container.bind<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts).to(ClearLoginAttempts)
container.bind<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts).to(IncreaseLoginAttempts)
container
.bind<GetUserKeyParamsRecovery>(TYPES.Auth_GetUserKeyParamsRecovery)
.toConstantValue(
new GetUserKeyParamsRecovery(
container.get(TYPES.Auth_KeyParamsFactory),
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_PKCERepository),
container.get(TYPES.Auth_SettingService),
container.get<KeyParamsFactoryInterface>(TYPES.Auth_KeyParamsFactory),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<PKCERepositoryInterface>(TYPES.Auth_PKCERepository),
container.get<GetSetting>(TYPES.Auth_GetSetting),
),
)
container.bind<UpdateUser>(TYPES.Auth_UpdateUser).to(UpdateUser)
container.bind<Register>(TYPES.Auth_Register).to(Register)
container
.bind<ApplyDefaultSettings>(TYPES.Auth_ApplyDefaultSettings)
.toConstantValue(
new ApplyDefaultSettings(
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
),
)
container
.bind<Register>(TYPES.Auth_Register)
.toConstantValue(
new Register(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<RoleRepositoryInterface>(TYPES.Auth_RoleRepository),
container.get<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115),
container.get<CrypterInterface>(TYPES.Auth_Crypter),
container.get<boolean>(TYPES.Auth_DISABLE_USER_REGISTRATION),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<ApplyDefaultSettings>(TYPES.Auth_ApplyDefaultSettings),
),
)
container.bind<GetActiveSessionsForUser>(TYPES.Auth_GetActiveSessionsForUser).to(GetActiveSessionsForUser)
container.bind<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser).to(DeleteOtherSessionsForUser)
container.bind<DeleteSessionForUser>(TYPES.Auth_DeleteSessionForUser).to(DeleteSessionForUser)
container.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials).to(ChangeCredentials)
container.bind<GetSettings>(TYPES.Auth_GetSettings).to(GetSettings)
container.bind<GetSetting>(TYPES.Auth_GetSetting).to(GetSetting)
container
.bind<GetSettings>(TYPES.Auth_GetSettings)
.toConstantValue(
new GetSettings(
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
),
)
container
.bind<GetSubscriptionSettings>(TYPES.Auth_GetSubscriptionSettings)
.toConstantValue(
new GetSubscriptionSettings(
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
),
)
container
.bind<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser)
.toConstantValue(
new GetRegularSubscriptionForUser(
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
),
)
container
.bind<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser)
.toConstantValue(
new GetSharedSubscriptionForUser(
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
),
)
container
.bind<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser)
.toConstantValue(
new GetSharedOrRegularSubscriptionForUser(
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
),
)
container
.bind<GetAllSettingsForUser>(TYPES.Auth_GetAllSettingsForUser)
.toConstantValue(
new GetAllSettingsForUser(
container.get<GetSettings>(TYPES.Auth_GetSettings),
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
container.get<GetSubscriptionSettings>(TYPES.Auth_GetSubscriptionSettings),
),
)
container.bind<GetUserFeatures>(TYPES.Auth_GetUserFeatures).to(GetUserFeatures)
container.bind<UpdateSetting>(TYPES.Auth_UpdateSetting).to(UpdateSetting)
container.bind<DeleteSetting>(TYPES.Auth_DeleteSetting).to(DeleteSetting)
container
.bind<SignInWithRecoveryCodes>(TYPES.Auth_SignInWithRecoveryCodes)
.toConstantValue(
new SignInWithRecoveryCodes(
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_AuthResponseFactory20200115),
container.get(TYPES.Auth_PKCERepository),
container.get(TYPES.Auth_Crypter),
container.get(TYPES.Auth_SettingService),
container.get(TYPES.Auth_GenerateRecoveryCodes),
container.get(TYPES.Auth_IncreaseLoginAttempts),
container.get(TYPES.Auth_ClearLoginAttempts),
container.get(TYPES.Auth_DeleteSetting),
container.get(TYPES.Auth_AuthenticatorRepository),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115),
container.get<PKCERepositoryInterface>(TYPES.Auth_PKCERepository),
container.get<CrypterInterface>(TYPES.Auth_Crypter),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes),
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
container.get<AuthenticatorRepositoryInterface>(TYPES.Auth_AuthenticatorRepository),
),
)
container
.bind<DeleteAccount>(TYPES.Auth_DeleteAccount)
.toConstantValue(
new DeleteAccount(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<TimerInterface>(TYPES.Auth_Timer),
),
)
container.bind<DeleteAccount>(TYPES.Auth_DeleteAccount).to(DeleteAccount)
container.bind<GetUserSubscription>(TYPES.Auth_GetUserSubscription).to(GetUserSubscription)
container.bind<GetUserOfflineSubscription>(TYPES.Auth_GetUserOfflineSubscription).to(GetUserOfflineSubscription)
container.bind<CreateSubscriptionToken>(TYPES.Auth_CreateSubscriptionToken).to(CreateSubscriptionToken)
@@ -958,12 +1109,38 @@ export class ContainerConfigLoader {
container
.bind<CreateOfflineSubscriptionToken>(TYPES.Auth_CreateOfflineSubscriptionToken)
.to(CreateOfflineSubscriptionToken)
container.bind<CreateValetToken>(TYPES.Auth_CreateValetToken).to(CreateValetToken)
container
.bind<CreateValetToken>(TYPES.Auth_CreateValetToken)
.toConstantValue(
new CreateValetToken(
container.get<TokenEncoderInterface<ValetTokenData>>(TYPES.Auth_ValetTokenEncoder),
container.get<SubscriptionSettingsAssociationServiceInterface>(
TYPES.Auth_SubscriptionSettingsAssociationService,
),
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<number>(TYPES.Auth_VALET_TOKEN_TTL),
),
)
container.bind<CreateListedAccount>(TYPES.Auth_CreateListedAccount).to(CreateListedAccount)
container.bind<InviteToSharedSubscription>(TYPES.Auth_InviteToSharedSubscription).to(InviteToSharedSubscription)
container
.bind<AcceptSharedSubscriptionInvitation>(TYPES.Auth_AcceptSharedSubscriptionInvitation)
.to(AcceptSharedSubscriptionInvitation)
.toConstantValue(
new AcceptSharedSubscriptionInvitation(
container.get<SharedSubscriptionInvitationRepositoryInterface>(
TYPES.Auth_SharedSubscriptionInvitationRepository,
),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<DeclineSharedSubscriptionInvitation>(TYPES.Auth_DeclineSharedSubscriptionInvitation)
.to(DeclineSharedSubscriptionInvitation)
@@ -974,23 +1151,31 @@ 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
.bind<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus)
.toConstantValue(
new UpdateTransitionStatus(
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
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),
),
)
@@ -1019,8 +1204,9 @@ export class ContainerConfigLoader {
.toConstantValue(
new DisableEmailSettingBasedOnEmailSubscription(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<SettingFactoryInterface>(TYPES.Auth_SettingFactory),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
),
)
@@ -1061,7 +1247,6 @@ export class ContainerConfigLoader {
container.bind<UserRequestsController>(TYPES.Auth_UserRequestsController).to(UserRequestsController)
// Handlers
container.bind<UserRegisteredEventHandler>(TYPES.Auth_UserRegisteredEventHandler).to(UserRegisteredEventHandler)
container
.bind<AccountDeletionRequestedEventHandler>(TYPES.Auth_AccountDeletionRequestedEventHandler)
.toConstantValue(
@@ -1075,7 +1260,16 @@ export class ContainerConfigLoader {
)
container
.bind<SubscriptionPurchasedEventHandler>(TYPES.Auth_SubscriptionPurchasedEventHandler)
.to(SubscriptionPurchasedEventHandler)
.toConstantValue(
new SubscriptionPurchasedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<SubscriptionCancelledEventHandler>(TYPES.Auth_SubscriptionCancelledEventHandler)
.to(SubscriptionCancelledEventHandler)
@@ -1090,16 +1284,42 @@ export class ContainerConfigLoader {
.to(SubscriptionExpiredEventHandler)
container
.bind<SubscriptionSyncRequestedEventHandler>(TYPES.Auth_SubscriptionSyncRequestedEventHandler)
.to(SubscriptionSyncRequestedEventHandler)
.toConstantValue(
new SubscriptionSyncRequestedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<ExtensionKeyGrantedEventHandler>(TYPES.Auth_ExtensionKeyGrantedEventHandler)
.to(ExtensionKeyGrantedEventHandler)
.toConstantValue(
new ExtensionKeyGrantedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<SubscriptionReassignedEventHandler>(TYPES.Auth_SubscriptionReassignedEventHandler)
.to(SubscriptionReassignedEventHandler)
container
.bind<UserEmailChangedEventHandler>(TYPES.Auth_UserEmailChangedEventHandler)
.to(UserEmailChangedEventHandler)
.toConstantValue(
new SubscriptionReassignedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<winston.Logger>(TYPES.Auth_Logger),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
),
)
container
.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler)
.toConstantValue(
@@ -1142,10 +1362,24 @@ export class ContainerConfigLoader {
)
container
.bind<ListedAccountCreatedEventHandler>(TYPES.Auth_ListedAccountCreatedEventHandler)
.to(ListedAccountCreatedEventHandler)
.toConstantValue(
new ListedAccountCreatedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<ListedAccountDeletedEventHandler>(TYPES.Auth_ListedAccountDeletedEventHandler)
.to(ListedAccountDeletedEventHandler)
.toConstantValue(
new ListedAccountDeletedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<UserDisabledSessionUserAgentLoggingEventHandler>(TYPES.Auth_UserDisabledSessionUserAgentLoggingEventHandler)
.to(UserDisabledSessionUserAgentLoggingEventHandler)
@@ -1174,14 +1408,6 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Logger),
),
)
container
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Auth_TransitionStatusUpdatedEventHandler)
.toConstantValue(
new TransitionStatusUpdatedEventHandler(
container.get<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<UserAddedToSharedVaultEventHandler>(TYPES.Auth_UserAddedToSharedVaultEventHandler)
.toConstantValue(
@@ -1210,7 +1436,6 @@ export class ContainerConfigLoader {
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
['SUBSCRIPTION_PURCHASED', container.get(TYPES.Auth_SubscriptionPurchasedEventHandler)],
['SUBSCRIPTION_CANCELLED', container.get(TYPES.Auth_SubscriptionCancelledEventHandler)],
@@ -1220,7 +1445,6 @@ export class ContainerConfigLoader {
['SUBSCRIPTION_SYNC_REQUESTED', container.get(TYPES.Auth_SubscriptionSyncRequestedEventHandler)],
['EXTENSION_KEY_GRANTED', container.get(TYPES.Auth_ExtensionKeyGrantedEventHandler)],
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.Auth_SubscriptionReassignedEventHandler)],
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)],
@@ -1239,7 +1463,6 @@ export class ContainerConfigLoader {
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.Auth_PredicateVerificationRequestedEventHandler)],
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)],
['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)],
['TRANSITION_STATUS_UPDATED', container.get(TYPES.Auth_TransitionStatusUpdatedEventHandler)],
['USER_ADDED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserAddedToSharedVaultEventHandler)],
['USER_REMOVED_FROM_SHARED_VAULT', container.get(TYPES.Auth_UserRemovedFromSharedVaultEventHandler)],
[
@@ -1372,33 +1595,41 @@ export class ContainerConfigLoader {
.bind<BaseSubscriptionTokensController>(TYPES.Auth_BaseSubscriptionTokensController)
.toConstantValue(
new BaseSubscriptionTokensController(
container.get(TYPES.Auth_CreateSubscriptionToken),
container.get(TYPES.Auth_AuthenticateSubscriptionToken),
container.get(TYPES.Auth_SettingService),
container.get(TYPES.Auth_UserProjector),
container.get(TYPES.Auth_RoleProjector),
container.get(TYPES.Auth_CrossServiceTokenEncoder),
container.get(TYPES.Auth_AUTH_JWT_TTL),
container.get(TYPES.Auth_ControllerContainer),
container.get<CreateSubscriptionToken>(TYPES.Auth_CreateSubscriptionToken),
container.get<AuthenticateSubscriptionToken>(TYPES.Auth_AuthenticateSubscriptionToken),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<ProjectorInterface<User>>(TYPES.Auth_UserProjector),
container.get<ProjectorInterface<Role>>(TYPES.Auth_RoleProjector),
container.get<TokenEncoderInterface<CrossServiceTokenData>>(TYPES.Auth_CrossServiceTokenEncoder),
container.get<number>(TYPES.Auth_AUTH_JWT_TTL),
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
),
)
container
.bind<BaseSubscriptionSettingsController>(TYPES.Auth_BaseSubscriptionSettingsController)
.toConstantValue(
new BaseSubscriptionSettingsController(
container.get(TYPES.Auth_GetSetting),
container.get(TYPES.Auth_ControllerContainer),
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
container.get<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
TYPES.Auth_SubscriptionSettingHttpMapper,
),
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
),
)
container
.bind<BaseSettingsController>(TYPES.Auth_BaseSettingsController)
.toConstantValue(
new BaseSettingsController(
container.get(TYPES.Auth_GetSettings),
container.get(TYPES.Auth_GetSetting),
container.get(TYPES.Auth_UpdateSetting),
container.get(TYPES.Auth_DeleteSetting),
container.get(TYPES.Auth_ControllerContainer),
container.get<GetAllSettingsForUser>(TYPES.Auth_GetAllSettingsForUser),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
container.get<MapperInterface<Setting, SettingHttpRepresentation>>(TYPES.Auth_SettingHttpMapper),
container.get<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
TYPES.Auth_SubscriptionSettingHttpMapper,
),
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
),
)
container

View File

@@ -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,

View File

@@ -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')

View File

@@ -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'),
@@ -36,7 +40,6 @@ const TYPES = {
Auth_AuthenticatorRepository: Symbol.for('Auth_AuthenticatorRepository'),
Auth_AuthenticatorChallengeRepository: Symbol.for('Auth_AuthenticatorChallengeRepository'),
Auth_CacheEntryRepository: Symbol.for('Auth_CacheEntryRepository'),
Auth_TransitionStatusRepository: Symbol.for('Auth_TransitionStatusRepository'),
Auth_SharedVaultUserRepository: Symbol.for('Auth_SharedVaultUserRepository'),
// ORM
Auth_ORMOfflineSettingRepository: Symbol.for('Auth_ORMOfflineSettingRepository'),
@@ -66,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'),
@@ -92,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'),
@@ -121,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'),
@@ -152,18 +149,22 @@ 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'),
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
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'),
@@ -173,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'),
@@ -190,7 +190,6 @@ const TYPES = {
Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'),
Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
Auth_TransitionStatusUpdatedEventHandler: Symbol.for('Auth_TransitionStatusUpdatedEventHandler'),
Auth_UserAddedToSharedVaultEventHandler: Symbol.for('Auth_UserAddedToSharedVaultEventHandler'),
Auth_UserRemovedFromSharedVaultEventHandler: Symbol.for('Auth_UserRemovedFromSharedVaultEventHandler'),
Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler: Symbol.for(
@@ -199,8 +198,6 @@ const TYPES = {
// Services
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
Auth_SessionService: Symbol.for('Auth_SessionService'),
Auth_SettingService: Symbol.for('Auth_SettingService'),
Auth_SubscriptionSettingService: Symbol.for('Auth_SubscriptionSettingService'),
Auth_OfflineSettingService: Symbol.for('Auth_OfflineSettingService'),
Auth_AuthResponseFactory20161215: Symbol.for('Auth_AuthResponseFactory20161215'),
Auth_AuthResponseFactory20190520: Symbol.for('Auth_AuthResponseFactory20190520'),
@@ -221,7 +218,6 @@ const TYPES = {
Auth_DomainEventSubscriber: Symbol.for('Auth_DomainEventSubscriber'),
Auth_DomainEventFactory: Symbol.for('Auth_DomainEventFactory'),
Auth_DomainEventMessageHandler: Symbol.for('Auth_DomainEventMessageHandler'),
Auth_HTTPClient: Symbol.for('Auth_HTTPClient'),
Auth_Crypter: Symbol.for('Auth_Crypter'),
Auth_CryptoNode: Symbol.for('Auth_CryptoNode'),
Auth_Timer: Symbol.for('Auth_Timer'),
@@ -232,11 +228,10 @@ const TYPES = {
Auth_SettingsAssociationService: Symbol.for('Auth_SettingsAssociationService'),
Auth_SubscriptionSettingsAssociationService: Symbol.for('Auth_SubscriptionSettingsAssociationService'),
Auth_FeatureService: Symbol.for('Auth_FeatureService'),
Auth_SettingDecrypter: Symbol.for('Auth_SettingDecrypter'),
Auth_SettingCrypter: Symbol.for('Auth_SettingCrypter'),
Auth_SettingInterpreter: Symbol.for('Auth_SettingInterpreter'),
Auth_ProtocolVersionSelector: Symbol.for('Auth_ProtocolVersionSelector'),
Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'),
Auth_UserSubscriptionService: Symbol.for('Auth_UserSubscriptionService'),
Auth_BaseAuthController: Symbol.for('Auth_BaseAuthController'),
Auth_BaseAuthenticatorsController: Symbol.for('Auth_BaseAuthenticatorsController'),
Auth_BaseSubscriptionInvitesController: Symbol.for('Auth_BaseSubscriptionInvitesController'),

View File

@@ -20,7 +20,6 @@ import {
StatisticPersistenceRequestedEvent,
SessionCreatedEvent,
SessionRefreshedEvent,
TransitionRequestedEvent,
} from '@standardnotes/domain-events'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import { TimerInterface } from '@standardnotes/time'
@@ -34,25 +33,6 @@ import { KeyParamsData } from '@standardnotes/responses'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
createTransitionRequestedEvent(dto: {
userUuid: string
type: 'items' | 'revisions'
timestamp: number
}): TransitionRequestedEvent {
return {
type: 'TRANSITION_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Auth,
},
payload: dto,
}
}
createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent {
return {
type: 'SESSION_CREATED',

View File

@@ -18,7 +18,6 @@ import {
StatisticPersistenceRequestedEvent,
SessionCreatedEvent,
SessionRefreshedEvent,
TransitionRequestedEvent,
} from '@standardnotes/domain-events'
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
import { KeyParamsData } from '@standardnotes/responses'
@@ -92,9 +91,4 @@ export interface DomainEventFactoryInterface {
}): StatisticPersistenceRequestedEvent
createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent
createSessionRefreshedEvent(dto: { userUuid: string }): SessionRefreshedEvent
createTransitionRequestedEvent(dto: {
userUuid: string
type: 'items' | 'revisions'
timestamp: number
}): TransitionRequestedEvent
}

View File

@@ -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 = {

View File

@@ -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()
})
})

View File

@@ -1,26 +1,22 @@
import { DomainEventHandlerInterface, ExtensionKeyGrantedEvent } from '@standardnotes/domain-events'
import { Username } from '@standardnotes/domain-core'
import { SettingName } from '@standardnotes/settings'
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 +54,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}`)
}
}
}

View File

@@ -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"}]',
},
})
})
})

View File

@@ -1,19 +1,18 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, ListedAccountCreatedEvent } from '@standardnotes/domain-events'
import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
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}`)
}
}
}

View File

@@ -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"}]',
},
})
})
})

View File

@@ -1,19 +1,18 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, ListedAccountDeletedEvent } from '@standardnotes/domain-events'
import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
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}`)
}
}
}

View File

@@ -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')
})
})

View File

@@ -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()
})
})

View File

@@ -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()
})
})

View File

@@ -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)
})
})

View File

@@ -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()
})
})

View File

@@ -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()
})
})

View File

@@ -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> {

View File

@@ -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()
})
})

View File

@@ -1,31 +1,25 @@
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 { 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 +47,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> {

View File

@@ -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()
})
})

View File

@@ -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()
})
})

View File

@@ -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()
})
})

View File

@@ -1,9 +1,10 @@
import { OfflineFeaturesTokenData } from '@standardnotes/security'
import { SettingName } from '@standardnotes/settings'
import { 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 +12,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 +88,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(

View File

@@ -1,23 +0,0 @@
import { DomainEventHandlerInterface, TransitionStatusUpdatedEvent } from '@standardnotes/domain-events'
import { UpdateTransitionStatus } from '../UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
import { Logger } from 'winston'
export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface {
constructor(
private updateTransitionStatusUseCase: UpdateTransitionStatus,
private logger: Logger,
) {}
async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
const result = await this.updateTransitionStatusUseCase.execute({
status: event.payload.status,
userUuid: event.payload.userUuid,
transitionType: event.payload.transitionType,
transitionTimestamp: event.payload.transitionTimestamp,
})
if (result.isFailed()) {
this.logger.error(`Failed to update transition status for user ${event.payload.userUuid}`)
}
}
}

View File

@@ -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')
})
})

View File

@@ -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()
})
})

View File

@@ -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}`)
}
}

View File

@@ -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()
})
})

View File

@@ -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,
})
}
}

View File

@@ -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,

View File

@@ -1,15 +1,13 @@
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 { 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 +16,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 +315,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
}
}

View File

@@ -1,7 +0,0 @@
import { User } from '../User/User'
import { SettingProps } from './SettingProps'
export type CreateOrReplaceSettingDto = {
user: User
props: SettingProps
}

View File

@@ -1,6 +0,0 @@
import { Setting } from './Setting'
export type CreateOrReplaceSettingResponse = {
status: 'created' | 'replaced'
setting: Setting
}

View File

@@ -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
}

View File

@@ -1,6 +0,0 @@
import { SubscriptionSetting } from './SubscriptionSetting'
export type CreateOrReplaceSubscriptionSettingResponse = {
status: 'created' | 'replaced'
subscriptionSetting: SubscriptionSetting
}

View File

@@ -1,7 +0,0 @@
import { SettingName } from '@standardnotes/settings'
export type FindSettingDTO = {
userUuid: string
settingName: SettingName
settingUuid?: string
}

View File

@@ -1,8 +0,0 @@
import { SettingName } from '@standardnotes/settings'
export type FindSubscriptionSettingDTO = {
userUuid: string
userSubscriptionUuid: string
subscriptionSettingName: SettingName
settingUuid?: string
}

View File

@@ -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))
}
}

View File

@@ -0,0 +1,240 @@
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 } from '@standardnotes/settings'
import { 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()
})
})
})

View 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
}
}

View 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>
}

View File

@@ -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()
})
})

View File

@@ -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
}
}

View File

@@ -1,6 +0,0 @@
import { Setting } from './Setting'
import { SubscriptionSetting } from './SubscriptionSetting'
export interface SettingDecrypterInterface {
decryptSettingValue(setting: Setting | SubscriptionSetting, userUuid: string): Promise<string | null>
}

View File

@@ -1,8 +1,4 @@
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
export type SettingDescription = {
value: string
sensitive: boolean
serverEncryptionVersion: EncryptionVersion
replaceable: boolean
}

View File

@@ -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()
})
})

View File

@@ -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}!`)
}
}
}

View File

@@ -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>
}

View File

@@ -15,19 +15,20 @@ 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 } 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 +45,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 +97,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 +118,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 () => {

View File

@@ -54,8 +54,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({

View File

@@ -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
}

View File

@@ -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>
}

View File

@@ -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',
})
})
})

View File

@@ -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,
}
}
}

View File

@@ -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>
}

View File

@@ -32,6 +32,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 +50,6 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
[
SettingName.NAMES.MuteMarketingEmails,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: MuteMarketingEmailsOption.NotMuted,
replaceable: false,
},
@@ -58,8 +57,6 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
[
SettingName.NAMES.LogSessionUserAgent,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: LogSessionUserAgentOption.Enabled,
replaceable: false,
},
@@ -70,8 +67,6 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
[
SettingName.NAMES.LogSessionUserAgent,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: LogSessionUserAgentOption.Disabled,
replaceable: false,
},

View File

@@ -1,3 +0,0 @@
import { Setting } from './Setting'
export type SimpleSetting = Omit<Setting, 'user' | 'serverEncryptionVersion'>

View File

@@ -1,3 +0,0 @@
import { SubscriptionSetting } from './SubscriptionSetting'
export type SimpleSubscriptionSetting = Omit<SubscriptionSetting, 'userSubscription' | 'serverEncryptionVersion'>

View File

@@ -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))
}
}

View File

@@ -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
}

View File

@@ -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>
}

Some files were not shown because too many files have changed in this diff Show More