Compare commits

...

53 Commits

Author SHA1 Message Date
standardci
8a0accd8ea chore(release): publish new version
- @standardnotes/analytics@2.32.6
 - @standardnotes/api-gateway@1.81.10
 - @standardnotes/auth-server@1.165.4
 - @standardnotes/domain-events-infra@1.20.4
 - @standardnotes/domain-events@2.133.1
 - @standardnotes/event-store@1.13.19
 - @standardnotes/files-server@1.32.5
 - @standardnotes/home-server@1.18.23
 - @standardnotes/revisions-server@1.47.5
 - @standardnotes/scheduler-server@1.26.6
 - @standardnotes/syncing-server@1.120.3
 - @standardnotes/websockets-server@1.17.6
2023-11-07 11:00:09 +00:00
Karol Sójko
d66ae62cf4 fix: account deletion event (#904)
* fix: account deletion event

* fix: feature service binding
2023-11-07 11:34:10 +01:00
standardci
b01d1c659d chore(release): publish new version
- @standardnotes/analytics@2.32.5
 - @standardnotes/api-gateway@1.81.9
 - @standardnotes/auth-server@1.165.3
 - @standardnotes/domain-events-infra@1.20.3
 - @standardnotes/event-store@1.13.18
 - @standardnotes/files-server@1.32.4
 - @standardnotes/home-server@1.18.22
 - @standardnotes/revisions-server@1.47.4
 - @standardnotes/scheduler-server@1.26.5
 - @standardnotes/syncing-server@1.120.2
 - @standardnotes/websockets-server@1.17.5
2023-11-07 08:52:34 +00:00
Karol Sójko
751f3b2547 fix: remove open telemetry from code (#903) 2023-11-07 09:15:31 +01:00
standardci
11514e3836 chore(release): publish new version
- @standardnotes/home-server@1.18.21
 - @standardnotes/syncing-server@1.120.1
2023-11-06 15:00:46 +00:00
Karol Sójko
71689c1497 fix(syncing-server): return cursor token upon transfer limit breached (#902)
* fix(syncing-server): return cursor token upon transfer limit breached

* fix e2e env vars
2023-11-06 15:38:28 +01:00
standardci
2742075edc chore(release): publish new version
- @standardnotes/auth-server@1.165.2
 - @standardnotes/home-server@1.18.20
2023-11-03 17:04:35 +00:00
Karol Sójko
7f16232f8b fix(auth): change log severity on user authentication 2023-11-03 17:36:29 +01:00
standardci
0b0703e6d1 chore(release): publish new version
- @standardnotes/api-gateway@1.81.8
 - @standardnotes/auth-server@1.165.1
 - @standardnotes/home-server@1.18.19
2023-11-03 16:27:17 +00:00
Karol Sójko
3e376c44e3 fix: retry attempts on session validation and more verbose logs (#898) 2023-11-03 11:23:02 +01:00
standardci
bfe2d4bb4a chore(release): publish new version
- @standardnotes/auth-server@1.165.0
 - @standardnotes/home-server@1.18.18
 - @standardnotes/syncing-server@1.120.0
2023-11-02 12:01:26 +00:00
Karol Sójko
7253a0a1d9 feat: add shared vault invitation email notifications (#897) 2023-11-02 12:35:01 +01:00
standardci
f2c5810023 chore(release): publish new version
- @standardnotes/home-server@1.18.17
2023-11-02 07:13:07 +00:00
Karol Sójko
2e5b9105b8 fix(home-server): remove unused dep 2023-11-02 07:42:10 +01:00
Karol Sójko
d14411d72e chore: upgrade deps 2023-11-02 07:40:27 +01:00
standardci
5226513b26 chore(release): publish new version
- @standardnotes/auth-server@1.164.2
 - @standardnotes/home-server@1.18.16
2023-11-01 14:18:52 +00:00
Mo
334449f8aa chore: update change email copy 2023-11-01 08:50:37 -05:00
dependabot[bot]
7f43d0c69d chore(deps): bump docker/login-action from 2 to 3 (#832)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 13:33:03 +01:00
dependabot[bot]
6f18276e7a chore(deps): bump crazy-max/ghaction-import-gpg from 5 to 6 (#818)
Bumps [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) from 5 to 6.
- [Release notes](https://github.com/crazy-max/ghaction-import-gpg/releases)
- [Commits](https://github.com/crazy-max/ghaction-import-gpg/compare/v5...v6)

---
updated-dependencies:
- dependency-name: crazy-max/ghaction-import-gpg
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 13:32:41 +01:00
dependabot[bot]
9ff18a18a5 chore(deps): bump aws-actions/amazon-ecr-login from 1 to 2 (#873)
Bumps [aws-actions/amazon-ecr-login](https://github.com/aws-actions/amazon-ecr-login) from 1 to 2.
- [Release notes](https://github.com/aws-actions/amazon-ecr-login/releases)
- [Changelog](https://github.com/aws-actions/amazon-ecr-login/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/amazon-ecr-login/compare/v1...v2)

---
updated-dependencies:
- dependency-name: aws-actions/amazon-ecr-login
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 13:31:49 +01:00
dependabot[bot]
999e72fb1f chore(deps): bump ua-parser-js from 1.0.35 to 1.0.37 (#891)
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 1.0.35 to 1.0.37.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/1.0.37/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/1.0.35...1.0.37)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 13:31:11 +01:00
standardci
4733e663a3 chore(release): publish new version
- @standardnotes/auth-server@1.164.1
 - @standardnotes/home-server@1.18.15
2023-11-01 11:51:38 +00:00
Karol Sójko
b48eeb16c3 fix(auth): creating valet tokens for shared subscription users (#895)
* fix(auth): creating valet tokens for shared subscription users

* fix(auth): updating storage quota
2023-11-01 12:18:44 +01:00
standardci
0aa2584e82 chore(release): publish new version
- @standardnotes/auth-server@1.164.0
 - @standardnotes/home-server@1.18.14
2023-11-01 08:55:15 +00:00
Karol Sójko
eb8c704d84 feat(auth): add sending email to old email address when the address is changed (#894) 2023-11-01 09:28:00 +01:00
standardci
e93fa14703 chore(release): publish new version
- @standardnotes/api-gateway@1.81.7
 - @standardnotes/files-server@1.32.3
 - @standardnotes/home-server@1.18.13
 - @standardnotes/syncing-server@1.119.4
2023-10-31 14:16:00 +00:00
Karol Sójko
16a6815b69 fix: add fallback methods for 404 requests (#893)
* fix: add fallback methods for 404 requests

* fix: remove fallback controllers exports

* fix: have only one fallback controller expored
2023-10-31 14:44:33 +01:00
standardci
b08e9731b8 chore(release): publish new version
- @standardnotes/auth-server@1.163.2
 - @standardnotes/home-server@1.18.12
2023-10-30 12:23:43 +00:00
Karol Sójko
9bd4fb2d79 fix(auth): checking permissions to update setting only when directly performed by user (#892) 2023-10-30 12:51:31 +01:00
standardci
647aeda1de chore(release): publish new version
- @standardnotes/auth-server@1.163.1
 - @standardnotes/home-server@1.18.11
2023-10-30 11:20:27 +00:00
Karol Sójko
78ff748d91 fix(auth): add more information on the listed creation error 2023-10-30 11:52:26 +01:00
standardci
31f8cf1169 chore(release): publish new version
- @standardnotes/api-gateway@1.81.6
 - @standardnotes/home-server@1.18.10
2023-10-27 10:03:28 +00:00
Karol Sójko
14bcf7b6c9 fix(api-gateway): logs for errors reaching service 2023-10-27 11:36:52 +02:00
standardci
74adddd1e7 chore(release): publish new version
- @standardnotes/analytics@2.32.4
 - @standardnotes/api-gateway@1.81.5
 - @standardnotes/auth-server@1.163.0
 - @standardnotes/domain-core@1.40.0
 - @standardnotes/event-store@1.13.17
 - @standardnotes/files-server@1.32.2
 - @standardnotes/home-server@1.18.9
 - @standardnotes/revisions-server@1.47.3
 - @standardnotes/scheduler-server@1.26.4
 - @standardnotes/settings@1.22.0
 - @standardnotes/syncing-server@1.119.3
 - @standardnotes/websockets-server@1.17.4
2023-10-26 13:52:56 +00:00
Karol Sójko
0e43bc0042 feat: extract setting name to domain-core package 2023-10-26 15:18:11 +02:00
standardci
b40d539611 chore(release): publish new version
- @standardnotes/api-gateway@1.81.4
 - @standardnotes/home-server@1.18.8
2023-10-26 11:53:47 +00:00
Karol Sójko
654663d17f fix(api-gateway): retry attempts and logs 2023-10-26 13:27:27 +02:00
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
344 changed files with 6004 additions and 10089 deletions

2
.github/ci.env vendored
View File

@@ -26,3 +26,5 @@ MYSQL_ROOT_PASSWORD=changeme123
AUTH_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
SYNCING_SERVER_CONTENT_SIZE_TRANSFER_LIMIT=1000000

View File

@@ -55,7 +55,7 @@ jobs:
run: yarn build
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -69,7 +69,7 @@ jobs:
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
uses: aws-actions/amazon-ecr-login@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@master

View File

@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -70,6 +70,7 @@ jobs:
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=2" >> packages/home-server/.env
echo "CONTENT_SIZE_TRANSFER_LIMIT=1000000" >> packages/home-server/.env
echo "DB_HOST=localhost" >> packages/home-server/.env
echo "DB_PORT=3306" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env

View File

@@ -112,13 +112,13 @@ jobs:
suite: 'vaults'
publish-self-hosting:
needs: [ test, lint, e2e-base ]
needs: [ test, lint, e2e-base, e2e-vaults ]
name: Publish Self Hosting Docker Image
uses: standardnotes/server/.github/workflows/common-self-hosting.yml@main
secrets: inherit
publish-services:
needs: [ test, lint, e2e-base ]
needs: [ test, lint, e2e-base, e2e-vaults ]
runs-on: ubuntu-latest
@@ -143,7 +143,7 @@ jobs:
git config --global user.email "ci@standardnotes.com"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v5
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}

13
.pnp.cjs generated
View File

@@ -5546,7 +5546,6 @@ const RAW_RUNTIME_STATE =
["@types/uuid", "npm:9.0.3"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
["axios", "npm:1.4.0"],\
["bcryptjs", "npm:2.4.3"],\
["cors", "npm:2.8.5"],\
["dayjs", "npm:1.11.7"],\
@@ -5567,7 +5566,7 @@ const RAW_RUNTIME_STATE =
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.17"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["ua-parser-js", "npm:1.0.35"],\
["ua-parser-js", "npm:1.0.37"],\
["uuid", "npm:9.0.0"],\
["winston", "npm:3.9.0"]\
],\
@@ -5786,7 +5785,6 @@ const RAW_RUNTIME_STATE =
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
["@types/cors", "npm:2.8.13"],\
["@types/express", "npm:4.17.17"],\
["@types/prettyjson", "npm:0.0.30"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
["cors", "npm:2.8.5"],\
@@ -5799,7 +5797,6 @@ const RAW_RUNTIME_STATE =
["inversify", "npm:6.0.1"],\
["inversify-express-utils", "npm:6.4.3"],\
["prettier", "npm:3.0.3"],\
["prettyjson", "npm:1.2.5"],\
["reflect-metadata", "npm:0.1.13"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["winston", "npm:3.9.0"]\
@@ -6081,7 +6078,7 @@ const RAW_RUNTIME_STATE =
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.17"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["ua-parser-js", "npm:1.0.35"],\
["ua-parser-js", "npm:1.0.37"],\
["uuid", "npm:9.0.0"],\
["winston", "npm:3.9.0"]\
],\
@@ -16150,10 +16147,10 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["ua-parser-js", [\
["npm:1.0.35", {\
"packageLocation": "./.yarn/cache/ua-parser-js-npm-1.0.35-38ecdb7612-b69c99c20f.zip/node_modules/ua-parser-js/",\
["npm:1.0.37", {\
"packageLocation": "./.yarn/cache/ua-parser-js-npm-1.0.37-b79655e1b5-56508f2428.zip/node_modules/ua-parser-js/",\
"packageDependencies": [\
["ua-parser-js", "npm:1.0.35"]\
["ua-parser-js", "npm:1.0.37"]\
],\
"linkType": "HARD"\
}]\

Binary file not shown.

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.32.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.5...@standardnotes/analytics@2.32.6) (2023-11-07)
**Note:** Version bump only for package @standardnotes/analytics
## [2.32.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.4...@standardnotes/analytics@2.32.5) (2023-11-07)
### Bug Fixes
* remove open telemetry from code ([#903](https://github.com/standardnotes/server/issues/903)) ([751f3b2](https://github.com/standardnotes/server/commit/751f3b25476c5be3d663ad8540c43266acd39493))
## [2.32.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.3...@standardnotes/analytics@2.32.4) (2023-10-26)
**Note:** Version bump only for package @standardnotes/analytics
## [2.32.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.2...@standardnotes/analytics@2.32.3) (2023-10-26)
**Note:** Version bump only for package @standardnotes/analytics
## [2.32.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.32.1...@standardnotes/analytics@2.32.2) (2023-10-19)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,11 +1,5 @@
import 'reflect-metadata'
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
import { EmailLevel, ServiceIdentifier } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AnalyticsScheduledTask })
sdk.start()
import { Logger } from 'winston'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
@@ -22,6 +16,7 @@ import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/Calculat
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
import { TimerInterface } from '@standardnotes/time'
import { StatisticMeasureName } from '../src/Domain/Statistics/StatisticMeasureName'
import { EmailLevel } from '@standardnotes/domain-core'
const requestReport = async (
analyticsStore: AnalyticsStoreInterface,
@@ -275,9 +270,6 @@ void container.load().then((container) => {
logger.info(`Sending report to following admins: ${adminEmails}`)
const tracer = new OpenTelemetryTracer()
tracer.startSpan(ServiceIdentifier.NAMES.AnalyticsScheduledTask, 'report')
Promise.resolve(
requestReport(
analyticsStore,
@@ -293,15 +285,11 @@ void container.load().then((container) => {
.then(() => {
logger.info('Usage report generation complete')
tracer.stopSpan()
process.exit(0)
})
.catch((error) => {
logger.error(`Could not finish usage report generation: ${error.message}`)
tracer.stopSpanWithError(error)
process.exit(1)
})
})

View File

@@ -1,11 +1,5 @@
import 'reflect-metadata'
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
import { ServiceIdentifier } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AnalyticsWorker })
sdk.start()
import { Logger } from 'winston'
import { DomainEventSubscriberInterface } from '@standardnotes/domain-events'
import * as dayjs from 'dayjs'

View File

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

View File

@@ -7,7 +7,7 @@ import {
DomainEventPublisherInterface,
DomainEventSubscriberInterface,
} from '@standardnotes/domain-events'
import { MapperInterface, ServiceIdentifier } from '@standardnotes/domain-core'
import { MapperInterface } from '@standardnotes/domain-core'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Mixpanel = require('mixpanel')
@@ -16,9 +16,9 @@ import TYPES from './Types'
import { AppDataSource } from './DataSource'
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
import {
SNSOpenTelemetryDomainEventPublisher,
SNSDomainEventPublisher,
SQSDomainEventSubscriber,
SQSEventMessageHandler,
SQSOpenTelemetryDomainEventSubscriber,
} from '@standardnotes/domain-events-infra'
import { Timer, TimerInterface } from '@standardnotes/time'
import { PeriodKeyGeneratorInterface } from '../Domain/Time/PeriodKeyGeneratorInterface'
@@ -139,9 +139,7 @@ export class ContainerConfigLoader {
container
.bind<DomainEventPublisherInterface>(TYPES.DomainEventPublisher)
.toConstantValue(
new SNSOpenTelemetryDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)),
)
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
if (env.get('MIXPANEL_TOKEN', true)) {
container.bind<Mixpanel>(TYPES.MixpanelClient).toConstantValue(Mixpanel.init(env.get('MIXPANEL_TOKEN', true)))
}
@@ -242,8 +240,7 @@ export class ContainerConfigLoader {
container
.bind<DomainEventSubscriberInterface>(TYPES.DomainEventSubscriber)
.toConstantValue(
new SQSOpenTelemetryDomainEventSubscriber(
ServiceIdentifier.NAMES.AnalyticsWorker,
new SQSDomainEventSubscriber(
container.get<SQSClient>(TYPES.SQS),
container.get<string>(TYPES.SQS_QUEUE_URL),
container.get<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler),

View File

@@ -3,6 +3,72 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.81.10](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.9...@standardnotes/api-gateway@1.81.10) (2023-11-07)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.81.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.8...@standardnotes/api-gateway@1.81.9) (2023-11-07)
### Bug Fixes
* remove open telemetry from code ([#903](https://github.com/standardnotes/api-gateway/issues/903)) ([751f3b2](https://github.com/standardnotes/api-gateway/commit/751f3b25476c5be3d663ad8540c43266acd39493))
## [1.81.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.7...@standardnotes/api-gateway@1.81.8) (2023-11-03)
### Bug Fixes
* retry attempts on session validation and more verbose logs ([#898](https://github.com/standardnotes/api-gateway/issues/898)) ([3e376c4](https://github.com/standardnotes/api-gateway/commit/3e376c44e3a6c336dcff3d8ef5eb3ab040d9a561))
## [1.81.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.6...@standardnotes/api-gateway@1.81.7) (2023-10-31)
### Bug Fixes
* add fallback methods for 404 requests ([#893](https://github.com/standardnotes/api-gateway/issues/893)) ([16a6815](https://github.com/standardnotes/api-gateway/commit/16a6815b69e344573ae07682f3bac1d44d715d79))
## [1.81.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.5...@standardnotes/api-gateway@1.81.6) (2023-10-27)
### Bug Fixes
* **api-gateway:** logs for errors reaching service ([14bcf7b](https://github.com/standardnotes/api-gateway/commit/14bcf7b6c9403c3413e7579f58ea17168d14dce7))
## [1.81.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.4...@standardnotes/api-gateway@1.81.5) (2023-10-26)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.81.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.3...@standardnotes/api-gateway@1.81.4) (2023-10-26)
### Bug Fixes
* **api-gateway:** retry attempts and logs ([654663d](https://github.com/standardnotes/api-gateway/commit/654663d17f6eee15f7bf2bc7f40e6c37a3d8e53c))
## [1.81.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.2...@standardnotes/api-gateway@1.81.3) (2023-10-26)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.81.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.1...@standardnotes/api-gateway@1.81.2) (2023-10-20)
### Bug Fixes
* **api-gateway:** add session validation retry attempts on timedout requests ([6aee51b](https://github.com/standardnotes/api-gateway/commit/6aee51bd45c25e85d01075a9c8d2854b32dd6e3c))
## [1.81.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.0...@standardnotes/api-gateway@1.81.1) (2023-10-20)
### Bug Fixes
* **api-gateway:** logs severity on retry attempts ([1c3d19c](https://github.com/standardnotes/api-gateway/commit/1c3d19cca43a7a3eba2b0d05c820de5112edf89e))
# [1.81.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.80.1...@standardnotes/api-gateway@1.81.0) (2023-10-20)
### Features
* **api-gateway:** add retry attempts on timedout requests ([#885](https://github.com/standardnotes/api-gateway/issues/885)) ([ce35767](https://github.com/standardnotes/api-gateway/commit/ce357679e9bc704ab562e9d6ca192f49a794a664))
## [1.80.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.80.0...@standardnotes/api-gateway@1.80.1) (2023-10-19)
### Bug Fixes
* **api-gateway:** stringify error in service proxy ([e385926](https://github.com/standardnotes/api-gateway/commit/e38592604644e0f52df0865ffae5b7e79d1d3d07))
# [1.80.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.79.14...@standardnotes/api-gateway@1.80.0) (2023-10-19)
### Features

View File

@@ -1,11 +1,5 @@
import 'reflect-metadata'
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
import { ServiceIdentifier } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.ApiGateway })
sdk.start()
import '../src/Controller/LegacyController'
import '../src/Controller/HealthCheckController'

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.80.0",
"version": "1.81.10",
"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

@@ -74,13 +74,16 @@ export abstract class AuthMiddleware extends BaseMiddleware {
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
} catch (error) {
const errorMessage = (error as AxiosError).isAxiosError
? JSON.stringify((error as AxiosError).response?.data)
: (error as Error).message
let detailedErrorMessage = (error as Error).message
if (error instanceof AxiosError) {
detailedErrorMessage = `Status: ${error.status}, code: ${error.code}, message: ${error.message}`
}
this.logger.error(`Could not pass the request to sessions/validate on underlying service: ${errorMessage}`)
this.logger.error(
`Could not pass the request to sessions/validate on underlying service: ${detailedErrorMessage}`,
)
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
this.logger.debug(`Response error: ${JSON.stringify(error)}`)
if ((error as AxiosError).response?.headers['content-type']) {
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
@@ -91,7 +94,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
? +((error as AxiosError).code as string)
: 500
response.status(errorCode).send(errorMessage)
const responseErrorMessage = (error as AxiosError).response?.data
response
.status(errorCode)
.send(
responseErrorMessage ??
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
)
return
}

View File

@@ -0,0 +1,9 @@
import { BaseHttpController, all, controller, results } from 'inversify-express-utils'
@controller('')
export class FallbackController extends BaseHttpController {
@all('*')
public async fallback(): Promise<results.NotFoundResult> {
return this.notFound()
}
}

View File

@@ -1,4 +1,5 @@
export * from './AuthMiddleware'
export * from './FallbackController'
export * from './HealthCheckController'
export * from './SubscriptionTokenAuthMiddleware'
export * from './TokenAuthenticationMethod'

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,49 @@ export class HttpServiceProxy implements ServiceProxyInterface {
@inject(TYPES.ApiGateway_HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
@inject(TYPES.ApiGateway_CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
@inject(TYPES.ApiGateway_Timer) private timer: TimerInterface,
) {}
async validateSession(headers: {
authorization: string
sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authResponse = await this.httpClient.request({
method: 'POST',
headers: {
Authorization: headers.authorization,
Accept: 'application/json',
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
validateStatus: (status: number) => {
return status >= 200 && status < 500
},
url: `${this.authServerUrl}/sessions/validate`,
})
async validateSession(
headers: {
authorization: string
sharedVaultOwnerContext?: string
},
retryAttempt?: number,
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
try {
const authResponse = await this.httpClient.request({
method: 'POST',
headers: {
Authorization: headers.authorization,
Accept: 'application/json',
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
validateStatus: (status: number) => {
return status >= 200 && status < 500
},
url: `${this.authServerUrl}/sessions/validate`,
})
return {
status: authResponse.status,
data: authResponse.data,
headers: {
contentType: authResponse.headers['content-type'] as string,
},
return {
status: authResponse.status,
data: authResponse.data,
headers: {
contentType: authResponse.headers['content-type'] as string,
},
}
} catch (error) {
const requestDidNotMakeIt = this.requestTimedOutOrDidNotReachDestination(error as Record<string, unknown>)
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
await this.timer.sleep(50)
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
return this.validateSession(headers, nextRetryAttempt)
}
throw error
}
}
@@ -169,6 +188,7 @@ export class HttpServiceProxy implements ServiceProxyInterface {
response: Response,
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
retryAttempt?: number,
): Promise<AxiosResponse | undefined> {
try {
const headers: Record<string, string> = {}
@@ -211,17 +231,47 @@ export class HttpServiceProxy implements ServiceProxyInterface {
await this.crossServiceTokenCache.invalidate(userUuid)
}
if (retryAttempt) {
this.logger.debug(
`Request to ${serverUrl}/${endpointOrMethodIdentifier} succeeded after ${retryAttempt} retries`,
)
}
return serviceResponse
} catch (error) {
const errorMessage = (error as AxiosError).isAxiosError
? JSON.stringify((error as AxiosError).response?.data)
: (error as Error).message
const requestDidNotMakeIt = this.requestTimedOutOrDidNotReachDestination(error as Record<string, unknown>)
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
await this.timer.sleep(50)
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
this.logger.debug(
`Retrying request to ${serverUrl}/${endpointOrMethodIdentifier} for the ${nextRetryAttempt} time`,
)
return this.getServerResponse(
serverUrl,
request,
response,
endpointOrMethodIdentifier,
payload,
nextRetryAttempt,
)
}
let detailedErrorMessage = (error as Error).message
if (error instanceof AxiosError) {
detailedErrorMessage = `Status: ${error.status}, code: ${error.code}, message: ${error.message}`
}
this.logger.error(
`Could not pass the request to ${serverUrl}/${endpointOrMethodIdentifier} on underlying service: ${errorMessage}`,
tooManyRetryAttempts
? `Request to ${serverUrl}/${endpointOrMethodIdentifier} timed out after ${retryAttempt} retries`
: `Could not pass the request to ${serverUrl}/${endpointOrMethodIdentifier} on underlying service: ${detailedErrorMessage}`,
)
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
this.logger.debug(`Response error: ${JSON.stringify(error)}`)
if ((error as AxiosError).response?.headers['content-type']) {
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
@@ -232,7 +282,14 @@ export class HttpServiceProxy implements ServiceProxyInterface {
? +((error as AxiosError).code as string)
: 500
response.status(errorCode).send(errorMessage)
const responseErrorMessage = (error as AxiosError).response?.data
response
.status(errorCode)
.send(
responseErrorMessage ??
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
)
}
return
@@ -363,4 +420,13 @@ export class HttpServiceProxy implements ServiceProxyInterface {
}
})
}
private requestTimedOutOrDidNotReachDestination(error: Record<string, unknown>): boolean {
return (
('code' in error && error.code === 'ETIMEDOUT') ||
('response' in error &&
'status' in (error.response as Record<string, unknown>) &&
[503, 504].includes((error.response as Record<string, unknown>).status as number))
)
}
}

View File

@@ -50,7 +50,13 @@ export interface ServiceProxyInterface {
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
): Promise<void>
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
validateSession(
headers: {
authorization: string
sharedVaultOwnerContext?: string
},
retryAttempt?: number,
): Promise<{
status: number
data: unknown
headers: {

View File

@@ -9,10 +9,13 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
private filesServerUrl: string,
) {}
async validateSession(headers: {
authorization: string
sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
async validateSession(
headers: {
authorization: string
sharedVaultOwnerContext?: string
},
_retryAttempt?: number,
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
if (!authService) {
throw new Error('Auth service not found')

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,86 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.165.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.3...@standardnotes/auth-server@1.165.4) (2023-11-07)
### Bug Fixes
* account deletion event ([#904](https://github.com/standardnotes/server/issues/904)) ([d66ae62](https://github.com/standardnotes/server/commit/d66ae62cf4f413cac5f6f4eac45dc0f1ddbc9e32))
## [1.165.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.2...@standardnotes/auth-server@1.165.3) (2023-11-07)
### Bug Fixes
* remove open telemetry from code ([#903](https://github.com/standardnotes/server/issues/903)) ([751f3b2](https://github.com/standardnotes/server/commit/751f3b25476c5be3d663ad8540c43266acd39493))
## [1.165.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.1...@standardnotes/auth-server@1.165.2) (2023-11-03)
### Bug Fixes
* **auth:** change log severity on user authentication ([7f16232](https://github.com/standardnotes/server/commit/7f16232f8b13e3736801b6dc0af799e0559a3cfa))
## [1.165.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.165.0...@standardnotes/auth-server@1.165.1) (2023-11-03)
### Bug Fixes
* retry attempts on session validation and more verbose logs ([#898](https://github.com/standardnotes/server/issues/898)) ([3e376c4](https://github.com/standardnotes/server/commit/3e376c44e3a6c336dcff3d8ef5eb3ab040d9a561))
# [1.165.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.164.2...@standardnotes/auth-server@1.165.0) (2023-11-02)
### Features
* add shared vault invitation email notifications ([#897](https://github.com/standardnotes/server/issues/897)) ([7253a0a](https://github.com/standardnotes/server/commit/7253a0a1d92099df844c9baf6541b440bbcb0a68))
## [1.164.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.164.1...@standardnotes/auth-server@1.164.2) (2023-11-01)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.164.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.164.0...@standardnotes/auth-server@1.164.1) (2023-11-01)
### Bug Fixes
* **auth:** creating valet tokens for shared subscription users ([#895](https://github.com/standardnotes/server/issues/895)) ([b48eeb1](https://github.com/standardnotes/server/commit/b48eeb16c32031e73e9757e34c4b50ca0a3a773d))
# [1.164.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.163.2...@standardnotes/auth-server@1.164.0) (2023-11-01)
### Features
* **auth:** add sending email to old email address when the address is changed ([#894](https://github.com/standardnotes/server/issues/894)) ([eb8c704](https://github.com/standardnotes/server/commit/eb8c704d84277130dc0dc51c1fe475a7220612cd))
## [1.163.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.163.1...@standardnotes/auth-server@1.163.2) (2023-10-30)
### Bug Fixes
* **auth:** checking permissions to update setting only when directly performed by user ([#892](https://github.com/standardnotes/server/issues/892)) ([9bd4fb2](https://github.com/standardnotes/server/commit/9bd4fb2d794dae032286c68f23d3896b68735bdd))
## [1.163.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.163.0...@standardnotes/auth-server@1.163.1) (2023-10-30)
### Bug Fixes
* **auth:** add more information on the listed creation error ([78ff748](https://github.com/standardnotes/server/commit/78ff748d911a5a4063903847ef761822bbb8f4e2))
# [1.163.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.162.0...@standardnotes/auth-server@1.163.0) (2023-10-26)
### Features
* extract setting name to domain-core package ([0e43bc0](https://github.com/standardnotes/server/commit/0e43bc00427113f421b0c4b67c067f0de96caf52))
# [1.162.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.161.0...@standardnotes/auth-server@1.162.0) (2023-10-26)
### Features
* refactor settings ([#890](https://github.com/standardnotes/server/issues/890)) ([1b5078e](https://github.com/standardnotes/server/commit/1b5078eb9629397822f5403643c60fbf4182df92))
# [1.161.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.160.0...@standardnotes/auth-server@1.161.0) (2023-10-23)
### Bug Fixes
* allow to cancel previous subscription when activating premium features in e2e tests ([15af563](https://github.com/standardnotes/server/commit/15af5635f05a8363336aa33830e0157f519eee83))
### Features
* **auth:** remove axios http calls to payments server ([#889](https://github.com/standardnotes/server/issues/889)) ([a812f34](https://github.com/standardnotes/server/commit/a812f3400af3712fd5481b0c38c8805bb9c79e2c))
# [1.160.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.159.2...@standardnotes/auth-server@1.160.0) (2023-10-19)
### Features

View File

@@ -1,10 +1,6 @@
import 'reflect-metadata'
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
import { ServiceIdentifier } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
sdk.start()
import { SettingName } from '@standardnotes/domain-core'
import { Stream } from 'stream'
@@ -18,7 +14,7 @@ import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
import { MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/settings'
import { MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
import { PermissionName } from '@standardnotes/features'
import { GetUserKeyParams } from '../src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
@@ -62,8 +58,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 +70,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,
),
@@ -106,24 +102,17 @@ void container.load().then((container) => {
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher)
const getUserKeyParamsUseCase: GetUserKeyParams = container.get(TYPES.Auth_GetUserKeyParams)
const tracer = new OpenTelemetryTracer()
tracer.startSpan(ServiceIdentifier.NAMES.AuthScheduledTask, 'backup')
Promise.resolve(
requestBackups(settingRepository, roleService, domainEventFactory, domainEventPublisher, getUserKeyParamsUseCase),
)
.then(() => {
logger.info(`${backupFrequency} ${backupProvider} backup requesting complete`)
tracer.stopSpan()
process.exit(0)
})
.catch((error) => {
logger.error(`Could not finish ${backupFrequency} ${backupProvider} backup requesting: ${error.message}`)
tracer.stopSpanWithError(error)
process.exit(1)
})
})

View File

@@ -1,11 +1,5 @@
import 'reflect-metadata'
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
import { ServiceIdentifier } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
sdk.start()
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
@@ -36,22 +30,15 @@ void container.load().then((container) => {
const cleanupSessionTraces: CleanupSessionTraces = container.get(TYPES.Auth_CleanupSessionTraces)
const cleanupExpiredSessions: CleanupExpiredSessions = container.get(TYPES.Auth_CleanupExpiredSessions)
const tracer = new OpenTelemetryTracer()
tracer.startSpan(ServiceIdentifier.NAMES.AuthScheduledTask, 'cleanup')
Promise.resolve(cleanup(cleanupSessionTraces, cleanupExpiredSessions))
.then(() => {
logger.info('Expired sessions and session traces cleaned.')
tracer.stopSpan()
process.exit(0)
})
.catch((error) => {
logger.error(`Could not clean sessions and session traces: ${error.message}`)
tracer.stopSpanWithError(error)
process.exit(1)
})
})

View File

@@ -1,11 +1,5 @@
import 'reflect-metadata'
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
import { ServiceIdentifier } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.Auth })
sdk.start()
import '../src/Infra/InversifyExpressUtils/AnnotatedAuthController'
import '../src/Infra/InversifyExpressUtils/AnnotatedAuthenticatorsController'
import '../src/Infra/InversifyExpressUtils/AnnotatedSessionsController'

View File

@@ -1,11 +1,5 @@
import 'reflect-metadata'
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
import { ServiceIdentifier } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthScheduledTask })
sdk.start()
import { Logger } from 'winston'
import { TimerInterface } from '@standardnotes/time'
@@ -26,9 +20,6 @@ void container.load().then((container) => {
const persistStats: PersistStatistics = container.get(TYPES.Auth_PersistStatistics)
const timer: TimerInterface = container.get(TYPES.Auth_Timer)
const tracer = new OpenTelemetryTracer()
tracer.startSpan(ServiceIdentifier.NAMES.AuthScheduledTask, 'stats')
Promise.resolve(
persistStats.execute({
sessionsInADay: timer.getUTCDateNDaysAgo(1),
@@ -37,15 +28,11 @@ void container.load().then((container) => {
.then(() => {
logger.info('Stats persisted.')
tracer.stopSpan()
process.exit(0)
})
.catch((error) => {
logger.error(`Could not persist stats: ${error.message}`)
tracer.stopSpanWithError(error)
process.exit(1)
})
})

View File

@@ -1,11 +1,5 @@
import 'reflect-metadata'
import { OpenTelemetrySDK, OpenTelemetryTracer } from '@standardnotes/domain-events-infra'
import { Email, ServiceIdentifier } 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'
@@ -16,11 +10,12 @@ import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
import { MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/settings'
import { MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
import { PermissionName } from '@standardnotes/features'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { GetUserKeyParams } from '../src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
import { Email, SettingName } from '@standardnotes/domain-core'
const inputArgs = process.argv.slice(2)
const backupEmail = inputArgs[0]
@@ -55,8 +50,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 +62,7 @@ const requestBackups = async (
await domainEventPublisher.publish(
domainEventFactory.createEmailBackupRequestedEvent(
user.uuid,
emailsMutedSetting?.uuid as string,
emailsMutedSetting?.id.toString() as string,
userHasEmailsMuted,
keyParamsResponse.keyParams,
),
@@ -94,9 +89,6 @@ void container.load().then((container) => {
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher)
const getUserKeyParamsUseCase: GetUserKeyParams = container.get(TYPES.Auth_GetUserKeyParams)
const tracer = new OpenTelemetryTracer()
tracer.startSpan(ServiceIdentifier.NAMES.AuthScheduledTask, 'user_email_backup')
Promise.resolve(
requestBackups(
userRepository,
@@ -110,15 +102,11 @@ void container.load().then((container) => {
.then(() => {
logger.info(`Email backup requesting complete for ${backupEmail}`)
tracer.stopSpan()
process.exit(0)
})
.catch((error) => {
logger.error(`Could not finish email backup requesting for ${backupEmail}: ${error.message}`)
tracer.stopSpanWithError(error)
process.exit(1)
})
})

View File

@@ -1,11 +1,5 @@
import 'reflect-metadata'
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
import { ServiceIdentifier } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.AuthWorker })
sdk.start()
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'

View File

@@ -1,10 +1,10 @@
import Redis, { Cluster } from 'ioredis'
import { SettingName } from '@standardnotes/settings'
import { MigrationInterface, QueryRunner } from 'typeorm'
import { Setting } from '../../src/Domain/Setting/Setting'
import { User } from '../../src/Domain/User/User'
import { EncryptionVersion } from '../../src/Domain/Encryption/EncryptionVersion'
import { SettingName, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
@@ -32,19 +32,21 @@ export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterfa
usersMFAStatus.set(item['user_uuid'], 1)
usersMFAUpdatedAt.set(item['user_uuid'], item['updated_at_timestamp'])
const setting = new Setting()
setting.uuid = item['uuid']
setting.name = SettingName.NAMES.MfaSecret
setting.value = item['content']
const settingOrError = Setting.create(
{
name: SettingName.NAMES.MfaSecret,
value: item['deleted'] ? null : item['content'],
serverEncryptionVersion: EncryptionVersion.Unencrypted,
timestamps: Timestamps.create(item['created_at_timestamp'], item['updated_at_timestamp']).getValue(),
userUuid: Uuid.create(user.uuid).getValue(),
sensitive: true,
},
new UniqueEntityId(item['uuid']),
)
if (item['deleted']) {
setting.value = null
usersMFAStatus.set(item['user_uuid'], 0)
}
setting.serverEncryptionVersion = EncryptionVersion.Unencrypted
setting.createdAt = item['created_at_timestamp']
setting.updatedAt = item['updated_at_timestamp']
setting.user = Promise.resolve(user)
await queryRunner.manager.save(setting)
await queryRunner.manager.save(settingOrError.getValue())
}
const redisClient = this.getRedisClient()

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.160.0",
"version": "1.165.4",
"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,14 +75,13 @@ 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,
DirectCallEventMessageHandler,
SNSOpenTelemetryDomainEventPublisher,
SNSDomainEventPublisher,
SQSDomainEventSubscriber,
SQSEventMessageHandler,
SQSOpenTelemetryDomainEventSubscriber,
} from '@standardnotes/domain-events-infra'
import { GetUserSubscription } from '../Domain/UseCase/GetUserSubscription/GetUserSubscription'
import { ChangeCredentials } from '../Domain/UseCase/ChangeCredentials/ChangeCredentials'
@@ -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'
@@ -188,7 +170,6 @@ import {
ControllerContainer,
ControllerContainerInterface,
MapperInterface,
ServiceIdentifier,
SharedVaultUser,
} from '@standardnotes/domain-core'
import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenceMapper'
@@ -271,6 +252,29 @@ import { UserDesignatedAsSurvivorInSharedVaultEventHandler } from '../Domain/Han
import { DisableEmailSettingBasedOnEmailSubscription } from '../Domain/UseCase/DisableEmailSettingBasedOnEmailSubscription/DisableEmailSettingBasedOnEmailSubscription'
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
import { KeyParamsFactoryInterface } from '../Domain/User/KeyParamsFactoryInterface'
import { TypeORMSubscriptionSetting } from '../Infra/TypeORM/TypeORMSubscriptionSetting'
import { SetSettingValue } from '../Domain/UseCase/SetSettingValue/SetSettingValue'
import { ApplyDefaultSubscriptionSettings } from '../Domain/UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting'
import { SetSubscriptionSettingValue } from '../Domain/UseCase/SetSubscriptionSettingValue/SetSubscriptionSettingValue'
import { GetSettings } from '../Domain/UseCase/GetSettings/GetSettings'
import { GetSubscriptionSettings } from '../Domain/UseCase/GetSubscriptionSettings/GetSubscriptionSettings'
import { GetAllSettingsForUser } from '../Domain/UseCase/GetAllSettingsForUser/GetAllSettingsForUser'
import { GetRegularSubscriptionForUser } from '../Domain/UseCase/GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
import { GetSharedSubscriptionForUser } from '../Domain/UseCase/GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
import { GetSharedOrRegularSubscriptionForUser } from '../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser'
import { ProjectorInterface } from '../Projection/ProjectorInterface'
import { SettingHttpRepresentation } from '../Mapping/Http/SettingHttpRepresentation'
import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
import { SubscriptionSettingHttpRepresentation } from '../Mapping/Http/SubscriptionSettingHttpRepresentation'
import { SettingHttpMapper } from '../Mapping/Http/SettingHttpMapper'
import { SubscriptionSettingHttpMapper } from '../Mapping/Http/SubscriptionSettingHttpMapper'
import { TypeORMSetting } from '../Infra/TypeORM/TypeORMSetting'
import { SettingPersistenceMapper } from '../Mapping/Persistence/SettingPersistenceMapper'
import { SubscriptionSettingPersistenceMapper } from '../Mapping/Persistence/SubscriptionSettingPersistenceMapper'
import { ApplyDefaultSettings } from '../Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings'
import { AuthResponseFactoryResolverInterface } from '../Domain/Auth/AuthResponseFactoryResolverInterface'
import { UserInvitedToSharedVaultEventHandler } from '../Domain/Handler/UserInvitedToSharedVaultEventHandler'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -374,10 +378,7 @@ export class ContainerConfigLoader {
.toConstantValue(
isConfiguredForHomeServer
? directCallDomainEventPublisher
: new SNSOpenTelemetryDomainEventPublisher(
container.get(TYPES.Auth_SNS),
container.get(TYPES.Auth_SNS_TOPIC_ARN),
),
: new SNSDomainEventPublisher(container.get(TYPES.Auth_SNS), container.get(TYPES.Auth_SNS_TOPIC_ARN)),
)
// Mapping
@@ -401,6 +402,22 @@ export class ContainerConfigLoader {
container
.bind<MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>>(TYPES.Auth_SharedVaultUserPersistenceMapper)
.toConstantValue(new SharedVaultUserPersistenceMapper())
container
.bind<MapperInterface<Setting, SettingHttpRepresentation>>(TYPES.Auth_SettingHttpMapper)
.toConstantValue(new SettingHttpMapper())
container
.bind<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
TYPES.Auth_SubscriptionSettingHttpMapper,
)
.toConstantValue(new SubscriptionSettingHttpMapper())
container
.bind<MapperInterface<Setting, TypeORMSetting>>(TYPES.Auth_SettingPersistenceMapper)
.toConstantValue(new SettingPersistenceMapper())
container
.bind<MapperInterface<SubscriptionSetting, TypeORMSubscriptionSetting>>(
TYPES.Auth_SubscriptionSettingPersistenceMapper,
)
.toConstantValue(new SubscriptionSettingPersistenceMapper())
// ORM
container
@@ -417,14 +434,14 @@ export class ContainerConfigLoader {
.bind<Repository<Session>>(TYPES.Auth_ORMSessionRepository)
.toConstantValue(appDataSource.getRepository(Session))
container
.bind<Repository<Setting>>(TYPES.Auth_ORMSettingRepository)
.toConstantValue(appDataSource.getRepository(Setting))
.bind<Repository<TypeORMSetting>>(TYPES.Auth_ORMSettingRepository)
.toConstantValue(appDataSource.getRepository(TypeORMSetting))
container
.bind<Repository<SharedSubscriptionInvitation>>(TYPES.Auth_ORMSharedSubscriptionInvitationRepository)
.toConstantValue(appDataSource.getRepository(SharedSubscriptionInvitation))
container
.bind<Repository<SubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository)
.toConstantValue(appDataSource.getRepository(SubscriptionSetting))
.bind<Repository<TypeORMSubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository)
.toConstantValue(appDataSource.getRepository(TypeORMSubscriptionSetting))
container.bind<Repository<User>>(TYPES.Auth_ORMUserRepository).toConstantValue(appDataSource.getRepository(User))
container
.bind<Repository<UserSubscription>>(TYPES.Auth_ORMUserSubscriptionRepository)
@@ -451,10 +468,24 @@ export class ContainerConfigLoader {
.bind<RevokedSessionRepositoryInterface>(TYPES.Auth_RevokedSessionRepository)
.to(TypeORMRevokedSessionRepository)
container.bind<UserRepositoryInterface>(TYPES.Auth_UserRepository).to(TypeORMUserRepository)
container.bind<SettingRepositoryInterface>(TYPES.Auth_SettingRepository).to(TypeORMSettingRepository)
container
.bind<SettingRepositoryInterface>(TYPES.Auth_SettingRepository)
.toConstantValue(
new TypeORMSettingRepository(
container.get<Repository<TypeORMSetting>>(TYPES.Auth_ORMSettingRepository),
container.get<MapperInterface<Setting, TypeORMSetting>>(TYPES.Auth_SettingPersistenceMapper),
),
)
container
.bind<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository)
.to(TypeORMSubscriptionSettingRepository)
.toConstantValue(
new TypeORMSubscriptionSettingRepository(
container.get<Repository<TypeORMSubscriptionSetting>>(TYPES.Auth_ORMSubscriptionSettingRepository),
container.get<MapperInterface<SubscriptionSetting, TypeORMSubscriptionSetting>>(
TYPES.Auth_SubscriptionSettingPersistenceMapper,
),
),
)
container
.bind<OfflineSettingRepositoryInterface>(TYPES.Auth_OfflineSettingRepository)
.to(TypeORMOfflineSettingRepository)
@@ -517,13 +548,6 @@ export class ContainerConfigLoader {
container.bind<UserProjector>(TYPES.Auth_UserProjector).to(UserProjector)
container.bind<RoleProjector>(TYPES.Auth_RoleProjector).to(RoleProjector)
container.bind<PermissionProjector>(TYPES.Auth_PermissionProjector).to(PermissionProjector)
container.bind<SettingProjector>(TYPES.Auth_SettingProjector).to(SettingProjector)
container
.bind<SubscriptionSettingProjector>(TYPES.Auth_SubscriptionSettingProjector)
.to(SubscriptionSettingProjector)
// Factories
container.bind<SettingFactoryInterface>(TYPES.Auth_SettingFactory).to(SettingFactory)
// env vars
container.bind(TYPES.Auth_JWT_SECRET).toConstantValue(env.get('JWT_SECRET'))
@@ -562,16 +586,7 @@ export class ContainerConfigLoader {
.toConstantValue(env.get('DISABLE_USER_REGISTRATION', true) === 'true')
container.bind(TYPES.Auth_SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
container.bind(TYPES.Auth_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
container
.bind(TYPES.Auth_USER_SERVER_REGISTRATION_URL)
.toConstantValue(env.get('USER_SERVER_REGISTRATION_URL', true))
container.bind(TYPES.Auth_USER_SERVER_AUTH_KEY).toConstantValue(env.get('USER_SERVER_AUTH_KEY', true))
container
.bind(TYPES.Auth_USER_SERVER_CHANGE_EMAIL_URL)
.toConstantValue(env.get('USER_SERVER_CHANGE_EMAIL_URL', true))
container.bind(TYPES.Auth_SYNCING_SERVER_URL).toConstantValue(env.get('SYNCING_SERVER_URL', true))
container.bind(TYPES.Auth_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
container.bind(TYPES.Auth_PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
container
.bind(TYPES.Auth_SESSION_TRACE_DAYS_TTL)
.toConstantValue(env.get('SESSION_TRACE_DAYS_TTL', true) ? +env.get('SESSION_TRACE_DAYS_TTL', true) : 90)
@@ -654,16 +669,61 @@ export class ContainerConfigLoader {
.to(RedisSubscriptionTokenRepository)
}
// Services
container
.bind<TraceSession>(TYPES.Auth_TraceSession)
.toConstantValue(
new TraceSession(
container.get(TYPES.Auth_SessionTraceRepository),
container.get(TYPES.Auth_Timer),
container.get(TYPES.Auth_SESSION_TRACE_DAYS_TTL),
),
)
container
.bind<SelectorInterface<ProtocolVersion>>(TYPES.Auth_ProtocolVersionSelector)
.toConstantValue(new DeterministicSelector<ProtocolVersion>())
container.bind<UAParser>(TYPES.Auth_DeviceDetector).toConstantValue(new UAParser())
container.bind<SessionService>(TYPES.Auth_SessionService).to(SessionService)
container.bind<UAParserInstance>(TYPES.Auth_DeviceDetector).toConstantValue(new UAParser())
container.bind<CrypterInterface>(TYPES.Auth_Crypter).to(CrypterNode)
container
.bind<SettingCrypterInterface>(TYPES.Auth_SettingCrypter)
.toConstantValue(
new SettingCrypter(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<CrypterInterface>(TYPES.Auth_Crypter),
),
)
container
.bind<GetSetting>(TYPES.Auth_GetSetting)
.toConstantValue(
new GetSetting(
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
),
)
container
.bind<SessionService>(TYPES.Auth_SessionService)
.toConstantValue(
new SessionService(
container.get<SessionRepositoryInterface>(TYPES.Auth_SessionRepository),
container.get<EphemeralSessionRepositoryInterface>(TYPES.Auth_EphemeralSessionRepository),
container.get<RevokedSessionRepositoryInterface>(TYPES.Auth_RevokedSessionRepository),
container.get<UAParserInstance>(TYPES.Auth_DeviceDetector),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<winston.Logger>(TYPES.Auth_Logger),
container.get<number>(TYPES.Auth_ACCESS_TOKEN_AGE),
container.get<number>(TYPES.Auth_REFRESH_TOKEN_AGE),
container.get<CryptoNode>(TYPES.Auth_CryptoNode),
container.get<TraceSession>(TYPES.Auth_TraceSession),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<string[]>(TYPES.Auth_READONLY_USERS),
container.get<GetSetting>(TYPES.Auth_GetSetting),
),
)
container.bind<AuthResponseFactory20161215>(TYPES.Auth_AuthResponseFactory20161215).to(AuthResponseFactory20161215)
container.bind<AuthResponseFactory20190520>(TYPES.Auth_AuthResponseFactory20190520).to(AuthResponseFactory20190520)
container.bind<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115).to(AuthResponseFactory20200115)
container.bind<AuthResponseFactoryResolver>(TYPES.Auth_AuthResponseFactoryResolver).to(AuthResponseFactoryResolver)
container
.bind<AuthResponseFactoryResolverInterface>(TYPES.Auth_AuthResponseFactoryResolver)
.to(AuthResponseFactoryResolver)
container.bind<KeyParamsFactory>(TYPES.Auth_KeyParamsFactory).to(KeyParamsFactory)
container
.bind<TokenDecoderInterface<SessionTokenData>>(TYPES.Auth_SessionTokenDecoder)
@@ -698,12 +758,9 @@ export class ContainerConfigLoader {
.bind<AuthenticationMethodResolver>(TYPES.Auth_AuthenticationMethodResolver)
.to(AuthenticationMethodResolver)
container.bind<DomainEventFactory>(TYPES.Auth_DomainEventFactory).to(DomainEventFactory)
container.bind<AxiosInstance>(TYPES.Auth_HTTPClient).toConstantValue(axios.create())
container.bind<CrypterInterface>(TYPES.Auth_Crypter).to(CrypterNode)
container
.bind<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService)
.to(SettingsAssociationService)
container.bind<SettingDecrypterInterface>(TYPES.Auth_SettingDecrypter).to(SettingDecrypter)
container
.bind<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams)
@@ -726,21 +783,6 @@ export class ContainerConfigLoader {
),
)
container
.bind<SettingServiceInterface>(TYPES.Auth_SettingService)
.toConstantValue(
new SettingService(
container.get<SettingFactoryInterface>(TYPES.Auth_SettingFactory),
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
container.get<SettingInterpreterInterface>(TYPES.Auth_SettingInterpreter),
container.get<SettingDecrypterInterface>(TYPES.Auth_SettingDecrypter),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<SubscriptionSettingServiceInterface>(TYPES.Auth_SubscriptionSettingService)
.to(SubscriptionSettingService)
container.bind<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService).to(OfflineSettingService)
container.bind<ContentDecoderInterface>(TYPES.Auth_ContenDecoder).toConstantValue(new ContentDecoder())
container.bind<ClientServiceInterface>(TYPES.Auth_WebSocketsClientService).to(WebSocketsClientService)
@@ -749,11 +791,19 @@ export class ContainerConfigLoader {
container
.bind<SubscriptionSettingsAssociationServiceInterface>(TYPES.Auth_SubscriptionSettingsAssociationService)
.to(SubscriptionSettingsAssociationService)
container.bind<FeatureServiceInterface>(TYPES.Auth_FeatureService).to(FeatureService)
container
.bind<FeatureServiceInterface>(TYPES.Auth_FeatureService)
.toConstantValue(
new FeatureService(
container.get<RoleToSubscriptionMapInterface>(TYPES.Auth_RoleToSubscriptionMap),
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
),
)
container
.bind<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector)
.toConstantValue(new DeterministicSelector<boolean>())
container.bind<UserSubscriptionServiceInterface>(TYPES.Auth_UserSubscriptionService).to(UserSubscriptionService)
// Middleware
container.bind<SessionMiddleware>(TYPES.Auth_SessionMiddleware).to(SessionMiddleware)
@@ -780,15 +830,6 @@ export class ContainerConfigLoader {
container.bind<OfflineUserAuthMiddleware>(TYPES.Auth_OfflineUserAuthMiddleware).to(OfflineUserAuthMiddleware)
// use cases
container
.bind<TraceSession>(TYPES.Auth_TraceSession)
.toConstantValue(
new TraceSession(
container.get(TYPES.Auth_SessionTraceRepository),
container.get(TYPES.Auth_Timer),
container.get(TYPES.Auth_SESSION_TRACE_DAYS_TTL),
),
)
container
.bind<PersistStatistics>(TYPES.Auth_PersistStatistics)
.toConstantValue(
@@ -863,24 +904,65 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_FeatureService),
),
)
container
.bind<SetSettingValue>(TYPES.Auth_SetSettingValue)
.toConstantValue(
new SetSettingValue(
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
),
)
container
.bind<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes)
.toConstantValue(
new GenerateRecoveryCodes(
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_SettingService),
container.get(TYPES.Auth_SetSettingValue),
container.get(TYPES.Auth_CryptoNode),
),
)
container
.bind<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting)
.toConstantValue(
new GetSubscriptionSetting(
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
),
)
container
.bind<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue)
.toConstantValue(
new SetSubscriptionSettingValue(
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
container.get<TimerInterface>(TYPES.Auth_Timer),
),
)
container
.bind<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings)
.toConstantValue(
new ApplyDefaultSubscriptionSettings(
container.get<SubscriptionSettingsAssociationServiceInterface>(
TYPES.Auth_SubscriptionSettingsAssociationService,
),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
),
)
container
.bind<ActivatePremiumFeatures>(TYPES.Auth_ActivatePremiumFeatures)
.toConstantValue(
new ActivatePremiumFeatures(
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_UserSubscriptionRepository),
container.get(TYPES.Auth_SubscriptionSettingService),
container.get(TYPES.Auth_RoleService),
container.get(TYPES.Auth_Timer),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<TimerInterface>(TYPES.Auth_Timer),
),
)
@@ -894,47 +976,149 @@ export class ContainerConfigLoader {
container.bind<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest)
container.bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken).to(RefreshSessionToken)
container.bind<SignIn>(TYPES.Auth_SignIn).to(SignIn)
container.bind<VerifyMFA>(TYPES.Auth_VerifyMFA).to(VerifyMFA)
container
.bind<VerifyMFA>(TYPES.Auth_VerifyMFA)
.toConstantValue(
new VerifyMFA(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector),
container.get<LockRepositoryInterface>(TYPES.Auth_LockRepository),
container.get<string>(TYPES.Auth_PSEUDO_KEY_PARAMS_KEY),
container.get<AuthenticatorRepositoryInterface>(TYPES.Auth_AuthenticatorRepository),
container.get<VerifyAuthenticatorAuthenticationResponse>(
TYPES.Auth_VerifyAuthenticatorAuthenticationResponse,
),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container.bind<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts).to(ClearLoginAttempts)
container.bind<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts).to(IncreaseLoginAttempts)
container
.bind<GetUserKeyParamsRecovery>(TYPES.Auth_GetUserKeyParamsRecovery)
.toConstantValue(
new GetUserKeyParamsRecovery(
container.get(TYPES.Auth_KeyParamsFactory),
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_PKCERepository),
container.get(TYPES.Auth_SettingService),
container.get<KeyParamsFactoryInterface>(TYPES.Auth_KeyParamsFactory),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<PKCERepositoryInterface>(TYPES.Auth_PKCERepository),
container.get<GetSetting>(TYPES.Auth_GetSetting),
),
)
container.bind<UpdateUser>(TYPES.Auth_UpdateUser).to(UpdateUser)
container.bind<Register>(TYPES.Auth_Register).to(Register)
container
.bind<ApplyDefaultSettings>(TYPES.Auth_ApplyDefaultSettings)
.toConstantValue(
new ApplyDefaultSettings(
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
),
)
container
.bind<Register>(TYPES.Auth_Register)
.toConstantValue(
new Register(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<RoleRepositoryInterface>(TYPES.Auth_RoleRepository),
container.get<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115),
container.get<CrypterInterface>(TYPES.Auth_Crypter),
container.get<boolean>(TYPES.Auth_DISABLE_USER_REGISTRATION),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<ApplyDefaultSettings>(TYPES.Auth_ApplyDefaultSettings),
),
)
container.bind<GetActiveSessionsForUser>(TYPES.Auth_GetActiveSessionsForUser).to(GetActiveSessionsForUser)
container.bind<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser).to(DeleteOtherSessionsForUser)
container.bind<DeleteSessionForUser>(TYPES.Auth_DeleteSessionForUser).to(DeleteSessionForUser)
container.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials).to(ChangeCredentials)
container.bind<GetSettings>(TYPES.Auth_GetSettings).to(GetSettings)
container.bind<GetSetting>(TYPES.Auth_GetSetting).to(GetSetting)
container
.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials)
.toConstantValue(
new ChangeCredentials(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<AuthResponseFactoryResolverInterface>(TYPES.Auth_AuthResponseFactoryResolver),
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<GetSettings>(TYPES.Auth_GetSettings)
.toConstantValue(
new GetSettings(
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
),
)
container
.bind<GetSubscriptionSettings>(TYPES.Auth_GetSubscriptionSettings)
.toConstantValue(
new GetSubscriptionSettings(
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
container.get<SettingCrypterInterface>(TYPES.Auth_SettingCrypter),
),
)
container
.bind<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser)
.toConstantValue(
new GetRegularSubscriptionForUser(
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
),
)
container
.bind<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser)
.toConstantValue(
new GetSharedSubscriptionForUser(
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
),
)
container
.bind<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser)
.toConstantValue(
new GetSharedOrRegularSubscriptionForUser(
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
),
)
container
.bind<GetAllSettingsForUser>(TYPES.Auth_GetAllSettingsForUser)
.toConstantValue(
new GetAllSettingsForUser(
container.get<GetSettings>(TYPES.Auth_GetSettings),
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
container.get<GetSubscriptionSettings>(TYPES.Auth_GetSubscriptionSettings),
),
)
container.bind<GetUserFeatures>(TYPES.Auth_GetUserFeatures).to(GetUserFeatures)
container.bind<UpdateSetting>(TYPES.Auth_UpdateSetting).to(UpdateSetting)
container.bind<DeleteSetting>(TYPES.Auth_DeleteSetting).to(DeleteSetting)
container
.bind<SignInWithRecoveryCodes>(TYPES.Auth_SignInWithRecoveryCodes)
.toConstantValue(
new SignInWithRecoveryCodes(
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_AuthResponseFactory20200115),
container.get(TYPES.Auth_PKCERepository),
container.get(TYPES.Auth_Crypter),
container.get(TYPES.Auth_SettingService),
container.get(TYPES.Auth_GenerateRecoveryCodes),
container.get(TYPES.Auth_IncreaseLoginAttempts),
container.get(TYPES.Auth_ClearLoginAttempts),
container.get(TYPES.Auth_DeleteSetting),
container.get(TYPES.Auth_AuthenticatorRepository),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<AuthResponseFactory20200115>(TYPES.Auth_AuthResponseFactory20200115),
container.get<PKCERepositoryInterface>(TYPES.Auth_PKCERepository),
container.get<CrypterInterface>(TYPES.Auth_Crypter),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes),
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
container.get<AuthenticatorRepositoryInterface>(TYPES.Auth_AuthenticatorRepository),
),
)
container
.bind<DeleteAccount>(TYPES.Auth_DeleteAccount)
.toConstantValue(
new DeleteAccount(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<TimerInterface>(TYPES.Auth_Timer),
),
)
container.bind<DeleteAccount>(TYPES.Auth_DeleteAccount).to(DeleteAccount)
container.bind<GetUserSubscription>(TYPES.Auth_GetUserSubscription).to(GetUserSubscription)
container.bind<GetUserOfflineSubscription>(TYPES.Auth_GetUserOfflineSubscription).to(GetUserOfflineSubscription)
container.bind<CreateSubscriptionToken>(TYPES.Auth_CreateSubscriptionToken).to(CreateSubscriptionToken)
@@ -947,12 +1131,38 @@ export class ContainerConfigLoader {
container
.bind<CreateOfflineSubscriptionToken>(TYPES.Auth_CreateOfflineSubscriptionToken)
.to(CreateOfflineSubscriptionToken)
container.bind<CreateValetToken>(TYPES.Auth_CreateValetToken).to(CreateValetToken)
container
.bind<CreateValetToken>(TYPES.Auth_CreateValetToken)
.toConstantValue(
new CreateValetToken(
container.get<TokenEncoderInterface<ValetTokenData>>(TYPES.Auth_ValetTokenEncoder),
container.get<SubscriptionSettingsAssociationServiceInterface>(
TYPES.Auth_SubscriptionSettingsAssociationService,
),
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<number>(TYPES.Auth_VALET_TOKEN_TTL),
),
)
container.bind<CreateListedAccount>(TYPES.Auth_CreateListedAccount).to(CreateListedAccount)
container.bind<InviteToSharedSubscription>(TYPES.Auth_InviteToSharedSubscription).to(InviteToSharedSubscription)
container
.bind<AcceptSharedSubscriptionInvitation>(TYPES.Auth_AcceptSharedSubscriptionInvitation)
.to(AcceptSharedSubscriptionInvitation)
.toConstantValue(
new AcceptSharedSubscriptionInvitation(
container.get<SharedSubscriptionInvitationRepositoryInterface>(
TYPES.Auth_SharedSubscriptionInvitationRepository,
),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<DeclineSharedSubscriptionInvitation>(TYPES.Auth_DeclineSharedSubscriptionInvitation)
.to(DeclineSharedSubscriptionInvitation)
@@ -963,15 +1173,32 @@ export class ContainerConfigLoader {
.bind<ListSharedSubscriptionInvitations>(TYPES.Auth_ListSharedSubscriptionInvitations)
.to(ListSharedSubscriptionInvitations)
container.bind<VerifyPredicate>(TYPES.Auth_VerifyPredicate).to(VerifyPredicate)
container.bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken).to(CreateCrossServiceToken)
container
.bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken)
.toConstantValue(
new CreateCrossServiceToken(
container.get<ProjectorInterface<User>>(TYPES.Auth_UserProjector),
container.get<ProjectorInterface<Session>>(TYPES.Auth_SessionProjector),
container.get<ProjectorInterface<Role>>(TYPES.Auth_RoleProjector),
container.get<TokenEncoderInterface<CrossServiceTokenData>>(TYPES.Auth_CrossServiceTokenEncoder),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<number>(TYPES.Auth_AUTH_JWT_TTL),
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
container.get<SharedVaultUserRepositoryInterface>(TYPES.Auth_SharedVaultUserRepository),
),
)
container.bind<ProcessUserRequest>(TYPES.Auth_ProcessUserRequest).to(ProcessUserRequest)
container
.bind<UpdateStorageQuotaUsedForUser>(TYPES.Auth_UpdateStorageQuotaUsedForUser)
.toConstantValue(
new UpdateStorageQuotaUsedForUser(
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_UserSubscriptionService),
container.get(TYPES.Auth_SubscriptionSettingService),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
@@ -999,8 +1226,9 @@ export class ContainerConfigLoader {
.toConstantValue(
new DisableEmailSettingBasedOnEmailSubscription(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<SettingRepositoryInterface>(TYPES.Auth_SettingRepository),
container.get<SettingFactoryInterface>(TYPES.Auth_SettingFactory),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
),
)
@@ -1041,7 +1269,6 @@ export class ContainerConfigLoader {
container.bind<UserRequestsController>(TYPES.Auth_UserRequestsController).to(UserRequestsController)
// Handlers
container.bind<UserRegisteredEventHandler>(TYPES.Auth_UserRegisteredEventHandler).to(UserRegisteredEventHandler)
container
.bind<AccountDeletionRequestedEventHandler>(TYPES.Auth_AccountDeletionRequestedEventHandler)
.toConstantValue(
@@ -1055,7 +1282,16 @@ export class ContainerConfigLoader {
)
container
.bind<SubscriptionPurchasedEventHandler>(TYPES.Auth_SubscriptionPurchasedEventHandler)
.to(SubscriptionPurchasedEventHandler)
.toConstantValue(
new SubscriptionPurchasedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<SubscriptionCancelledEventHandler>(TYPES.Auth_SubscriptionCancelledEventHandler)
.to(SubscriptionCancelledEventHandler)
@@ -1070,16 +1306,42 @@ export class ContainerConfigLoader {
.to(SubscriptionExpiredEventHandler)
container
.bind<SubscriptionSyncRequestedEventHandler>(TYPES.Auth_SubscriptionSyncRequestedEventHandler)
.to(SubscriptionSyncRequestedEventHandler)
.toConstantValue(
new SubscriptionSyncRequestedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<ExtensionKeyGrantedEventHandler>(TYPES.Auth_ExtensionKeyGrantedEventHandler)
.to(ExtensionKeyGrantedEventHandler)
.toConstantValue(
new ExtensionKeyGrantedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<SubscriptionReassignedEventHandler>(TYPES.Auth_SubscriptionReassignedEventHandler)
.to(SubscriptionReassignedEventHandler)
container
.bind<UserEmailChangedEventHandler>(TYPES.Auth_UserEmailChangedEventHandler)
.to(UserEmailChangedEventHandler)
.toConstantValue(
new SubscriptionReassignedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<winston.Logger>(TYPES.Auth_Logger),
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
),
)
container
.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler)
.toConstantValue(
@@ -1122,10 +1384,24 @@ export class ContainerConfigLoader {
)
container
.bind<ListedAccountCreatedEventHandler>(TYPES.Auth_ListedAccountCreatedEventHandler)
.to(ListedAccountCreatedEventHandler)
.toConstantValue(
new ListedAccountCreatedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<ListedAccountDeletedEventHandler>(TYPES.Auth_ListedAccountDeletedEventHandler)
.to(ListedAccountDeletedEventHandler)
.toConstantValue(
new ListedAccountDeletedEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<UserDisabledSessionUserAgentLoggingEventHandler>(TYPES.Auth_UserDisabledSessionUserAgentLoggingEventHandler)
.to(UserDisabledSessionUserAgentLoggingEventHandler)
@@ -1180,9 +1456,17 @@ export class ContainerConfigLoader {
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<UserInvitedToSharedVaultEventHandler>(TYPES.Auth_UserInvitedToSharedVaultEventHandler)
.toConstantValue(
new UserInvitedToSharedVaultEventHandler(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
['SUBSCRIPTION_PURCHASED', container.get(TYPES.Auth_SubscriptionPurchasedEventHandler)],
['SUBSCRIPTION_CANCELLED', container.get(TYPES.Auth_SubscriptionCancelledEventHandler)],
@@ -1192,7 +1476,6 @@ export class ContainerConfigLoader {
['SUBSCRIPTION_SYNC_REQUESTED', container.get(TYPES.Auth_SubscriptionSyncRequestedEventHandler)],
['EXTENSION_KEY_GRANTED', container.get(TYPES.Auth_ExtensionKeyGrantedEventHandler)],
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.Auth_SubscriptionReassignedEventHandler)],
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)],
@@ -1217,6 +1500,7 @@ export class ContainerConfigLoader {
'USER_DESIGNATED_AS_SURVIVOR_IN_SHARED_VAULT',
container.get(TYPES.Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler),
],
['USER_INVITED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserInvitedToSharedVaultEventHandler)],
])
if (isConfiguredForHomeServer) {
@@ -1236,8 +1520,7 @@ export class ContainerConfigLoader {
container
.bind<DomainEventSubscriberInterface>(TYPES.Auth_DomainEventSubscriber)
.toConstantValue(
new SQSOpenTelemetryDomainEventSubscriber(
ServiceIdentifier.NAMES.AuthWorker,
new SQSDomainEventSubscriber(
container.get<SQSClient>(TYPES.Auth_SQS),
container.get<string>(TYPES.Auth_SQS_QUEUE_URL),
container.get<DomainEventMessageHandlerInterface>(TYPES.Auth_DomainEventMessageHandler),
@@ -1343,33 +1626,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'),
@@ -65,10 +69,6 @@ const TYPES = {
Auth_UserProjector: Symbol.for('Auth_UserProjector'),
Auth_RoleProjector: Symbol.for('Auth_RoleProjector'),
Auth_PermissionProjector: Symbol.for('Auth_PermissionProjector'),
Auth_SettingProjector: Symbol.for('Auth_SettingProjector'),
Auth_SubscriptionSettingProjector: Symbol.for('Auth_SubscriptionSettingProjector'),
// Factories
Auth_SettingFactory: Symbol.for('Auth_SettingFactory'),
// env vars
Auth_JWT_SECRET: Symbol.for('Auth_JWT_SECRET'),
Auth_LEGACY_JWT_SECRET: Symbol.for('Auth_LEGACY_JWT_SECRET'),
@@ -91,12 +91,7 @@ const TYPES = {
Auth_SNS_AWS_REGION: Symbol.for('Auth_SNS_AWS_REGION'),
Auth_SQS_QUEUE_URL: Symbol.for('Auth_SQS_QUEUE_URL'),
Auth_SQS_AWS_REGION: Symbol.for('Auth_SQS_AWS_REGION'),
Auth_USER_SERVER_REGISTRATION_URL: Symbol.for('Auth_USER_SERVER_REGISTRATION_URL'),
Auth_USER_SERVER_AUTH_KEY: Symbol.for('Auth_USER_SERVER_AUTH_KEY'),
Auth_USER_SERVER_CHANGE_EMAIL_URL: Symbol.for('Auth_USER_SERVER_CHANGE_EMAIL_URL'),
Auth_SYNCING_SERVER_URL: Symbol.for('Auth_SYNCING_SERVER_URL'),
Auth_VERSION: Symbol.for('Auth_VERSION'),
Auth_PAYMENTS_SERVER_URL: Symbol.for('Auth_PAYMENTS_SERVER_URL'),
Auth_SESSION_TRACE_DAYS_TTL: Symbol.for('Auth_SESSION_TRACE_DAYS_TTL'),
Auth_U2F_RELYING_PARTY_ID: Symbol.for('Auth_U2F_RELYING_PARTY_ID'),
Auth_U2F_RELYING_PARTY_NAME: Symbol.for('Auth_U2F_RELYING_PARTY_NAME'),
@@ -120,9 +115,12 @@ const TYPES = {
Auth_DeleteSessionForUser: Symbol.for('Auth_DeleteSessionForUser'),
Auth_ChangeCredentials: Symbol.for('Auth_ChangePassword'),
Auth_GetSettings: Symbol.for('Auth_GetSettings'),
Auth_GetSubscriptionSettings: Symbol.for('Auth_GetSubscriptionSettings'),
Auth_GetRegularSubscriptionForUser: Symbol.for('Auth_GetRegularSubscriptionForUser'),
Auth_GetSharedSubscriptionForUser: Symbol.for('Auth_GetSharedSubscriptionForUser'),
Auth_GetAllSettingsForUser: Symbol.for('Auth_GetAllSettingsForUser'),
Auth_GetSetting: Symbol.for('Auth_GetSetting'),
Auth_GetUserFeatures: Symbol.for('Auth_GetUserFeatures'),
Auth_UpdateSetting: Symbol.for('Auth_UpdateSetting'),
Auth_DeleteSetting: Symbol.for('Auth_DeleteSetting'),
Auth_DeleteAccount: Symbol.for('Auth_DeleteAccount'),
Auth_GetUserSubscription: Symbol.for('Auth_GetUserSubscription'),
@@ -151,7 +149,12 @@ const TYPES = {
Auth_VerifyAuthenticatorAuthenticationResponse: Symbol.for('Auth_VerifyAuthenticatorAuthenticationResponse'),
Auth_ListAuthenticators: Symbol.for('Auth_ListAuthenticators'),
Auth_DeleteAuthenticator: Symbol.for('Auth_DeleteAuthenticator'),
Auth_SetSettingValue: Symbol.for('Auth_SetSettingValue'),
Auth_GenerateRecoveryCodes: Symbol.for('Auth_GenerateRecoveryCodes'),
Auth_GetSubscriptionSetting: Symbol.for('Auth_GetSubscriptionSetting'),
Auth_SetSubscriptionSettingValue: Symbol.for('Auth_SetSubscriptionSettingValue'),
Auth_ApplyDefaultSubscriptionSettings: Symbol.for('Auth_ApplyDefaultSubscriptionSettings'),
Auth_ApplyDefaultSettings: Symbol.for('Auth_ApplyDefaultSettings'),
Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'),
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
@@ -159,9 +162,9 @@ const TYPES = {
Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'),
Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'),
Auth_DesignateSurvivor: Symbol.for('Auth_DesignateSurvivor'),
Auth_GetSharedOrRegularSubscriptionForUser: Symbol.for('Auth_GetSharedOrRegularSubscriptionForUser'),
Auth_DisableEmailSettingBasedOnEmailSubscription: Symbol.for('Auth_DisableEmailSettingBasedOnEmailSubscription'),
// Handlers
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
Auth_SubscriptionPurchasedEventHandler: Symbol.for('Auth_SubscriptionPurchasedEventHandler'),
Auth_SubscriptionCancelledEventHandler: Symbol.for('Auth_SubscriptionCancelledEventHandler'),
@@ -171,7 +174,6 @@ const TYPES = {
Auth_SubscriptionExpiredEventHandler: Symbol.for('Auth_SubscriptionExpiredEventHandler'),
Auth_SubscriptionSyncRequestedEventHandler: Symbol.for('Auth_SubscriptionSyncRequestedEventHandler'),
Auth_ExtensionKeyGrantedEventHandler: Symbol.for('Auth_ExtensionKeyGrantedEventHandler'),
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
@@ -193,11 +195,10 @@ const TYPES = {
Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler: Symbol.for(
'Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler',
),
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
// Services
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
Auth_SessionService: Symbol.for('Auth_SessionService'),
Auth_SettingService: Symbol.for('Auth_SettingService'),
Auth_SubscriptionSettingService: Symbol.for('Auth_SubscriptionSettingService'),
Auth_OfflineSettingService: Symbol.for('Auth_OfflineSettingService'),
Auth_AuthResponseFactory20161215: Symbol.for('Auth_AuthResponseFactory20161215'),
Auth_AuthResponseFactory20190520: Symbol.for('Auth_AuthResponseFactory20190520'),
@@ -218,7 +219,6 @@ const TYPES = {
Auth_DomainEventSubscriber: Symbol.for('Auth_DomainEventSubscriber'),
Auth_DomainEventFactory: Symbol.for('Auth_DomainEventFactory'),
Auth_DomainEventMessageHandler: Symbol.for('Auth_DomainEventMessageHandler'),
Auth_HTTPClient: Symbol.for('Auth_HTTPClient'),
Auth_Crypter: Symbol.for('Auth_Crypter'),
Auth_CryptoNode: Symbol.for('Auth_CryptoNode'),
Auth_Timer: Symbol.for('Auth_Timer'),
@@ -229,11 +229,10 @@ const TYPES = {
Auth_SettingsAssociationService: Symbol.for('Auth_SettingsAssociationService'),
Auth_SubscriptionSettingsAssociationService: Symbol.for('Auth_SubscriptionSettingsAssociationService'),
Auth_FeatureService: Symbol.for('Auth_FeatureService'),
Auth_SettingDecrypter: Symbol.for('Auth_SettingDecrypter'),
Auth_SettingCrypter: Symbol.for('Auth_SettingCrypter'),
Auth_SettingInterpreter: Symbol.for('Auth_SettingInterpreter'),
Auth_ProtocolVersionSelector: Symbol.for('Auth_ProtocolVersionSelector'),
Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'),
Auth_UserSubscriptionService: Symbol.for('Auth_UserSubscriptionService'),
Auth_BaseAuthController: Symbol.for('Auth_BaseAuthController'),
Auth_BaseAuthenticatorsController: Symbol.for('Auth_BaseAuthenticatorsController'),
Auth_BaseSubscriptionInvitesController: Symbol.for('Auth_BaseSubscriptionInvitesController'),

View File

@@ -0,0 +1,9 @@
import { html } from './user-email-changed.html'
export function getSubject(): string {
return 'Confirmation: Your Email Address Has Been Successfully Updated'
}
export function getBody(newEmail: string): string {
return html(newEmail)
}

View File

@@ -0,0 +1,9 @@
import { html } from './user-invited-to-shared-vault.html'
export function getSubject(): string {
return "You're Invited to a Shared Vault!"
}
export function getBody(): string {
return html()
}

View File

@@ -0,0 +1,14 @@
export const html = (newEmail: string) => `
<p>Hello,</p>
<p>We are writing to inform you that your request to update your email address has been successfully processed. The email address associated with your Standard Notes account has now been changed to the following:</p>
<p>New Email Address: ${newEmail}</p>
<p>From now on, you can log in to your account using your new email address. Your password and all other account details remain the same. If you did not initiate this change or have any concerns about this update, please contact our support team by visiting our <a href="https://standardnotes.com/help">help page</a>
or by replying directly to this email.</p>
<p>Best regards,</p>
<p>
Standard Notes
</p>
`

View File

@@ -0,0 +1,21 @@
export const html = () => `
<p>Hello,</p>
<p>You've been invited to join a shared vault. This shared workspace will help you collaborate and securely manage notes and files.</p>
<p>To accept this invitation and access the shared vault, please follow these steps:</p>
<ol>
<li>Go to your account settings.</li>
<li>Navigate to the "Vaults" section.</li>
<li>You will find the invitation there — simply click to accept.</li>
</ol>
<p>If you have any questions, please contact our support team by visiting our <a href="https://standardnotes.com/help">help page</a>
or by replying directly to this email.</p>
<p>Best regards,</p>
<p>
Standard Notes
</p>
`

View File

@@ -281,9 +281,16 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
createAccountDeletionRequestedEvent(dto: {
userUuid: string
email: string
userCreatedAtTimestamp: number
regularSubscriptionUuid: string | undefined
roleNames: string[]
regularSubscription?: {
uuid: string
ownerUuid: string
}
sharedSubscription?: {
uuid: string
ownerUuid: string
}
}): AccountDeletionRequestedEvent {
return {
type: 'ACCOUNT_DELETION_REQUESTED',

View File

@@ -45,9 +45,16 @@ export interface DomainEventFactoryInterface {
): EmailBackupRequestedEvent
createAccountDeletionRequestedEvent(dto: {
userUuid: string
email: string
userCreatedAtTimestamp: number
regularSubscriptionUuid: string | undefined
roleNames: string[]
regularSubscription?: {
uuid: string
ownerUuid: string
}
sharedSubscription?: {
uuid: string
ownerUuid: string
}
}): AccountDeletionRequestedEvent
createUserRolesChangedEvent(userUuid: string, email: string, currentRoles: string[]): UserRolesChangedEvent
createUserEmailChangedEvent(userUuid: string, fromEmail: string, toEmail: string): UserEmailChangedEvent

View File

@@ -35,6 +35,7 @@ import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/Offl
import { TimerInterface } from '@standardnotes/time'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
describe('FeatureService', () => {
let roleToSubscriptionMap: RoleToSubscriptionMapInterface
@@ -52,8 +53,10 @@ describe('FeatureService', () => {
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
let timer: TimerInterface
let offlineUserSubscription: OfflineUserSubscription
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
const createService = () => new FeatureService(roleToSubscriptionMap, offlineUserSubscriptionRepository, timer)
const createService = () =>
new FeatureService(roleToSubscriptionMap, offlineUserSubscriptionRepository, timer, userSubscriptionRepository)
beforeEach(() => {
roleToSubscriptionMap = {} as jest.Mocked<RoleToSubscriptionMapInterface>
@@ -107,11 +110,10 @@ describe('FeatureService', () => {
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 555,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: false,
subscriptionId: 1,
subscriptionType: UserSubscriptionType.Regular,
subscriptionSettings: Promise.resolve([]),
}
subscription2 = {
@@ -121,11 +123,10 @@ describe('FeatureService', () => {
renewedAt: null,
planName: SubscriptionName.ProPlan,
endsAt: 777,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: false,
subscriptionId: 2,
subscriptionType: UserSubscriptionType.Regular,
subscriptionSettings: Promise.resolve([]),
}
subscription3 = {
@@ -135,11 +136,10 @@ describe('FeatureService', () => {
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 333,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: true,
subscriptionId: 3,
subscriptionType: UserSubscriptionType.Regular,
subscriptionSettings: Promise.resolve([]),
}
subscription4 = {
@@ -149,19 +149,20 @@ describe('FeatureService', () => {
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 333,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: true,
subscriptionId: 4,
subscriptionType: UserSubscriptionType.Regular,
subscriptionSettings: Promise.resolve([]),
}
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve([subscription1]),
} as jest.Mocked<User>
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1])
offlineUserSubscription = {
roles: Promise.resolve([role1]),
uuid: 'subscription-1-1-1',
@@ -251,9 +252,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2, nonSubscriptionRole]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
expect(await createService().userIsEntitledToFeature(user, 'files-beta')).toBe(true)
})
@@ -273,9 +275,12 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve([subscription3, subscription1, subscription4]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest
.fn()
.mockReturnValue([subscription3, subscription1, subscription4])
const features = await createService().getFeaturesForUser(user)
expect(features).toEqual(
expect.arrayContaining([
@@ -288,14 +293,13 @@ describe('FeatureService', () => {
})
it('should not return user features if a subscription could not be found', async () => {
const subscriptions: Array<UserSubscription> = []
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve(subscriptions),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([])
expect(await createService().getFeaturesForUser(user)).toEqual([])
})
@@ -311,9 +315,12 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve([subscription3, subscription1, subscription4]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest
.fn()
.mockReturnValue([subscription3, subscription1, subscription4])
expect(await createService().getFeaturesForUser(user)).toEqual([])
})
@@ -325,19 +332,19 @@ describe('FeatureService', () => {
renewedAt: null,
planName: 'non existing plan name' as SubscriptionName,
endsAt: 555,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: false,
subscriptionId: 1,
subscriptionType: UserSubscriptionType.Regular,
subscriptionSettings: Promise.resolve([]),
}
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve([subscription1]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1])
expect(await createService().getFeaturesForUser(user)).toEqual([])
})
@@ -356,9 +363,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
const features = await createService().getFeaturesForUser(user)
expect(features).toEqual(
expect.arrayContaining([
@@ -414,9 +422,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2, nonSubscriptionRole]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
const features = await createService().getFeaturesForUser(user)
expect(features).toEqual(
expect.arrayContaining([
@@ -450,9 +459,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
const longestExpireAt = 777
const features = await createService().getFeaturesForUser(user)
@@ -487,9 +497,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
const features = await createService().getFeaturesForUser(user)
expect(features).toEqual(
expect.arrayContaining([

View File

@@ -1,24 +1,22 @@
import { SubscriptionName } from '@standardnotes/common'
import { FeatureDescription, GetFeatures } from '@standardnotes/features'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface'
import { TimerInterface } from '@standardnotes/time'
import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface'
import { User } from '../User/User'
import { UserSubscription } from '../Subscription/UserSubscription'
import { FeatureServiceInterface } from './FeatureServiceInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Role } from '../Role/Role'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { TimerInterface } from '@standardnotes/time'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
@injectable()
export class FeatureService implements FeatureServiceInterface {
constructor(
@inject(TYPES.Auth_RoleToSubscriptionMap) private roleToSubscriptionMap: RoleToSubscriptionMapInterface,
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
private roleToSubscriptionMap: RoleToSubscriptionMapInterface,
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
private timer: TimerInterface,
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
) {}
async userIsEntitledToFeature(user: User, featureIdentifier: string): Promise<boolean> {
@@ -61,7 +59,7 @@ export class FeatureService implements FeatureServiceInterface {
}
async getFeaturesForUser(user: User): Promise<Array<FeatureDescription>> {
const userSubscriptions = await user.subscriptions
const userSubscriptions = await this.userSubscriptionRepository.findByUserUuid(user.uuid)
return this.getFeaturesForSubscriptions(userSubscriptions, await user.roles)
}

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,21 @@
import { DomainEventHandlerInterface, ExtensionKeyGrantedEvent } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/settings'
import { SettingName, Username } from '@standardnotes/domain-core'
import { OfflineFeaturesTokenData } from '@standardnotes/security'
import { ContentDecoderInterface } from '@standardnotes/common'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface'
import { OfflineSettingName } from '../Setting/OfflineSettingName'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { Username } from '@standardnotes/domain-core'
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
@injectable()
export class ExtensionKeyGrantedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_OfflineSettingService) private offlineSettingService: OfflineSettingServiceInterface,
@inject(TYPES.Auth_ContenDecoder) private contentDecoder: ContentDecoderInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
private userRepository: UserRepositoryInterface,
private setSettingValue: SetSettingValue,
private offlineSettingService: OfflineSettingServiceInterface,
private contentDecoder: ContentDecoderInterface,
private logger: Logger,
) {}
async handle(event: ExtensionKeyGrantedEvent): Promise<void> {
@@ -58,14 +53,14 @@ export class ExtensionKeyGrantedEventHandler implements DomainEventHandlerInterf
return
}
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: event.payload.extensionKey,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true,
},
const result = await this.setSettingValue.execute({
userUuid: user.uuid,
settingName: SettingName.NAMES.ExtensionKey,
value: event.payload.extensionKey,
})
if (result.isFailed()) {
this.logger.error(`Could not set extension key for user ${user.uuid}`)
}
}
}

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 { SettingName, Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, ListedAccountCreatedEvent } from '@standardnotes/domain-events'
import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { ListedAuthorSecretsData } from '@standardnotes/settings'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { GetSetting } from '../UseCase/GetSetting/GetSetting'
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
@injectable()
export class ListedAccountCreatedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
private userRepository: UserRepositoryInterface,
private getSetting: GetSetting,
private setSettingValue: SetSettingValue,
private logger: Logger,
) {}
async handle(event: ListedAccountCreatedEvent): Promise<void> {
@@ -34,23 +33,27 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter
let authSecrets: ListedAuthorSecretsData = [newSecret]
const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(),
const listedAuthorSecretsSettingOrError = await this.getSetting.execute({
settingName: SettingName.NAMES.ListedAuthorSecrets,
userUuid: user.uuid,
decrypted: true,
allowSensitiveRetrieval: false,
})
if (listedAuthorSecretsSetting !== null) {
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.value as string)
if (!listedAuthorSecretsSettingOrError.isFailed()) {
const listedAuthorSecretsSetting = listedAuthorSecretsSettingOrError.getValue()
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.decryptedValue as string)
existingSecrets.push(newSecret)
authSecrets = existingSecrets
}
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.NAMES.ListedAuthorSecrets,
unencryptedValue: JSON.stringify(authSecrets),
sensitive: false,
},
const result = await this.setSettingValue.execute({
userUuid: user.uuid,
settingName: SettingName.NAMES.ListedAuthorSecrets,
value: JSON.stringify(authSecrets),
})
if (result.isFailed()) {
this.logger.error(`Could not update listed author secrets for user with uuid ${user.uuid}: ${result.getError()}`)
}
}
}

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 { SettingName, Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, ListedAccountDeletedEvent } from '@standardnotes/domain-events'
import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { ListedAuthorSecretsData } from '@standardnotes/settings'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { GetSetting } from '../UseCase/GetSetting/GetSetting'
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
@injectable()
export class ListedAccountDeletedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
private userRepository: UserRepositoryInterface,
private getSetting: GetSetting,
private setSettingValue: SetSettingValue,
private logger: Logger,
) {}
async handle(event: ListedAccountDeletedEvent): Promise<void> {
@@ -31,30 +30,35 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter
return
}
const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(),
const listedAuthorSecretsSettingOrError = await this.getSetting.execute({
settingName: SettingName.NAMES.ListedAuthorSecrets,
decrypted: true,
userUuid: user.uuid,
allowSensitiveRetrieval: false,
})
if (listedAuthorSecretsSetting === null) {
this.logger.warn(`Could not find listed secrets setting for user ${user.uuid}`)
if (listedAuthorSecretsSettingOrError.isFailed()) {
this.logger.error(`Could not find listed secrets setting for user ${user.uuid}`)
return
}
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.value as string)
const listedAuthorSecretsSetting = listedAuthorSecretsSettingOrError.getValue()
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.decryptedValue as string)
const filteredSecrets = existingSecrets.filter(
(secret) =>
secret.authorId !== event.payload.userId ||
(secret.authorId === event.payload.userId && secret.hostUrl !== event.payload.hostUrl),
)
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.NAMES.ListedAuthorSecrets,
unencryptedValue: JSON.stringify(filteredSecrets),
sensitive: false,
},
const result = await this.setSettingValue.execute({
settingName: SettingName.NAMES.ListedAuthorSecrets,
value: JSON.stringify(filteredSecrets),
userUuid: user.uuid,
})
if (result.isFailed()) {
this.logger.error(`Could not update listed author secrets for user with uuid ${user.uuid}`)
}
}
}

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

@@ -7,7 +7,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Username } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
@injectable()
export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterface {
@@ -48,7 +48,22 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
const userUuidOrError = Uuid.create(userSubscription.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(`Could not remove role from user with uuid: ${userUuidOrError.getError()}`)
continue
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
continue
}
await this.roleService.removeUserRoleBasedOnSubscription(user, subscriptionName)
}
}

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> {
@@ -82,7 +84,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
): Promise<UserSubscription> {
const subscription = new UserSubscription()
subscription.planName = subscriptionName
subscription.user = Promise.resolve(user)
subscription.userUuid = user.uuid
subscription.createdAt = timestamp
subscription.updatedAt = timestamp
subscription.endsAt = subscriptionExpiresAt

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,24 @@
import { DomainEventHandlerInterface, SubscriptionReassignedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { SettingName } from '@standardnotes/settings'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { Username } from '@standardnotes/domain-core'
import { SettingName, Username } from '@standardnotes/domain-core'
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
@injectable()
export class SubscriptionReassignedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_UserSubscriptionRepository)
private userRepository: UserRepositoryInterface,
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
@inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_SubscriptionSettingService)
private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
private roleService: RoleServiceInterface,
private logger: Logger,
private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings,
private setSettingValue: SetSettingValue,
) {}
async handle(event: SubscriptionReassignedEvent): Promise<void> {
@@ -53,17 +46,25 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
await this.addUserRole(user, event.payload.subscriptionName)
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: event.payload.extensionKey,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true,
},
const result = await this.setSettingValue.execute({
userUuid: user.uuid,
settingName: SettingName.NAMES.ExtensionKey,
value: event.payload.extensionKey,
})
if (result.isFailed()) {
this.logger.error(`Could not set extension key for user ${user.uuid}`)
}
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({
subscriptionPlanName: event.payload.subscriptionName,
userUuid: user.uuid,
userSubscriptionUuid: userSubscription.uuid,
})
if (applyingSettingsResult.isFailed()) {
this.logger.error(
`Could not apply default subscription settings for user ${user.uuid}: ${applyingSettingsResult.getError()}`,
)
}
}
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
@@ -79,7 +80,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
): Promise<UserSubscription> {
const subscription = new UserSubscription()
subscription.planName = subscriptionName
subscription.user = Promise.resolve(user)
subscription.userUuid = user.uuid
subscription.createdAt = timestamp
subscription.updatedAt = timestamp
subscription.endsAt = subscriptionExpiresAt

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

@@ -7,7 +7,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Username } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
@injectable()
export class SubscriptionRefundedEventHandler implements DomainEventHandlerInterface {
@@ -48,7 +48,22 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
const userUuidOrError = Uuid.create(userSubscription.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(`Could not remove role from user with uuid: ${userUuidOrError.getError()}`)
continue
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
continue
}
await this.roleService.removeUserRoleBasedOnSubscription(user, subscriptionName)
}
}

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

@@ -8,7 +8,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { Logger } from 'winston'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { Username } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
@injectable()
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
@@ -71,7 +71,20 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
private async addRoleToSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
const user = await userSubscription.user
const userUuidOrError = Uuid.create(userSubscription.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(`Could not add role to user with uuid: ${userUuidOrError.getError()}`)
continue
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
continue
}
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}

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,9 @@
import { OfflineFeaturesTokenData } from '@standardnotes/security'
import { SettingName, Username } from '@standardnotes/domain-core'
import { ContentDecoderInterface } from '@standardnotes/common'
import { DomainEventHandlerInterface, SubscriptionSyncRequestedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
@@ -11,31 +11,23 @@ import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface'
import { ContentDecoderInterface } from '@standardnotes/common'
import { OfflineSettingName } from '../Setting/OfflineSettingName'
import { SettingName } from '@standardnotes/settings'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { Username } from '@standardnotes/domain-core'
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
@injectable()
export class SubscriptionSyncRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_UserSubscriptionRepository)
private userRepository: UserRepositoryInterface,
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
@inject(TYPES.Auth_RoleService) private roleService: RoleServiceInterface,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_SubscriptionSettingService)
private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.Auth_OfflineSettingService) private offlineSettingService: OfflineSettingServiceInterface,
@inject(TYPES.Auth_ContenDecoder) private contentDecoder: ContentDecoderInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
private roleService: RoleServiceInterface,
private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings,
private setSettingValue: SetSettingValue,
private offlineSettingService: OfflineSettingServiceInterface,
private contentDecoder: ContentDecoderInterface,
private logger: Logger,
) {}
async handle(event: SubscriptionSyncRequestedEvent): Promise<void> {
@@ -95,17 +87,26 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: event.payload.extensionKey,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true,
},
const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({
userSubscriptionUuid: userSubscription.uuid,
userUuid: user.uuid,
subscriptionPlanName: event.payload.subscriptionName,
})
if (applyingSettingsResult.isFailed()) {
this.logger.error(
`Could not apply default subscription settings for user ${user.uuid}: ${applyingSettingsResult.getError()}`,
)
}
const result = await this.setSettingValue.execute({
userUuid: user.uuid,
settingName: SettingName.NAMES.ExtensionKey,
value: event.payload.subscriptionName,
})
if (result.isFailed()) {
this.logger.error(`Could not set extension key for user ${user.uuid}`)
}
}
private async createOrUpdateSubscription(
@@ -127,7 +128,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
}
subscription.planName = subscriptionName
subscription.user = Promise.resolve(user)
subscription.userUuid = user.uuid
subscription.createdAt = timestamp
subscription.updatedAt = timestamp
subscription.endsAt = subscriptionExpiresAt

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

@@ -0,0 +1,41 @@
import {
DomainEventHandlerInterface,
DomainEventPublisherInterface,
UserInvitedToSharedVaultEvent,
} from '@standardnotes/domain-events'
import { EmailLevel, Uuid } from '@standardnotes/domain-core'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { getBody, getSubject } from '../Email/UserInvitedToSharedVault'
export class UserInvitedToSharedVaultEventHandler implements DomainEventHandlerInterface {
constructor(
private userRepository: UserRepositoryInterface,
private domainEventFactory: DomainEventFactoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
) {}
async handle(event: UserInvitedToSharedVaultEvent): Promise<void> {
const userUuidOrError = Uuid.create(event.payload.invite.user_uuid)
if (userUuidOrError.isFailed()) {
return
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (!user) {
return
}
await this.domainEventPublisher.publish(
this.domainEventFactory.createEmailRequestedEvent({
body: getBody(),
level: EmailLevel.LEVELS.System,
subject: getSubject(),
messageIdentifier: 'USER_INVITED_TO_SHARED_VAULT',
userEmail: user.email,
}),
)
}
}

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,14 @@
import * as crypto from 'crypto'
import * as dayjs from 'dayjs'
import { UAParser } from 'ua-parser-js'
import { inject, injectable } from 'inversify'
import { v4 as uuidv4 } from 'uuid'
import { UAParserInstance } from 'ua-parser-js'
import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston'
import { LogSessionUserAgentOption, SettingName } from '@standardnotes/settings'
import { SettingName } from '@standardnotes/domain-core'
import { LogSessionUserAgentOption } from '@standardnotes/settings'
import { SessionBody } from '@standardnotes/responses'
import { CryptoNode } from '@standardnotes/sncrypto-node'
import TYPES from '../../Bootstrap/Types'
import { Session } from './Session'
import { SessionRepositoryInterface } from './SessionRepositoryInterface'
import { SessionServiceInterface } from './SessionServiceInterface'
@@ -18,30 +17,27 @@ import { EphemeralSessionRepositoryInterface } from './EphemeralSessionRepositor
import { EphemeralSession } from './EphemeralSession'
import { RevokedSession } from './RevokedSession'
import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { TraceSession } from '../UseCase/TraceSession/TraceSession'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { GetSetting } from '../UseCase/GetSetting/GetSetting'
@injectable()
export class SessionService implements SessionServiceInterface {
static readonly SESSION_TOKEN_VERSION = 1
constructor(
@inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface,
@inject(TYPES.Auth_EphemeralSessionRepository)
private sessionRepository: SessionRepositoryInterface,
private ephemeralSessionRepository: EphemeralSessionRepositoryInterface,
@inject(TYPES.Auth_RevokedSessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
@inject(TYPES.Auth_DeviceDetector) private deviceDetector: UAParser,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
@inject(TYPES.Auth_ACCESS_TOKEN_AGE) private accessTokenAge: number,
@inject(TYPES.Auth_REFRESH_TOKEN_AGE) private refreshTokenAge: number,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_CryptoNode) private cryptoNode: CryptoNode,
@inject(TYPES.Auth_TraceSession) private traceSession: TraceSession,
@inject(TYPES.Auth_UserSubscriptionRepository)
private revokedSessionRepository: RevokedSessionRepositoryInterface,
private deviceDetector: UAParserInstance,
private timer: TimerInterface,
private logger: Logger,
private accessTokenAge: number,
private refreshTokenAge: number,
private cryptoNode: CryptoNode,
private traceSession: TraceSession,
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
@inject(TYPES.Auth_READONLY_USERS) private readonlyUsers: string[],
private readonlyUsers: string[],
private getSetting: GetSetting,
) {}
async createNewSessionForUser(dto: {
@@ -320,15 +316,17 @@ export class SessionService implements SessionServiceInterface {
}
private async isLoggingUserAgentEnabledOnSessions(user: User): Promise<boolean> {
const loggingSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(),
const loggingSettingOrError = await this.getSetting.execute({
settingName: SettingName.NAMES.LogSessionUserAgent,
decrypted: true,
userUuid: user.uuid,
allowSensitiveRetrieval: true,
})
if (loggingSetting === null) {
if (loggingSettingOrError.isFailed()) {
return true
}
const loggingSetting = loggingSettingOrError.getValue()
return loggingSetting.value === LogSessionUserAgentOption.Enabled
return loggingSetting.decryptedValue === LogSessionUserAgentOption.Enabled
}
}

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,239 @@
import 'reflect-metadata'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { Setting } from './Setting'
import { SettingCrypter } from './SettingCrypter'
import { SubscriptionSetting } from './SubscriptionSetting'
import { SettingName, Timestamps, Uuid } from '@standardnotes/domain-core'
describe('SettingCrypter', () => {
let userRepository: UserRepositoryInterface
let crypter: CrypterInterface
let user: User
const createDecrypter = () => new SettingCrypter(userRepository, crypter)
beforeEach(() => {
crypter = {} as jest.Mocked<CrypterInterface>
crypter.decryptForUser = jest.fn().mockReturnValue('decrypted')
user = {
uuid: '4-5-6',
} as jest.Mocked<User>
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
})
describe('setting', () => {
it('should encrypt a string value', async () => {
const string = 'decrypted'
crypter.encryptForUser = jest.fn().mockReturnValue('encrypted')
const encrypted = await createDecrypter().encryptValue(
string,
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
)
expect(encrypted).toEqual('encrypted')
})
it('should return null when trying to encrypt a null value', async () => {
const encrypted = await createDecrypter().encryptValue(
null,
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
)
expect(encrypted).toBeNull()
})
it('should throw error when encrypting and user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
let caughtError = null
try {
await createDecrypter().encryptValue('test', Uuid.create('00000000-0000-0000-0000-000000000000').getValue())
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
it('should decrypt an encrypted value of a setting', async () => {
const setting = Setting.create({
name: SettingName.NAMES.ListedAuthorSecrets,
value: 'encrypted',
serverEncryptionVersion: EncryptionVersion.Default,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sensitive: false,
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual(
'decrypted',
)
})
it('should return null if the setting value is null', async () => {
const setting = Setting.create({
name: SettingName.NAMES.ListedAuthorSecrets,
value: null,
serverEncryptionVersion: EncryptionVersion.Default,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sensitive: false,
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toBeNull()
})
it('should return unencrypted value if the setting value is unencrypted', async () => {
const setting = Setting.create({
name: SettingName.NAMES.ListedAuthorSecrets,
value: 'test',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sensitive: false,
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual(
'test',
)
})
it('should throw if the user could not be found', async () => {
const setting = Setting.create({
name: SettingName.NAMES.ListedAuthorSecrets,
value: 'encrypted',
serverEncryptionVersion: EncryptionVersion.Default,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sensitive: false,
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
let caughtError = null
try {
await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
it('should throw if the user uuid is invalid', async () => {
const setting = Setting.create({
name: SettingName.NAMES.ListedAuthorSecrets,
value: 'encrypted',
serverEncryptionVersion: EncryptionVersion.Default,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sensitive: false,
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
let caughtError = null
try {
await createDecrypter().decryptSettingValue(setting, 'invalid')
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
})
describe('subscription setting', () => {
it('should decrypt an encrypted value of a setting', async () => {
const setting = SubscriptionSetting.create({
name: SettingName.NAMES.ExtensionKey,
value: 'encrypted',
sensitive: true,
serverEncryptionVersion: EncryptionVersion.Default,
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
expect(
await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000'),
).toEqual('decrypted')
})
it('should return null if the setting value is null', async () => {
const setting = SubscriptionSetting.create({
name: SettingName.NAMES.ExtensionKey,
value: null,
sensitive: true,
serverEncryptionVersion: EncryptionVersion.Default,
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
expect(
await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000'),
).toBeNull()
})
it('should return unencrypted value if the setting value is unencrypted', async () => {
const setting = SubscriptionSetting.create({
name: SettingName.NAMES.ExtensionKey,
value: 'test',
sensitive: true,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
expect(
await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000'),
).toEqual('test')
})
it('should throw if the user could not be found', async () => {
const setting = SubscriptionSetting.create({
name: SettingName.NAMES.ExtensionKey,
value: 'encrypted',
sensitive: true,
serverEncryptionVersion: EncryptionVersion.Default,
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
let caughtError = null
try {
await createDecrypter().decryptSubscriptionSettingValue(setting, '00000000-0000-0000-0000-000000000000')
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
it('should throw if the user uuid is invalid', async () => {
const setting = SubscriptionSetting.create({
name: SettingName.NAMES.ExtensionKey,
value: 'encrypted',
sensitive: true,
serverEncryptionVersion: EncryptionVersion.Default,
userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
let caughtError = null
try {
await createDecrypter().decryptSubscriptionSettingValue(setting, 'invalid')
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
})
})

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

@@ -4,30 +4,26 @@ import {
MuteEmailsSettingChangedEvent,
UserDisabledSessionUserAgentLoggingEvent,
} from '@standardnotes/domain-events'
import {
EmailBackupFrequency,
LogSessionUserAgentOption,
MuteMarketingEmailsOption,
SettingName,
} from '@standardnotes/settings'
import { EmailBackupFrequency, LogSessionUserAgentOption, MuteMarketingEmailsOption } from '@standardnotes/settings'
import 'reflect-metadata'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { User } from '../User/User'
import { Setting } from './Setting'
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
import { SettingCrypterInterface } from './SettingCrypterInterface'
import { SettingInterpreter } from './SettingInterpreter'
import { SettingRepositoryInterface } from './SettingRepositoryInterface'
import { GetUserKeyParams } from '../UseCase/GetUserKeyParams/GetUserKeyParams'
import { KeyParamsData } from '@standardnotes/responses'
import { Uuid, Timestamps, UniqueEntityId, SettingName } from '@standardnotes/domain-core'
describe('SettingInterpreter', () => {
let user: User
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let settingRepository: SettingRepositoryInterface
let settingDecrypter: SettingDecrypterInterface
let settingCrypter: SettingCrypterInterface
let logger: Logger
let getUserKeyParams: GetUserKeyParams
@@ -44,8 +40,8 @@ describe('SettingInterpreter', () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(null)
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
settingDecrypter = {} as jest.Mocked<SettingDecrypterInterface>
settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
settingCrypter = {} as jest.Mocked<SettingCrypterInterface>
settingCrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
@@ -96,11 +92,19 @@ describe('SettingInterpreter', () => {
})
it('should trigger backup if email backup setting is created - emails muted', async () => {
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({
name: SettingName.NAMES.MuteFailedBackupsEmails,
uuid: '6-7-8',
value: 'muted',
} as jest.Mocked<Setting>)
const setting = Setting.create(
{
name: SettingName.NAMES.MuteFailedBackupsEmails,
value: 'muted',
serverEncryptionVersion: 0,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sensitive: false,
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('7fb54003-1dd2-40bd-8900-2bacd6cf629c'),
).getValue()
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.EmailBackupFrequency,
@@ -109,7 +113,12 @@ describe('SettingInterpreter', () => {
)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '6-7-8', true, {})
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith(
'4-5-6',
'7fb54003-1dd2-40bd-8900-2bacd6cf629c',
true,
{},
)
})
it('should not trigger backup if email backup setting is disabled', async () => {

View File

@@ -1,11 +1,6 @@
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import {
EmailBackupFrequency,
LogSessionUserAgentOption,
MuteFailedBackupsEmailsOption,
SettingName,
} from '@standardnotes/settings'
import { EmailLevel, SettingName } from '@standardnotes/domain-core'
import { EmailBackupFrequency, LogSessionUserAgentOption, MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { User } from '../User/User'
@@ -54,8 +49,8 @@ export class SettingInterpreter implements SettingInterpreterInterface {
userUuid,
)
if (muteFailedEmailsBackupSetting !== null) {
userHasEmailsMuted = muteFailedEmailsBackupSetting.value === MuteFailedBackupsEmailsOption.Muted
muteEmailsSettingUuid = muteFailedEmailsBackupSetting.uuid
userHasEmailsMuted = muteFailedEmailsBackupSetting.props.value === MuteFailedBackupsEmailsOption.Muted
muteEmailsSettingUuid = muteFailedEmailsBackupSetting.id.toString()
}
const keyParamsResponse = await this.getUserKeyParams.execute({

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

@@ -1,6 +1,6 @@
import { ReadStream } from 'fs'
import { SettingName } from '@standardnotes/domain-core'
import { SettingName } from '@standardnotes/settings'
import { DeleteSettingDto } from '../UseCase/DeleteSetting/DeleteSettingDto'
import { Setting } from './Setting'
@@ -13,5 +13,6 @@ export interface SettingRepositoryInterface {
streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream>
streamAllByName(name: SettingName): Promise<ReadStream>
deleteByUserUuid(dto: DeleteSettingDto): Promise<void>
save(setting: Setting): Promise<Setting>
insert(setting: Setting): Promise<void>
update(setting: Setting): Promise<void>
}

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

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