mirror of
https://github.com/standardnotes/server
synced 2026-04-23 12:01:42 -04:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55587f6207 | |||
| 0d6b45c795 | |||
| 95f64d9952 | |||
| 54da5def4b | |||
| d2fc1e057d | |||
| 0a90d98c71 | |||
| cc269e3b35 | |||
| b19093179b | |||
| e2cc0bc003 | |||
| 644c52ae36 | |||
| 2554273a3f | |||
| a8ee149d7a | |||
| dcf92d58f9 | |||
| 053092031c | |||
| c12e3eb3ec | |||
| 07def20f6b | |||
| 6c2cca66bd | |||
| 6efd336f34 | |||
| 81eb4be200 | |||
| 76cee6dbad | |||
| dcc35a5738 | |||
| 5628de6445 | |||
| 53bea47727 | |||
| d6cf8d400a | |||
| b58cc335f2 | |||
| 03d1bc611c | |||
| a48b09cefe | |||
| d3f36c05df | |||
| 488ade25ab | |||
| 413a276d20 | |||
| 65675a21d6 | |||
| d35de38289 | |||
| 83e1baa978 | |||
| 875edce5b1 | |||
| 1baa504728 | |||
| 965ae79414 | |||
| 7a8448c116 | |||
| d935157ee8 | |||
| 9313e6b568 | |||
| 8033177f48 | |||
| 11011fa15d | |||
| c2e9f3e72b | |||
| f0fb7fd1cd | |||
| 15e342fd51 | |||
| dfa7e06f87 | |||
| a9aef5521b | |||
| a628bdc44e | |||
| db6f966045 | |||
| 9b602ed405 | |||
| db15457ce4 | |||
| 719d8558a3 | |||
| c207c3fc84 | |||
| 4bde4758c3 | |||
| 5eb957c82a | |||
| 0b38617acf | |||
| 377d32c449 | |||
| cdfb0c2603 | |||
| d85152429c | |||
| 422e596fc7 | |||
| 89334c9022 | |||
| f5a0e88ab9 | |||
| a59ba08339 | |||
| 2641056c51 | |||
| 5d812befc4 | |||
| 1c592d6f96 | |||
| 531f13fe1f | |||
| 4757cc8dae | |||
| ecdfe9ecc0 | |||
| d19cb08e9c | |||
| f45320e5ed | |||
| 93ded34de9 | |||
| dd13e2eaf7 | |||
| 1405c6f260 | |||
| 0dab31f993 |
@@ -2484,16 +2484,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/api", [\
|
||||
["npm:1.1.19", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.1.19-6a6d650ec9-cca168245a.zip/node_modules/@standardnotes/api/",\
|
||||
["npm:1.8.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.8.1-15c2e051d4-76c5d1a2d2.zip/node_modules/@standardnotes/api/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/api", "npm:1.1.19"],\
|
||||
["@standardnotes/auth", "npm:3.19.4"],\
|
||||
["@standardnotes/api", "npm:1.8.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/encryption", "npm:1.12.0"],\
|
||||
["@standardnotes/responses", "npm:1.6.39"],\
|
||||
["@standardnotes/services", "npm:1.15.0"],\
|
||||
["@standardnotes/utils", "npm:1.6.12"]\
|
||||
["@standardnotes/encryption", "npm:1.15.3"],\
|
||||
["@standardnotes/models", "npm:1.18.3"],\
|
||||
["@standardnotes/responses", "npm:1.10.2"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/utils", "npm:1.9.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
@@ -2562,11 +2563,11 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.5.0"],\
|
||||
["@standardnotes/analytics", "workspace:packages/analytics"],\
|
||||
["@standardnotes/api", "npm:1.1.19"],\
|
||||
["@standardnotes/api", "npm:1.8.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/features", "npm:1.50.0"],\
|
||||
["@standardnotes/features", "npm:1.52.1"],\
|
||||
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
||||
["@standardnotes/responses", "npm:1.6.39"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
@@ -2650,7 +2651,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.50.0"],\
|
||||
["@standardnotes/features", "npm:1.52.1"],\
|
||||
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@types/jest", "npm:28.1.4"],\
|
||||
@@ -2687,16 +2688,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/encryption", [\
|
||||
["npm:1.12.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.12.0-eb2342c675-1a28653b1e.zip/node_modules/@standardnotes/encryption/",\
|
||||
["npm:1.15.3", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.15.3-3580c52c1f-1a7863299f.zip/node_modules/@standardnotes/encryption/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/encryption", "npm:1.12.0"],\
|
||||
["@standardnotes/encryption", "npm:1.15.3"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/models", "npm:1.14.0"],\
|
||||
["@standardnotes/responses", "npm:1.6.39"],\
|
||||
["@standardnotes/services", "npm:1.15.0"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.9.0"],\
|
||||
["@standardnotes/utils", "npm:1.6.12"],\
|
||||
["@standardnotes/models", "npm:1.18.3"],\
|
||||
["@standardnotes/responses", "npm:1.10.2"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.11.1"],\
|
||||
["@standardnotes/utils", "npm:1.9.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
@@ -2742,6 +2742,28 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.52.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.0-8c1adf7881-3e6014272f.zip/node_modules/@standardnotes/features/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/features", "npm:1.52.0"],\
|
||||
["@standardnotes/auth", "npm:3.19.4"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.52.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.1-1fee85cf4e-ff3684399e.zip/node_modules/@standardnotes/features/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/features", "npm:1.52.1"],\
|
||||
["@standardnotes/auth", "npm:3.19.4"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/files-server", [\
|
||||
@@ -2797,14 +2819,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/models", [\
|
||||
["npm:1.14.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.14.0-6f064d99e7-bfb9d517b6.zip/node_modules/@standardnotes/models/",\
|
||||
["npm:1.18.3", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.18.3-6c65a62f30-21830c805f.zip/node_modules/@standardnotes/models/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/models", "npm:1.14.0"],\
|
||||
["@standardnotes/models", "npm:1.18.3"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.50.0"],\
|
||||
["@standardnotes/responses", "npm:1.6.39"],\
|
||||
["@standardnotes/utils", "npm:1.6.12"],\
|
||||
["@standardnotes/features", "npm:1.52.0"],\
|
||||
["@standardnotes/responses", "npm:1.10.2"],\
|
||||
["@standardnotes/utils", "npm:1.9.0"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
@@ -2840,6 +2862,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/responses", [\
|
||||
["npm:1.10.2", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.2-39d2d1f9b5-364724b5c7.zip/node_modules/@standardnotes/responses/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/responses", "npm:1.10.2"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.52.0"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.6.39", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.6.39-395f4c2d65-0ea1d4d5b8.zip/node_modules/@standardnotes/responses/",\
|
||||
"packageDependencies": [\
|
||||
@@ -2932,21 +2965,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "SOFT"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/services", [\
|
||||
["npm:1.15.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-services-npm-1.15.0-acab3bc6a3-1028a5b4c1.zip/node_modules/@standardnotes/services/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/services", "npm:1.15.0"],\
|
||||
["@standardnotes/auth", "npm:3.19.4"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/models", "npm:1.14.0"],\
|
||||
["@standardnotes/responses", "npm:1.6.39"],\
|
||||
["@standardnotes/utils", "npm:1.6.12"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/settings", [\
|
||||
["workspace:packages/settings", {\
|
||||
"packageLocation": "./packages/settings/",\
|
||||
@@ -2960,6 +2978,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/sncrypto-common", [\
|
||||
["npm:1.11.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.11.1-58d12d6912-69d698abb7.zip/node_modules/@standardnotes/sncrypto-common/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/sncrypto-common", "npm:1.11.1"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.9.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.9.0-48773f745a-42252d7198.zip/node_modules/@standardnotes/sncrypto-common/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3071,6 +3097,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["lodash", "npm:4.17.21"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.9.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.9.0-da939553f6-4591aff48d.zip/node_modules/@standardnotes/utils/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/utils", "npm:1.9.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["dompurify", "npm:2.4.0"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@szmarczak/http-timer", [\
|
||||
@@ -5844,6 +5881,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["dompurify", "npm:2.3.8"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:2.4.0", {\
|
||||
"packageLocation": "./.yarn/cache/dompurify-npm-2.4.0-0ffecf22ef-c93ea73cf8.zip/node_modules/dompurify/",\
|
||||
"packageDependencies": [\
|
||||
["dompurify", "npm:2.4.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["dot-prop", [\
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.29.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.29.0...@standardnotes/analytics@1.29.1) (2022-09-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** change remaining subscription time stats to percentage ([5eb957c](https://github.com/standardnotes/server/commit/5eb957c82a8cc5fdcb6815e2cd30e49cd2b1e8ac))
|
||||
|
||||
# [1.29.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.28.0...@standardnotes/analytics@1.29.0) (2022-09-15)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add remaining subscription time stats ([a59ba08](https://github.com/standardnotes/server/commit/a59ba083397c75960af0e8a102b617bf5baa287f))
|
||||
|
||||
# [1.28.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.27.0...@standardnotes/analytics@1.28.0) (2022-09-09)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "1.28.0",
|
||||
"version": "1.29.1",
|
||||
"engines": {
|
||||
"node": ">=14.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ export enum StatisticsMeasure {
|
||||
SubscriptionLength = 'subscription-length',
|
||||
RegistrationLength = 'registration-length',
|
||||
RegistrationToSubscriptionTime = 'registration-to-subscription-time',
|
||||
RemainingSubscriptionTimePercentage = 'remaining-subscription-time-percentage',
|
||||
Refunds = 'refunds',
|
||||
NotesCountFreeUsers = 'notes-count-free-users',
|
||||
NotesCountPaidUsers = 'notes-count-paid-users',
|
||||
|
||||
@@ -3,6 +3,64 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.1...@standardnotes/api-gateway@1.22.2) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.22.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.0...@standardnotes/api-gateway@1.22.1) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** remove admin graphql endpoint from being publicly available ([0a90d98](https://github.com/standardnotes/api-gateway/commit/0a90d98c71c6023b700f852c91aedfe1ad23af55))
|
||||
|
||||
# [1.22.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.21.1...@standardnotes/api-gateway@1.22.0) (2022-09-22)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** remove muting emails by use case in favor of updating user settings ([d3f36c0](https://github.com/standardnotes/api-gateway/commit/d3f36c05dfc114098a6c231d81149ebd1a959b74))
|
||||
|
||||
## [1.21.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.21.0...@standardnotes/api-gateway@1.21.1) (2022-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** web socket connection routing ([d35de38](https://github.com/standardnotes/api-gateway/commit/d35de38289e70d707d57a859b8bf39833fa825dd))
|
||||
|
||||
# [1.21.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.20.0...@standardnotes/api-gateway@1.21.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add creating cross service token in exchange for web socket connection token ([965ae79](https://github.com/standardnotes/api-gateway/commit/965ae79414e25d0959f67e16dcbb054229013e1c))
|
||||
|
||||
# [1.20.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.6...@standardnotes/api-gateway@1.20.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add creating web socket connection tokens ([8033177](https://github.com/standardnotes/api-gateway/commit/8033177f48dc961194f24fb7daa1073b8b697b74))
|
||||
|
||||
## [1.19.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.5...@standardnotes/api-gateway@1.19.6) (2022-09-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.19.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.4...@standardnotes/api-gateway@1.19.5) (2022-09-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** change remaining subscription time stats to percentage ([5eb957c](https://github.com/standardnotes/api-gateway/commit/5eb957c82a8cc5fdcb6815e2cd30e49cd2b1e8ac))
|
||||
|
||||
## [1.19.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.3...@standardnotes/api-gateway@1.19.4) (2022-09-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.19.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.2...@standardnotes/api-gateway@1.19.3) (2022-09-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** add remaining subscription time to stats ([89334c9](https://github.com/standardnotes/api-gateway/commit/89334c90221045308d83fce9e97c146185d21389))
|
||||
|
||||
## [1.19.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.1...@standardnotes/api-gateway@1.19.2) (2022-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.19.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.0...@standardnotes/api-gateway@1.19.1) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -94,6 +94,7 @@ const requestReport = async (
|
||||
StatisticsMeasure.RegistrationLength,
|
||||
StatisticsMeasure.SubscriptionLength,
|
||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
||||
StatisticsMeasure.NotesCountFreeUsers,
|
||||
StatisticsMeasure.NotesCountPaidUsers,
|
||||
StatisticsMeasure.FilesCount,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.19.1",
|
||||
"version": "1.22.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ import { SubscriptionTokenAuthMiddleware } from '../Controller/SubscriptionToken
|
||||
import { StatisticsMiddleware } from '../Controller/StatisticsMiddleware'
|
||||
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
import { RedisCrossServiceTokenCache } from '../Infra/Redis/RedisCrossServiceTokenCache'
|
||||
import { WebSocketAuthMiddleware } from '../Controller/WebSocketAuthMiddleware'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -85,6 +86,7 @@ export class ContainerConfigLoader {
|
||||
|
||||
// Middleware
|
||||
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
|
||||
container.bind<WebSocketAuthMiddleware>(TYPES.WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
|
||||
container
|
||||
.bind<SubscriptionTokenAuthMiddleware>(TYPES.SubscriptionTokenAuthMiddleware)
|
||||
.to(SubscriptionTokenAuthMiddleware)
|
||||
|
||||
@@ -18,6 +18,7 @@ const TYPES = {
|
||||
// Middleware
|
||||
StatisticsMiddleware: Symbol.for('StatisticsMiddleware'),
|
||||
AuthMiddleware: Symbol.for('AuthMiddleware'),
|
||||
WebSocketAuthMiddleware: Symbol.for('WebSocketAuthMiddleware'),
|
||||
SubscriptionTokenAuthMiddleware: Symbol.for('SubscriptionTokenAuthMiddleware'),
|
||||
// Services
|
||||
HTTPService: Symbol.for('HTTPService'),
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { verify } from 'jsonwebtoken'
|
||||
import { AxiosError, AxiosInstance } from 'axios'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class WebSocketAuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
|
||||
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
|
||||
@inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
|
||||
const authHeaderValue = request.headers.authorization as string
|
||||
|
||||
if (!authHeaderValue) {
|
||||
response.status(401).send({
|
||||
error: {
|
||||
tag: 'invalid-auth',
|
||||
message: 'Invalid login credentials.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: authHeaderValue,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
},
|
||||
url: `${this.authServerUrl}/sockets/tokens/validate`,
|
||||
})
|
||||
|
||||
if (authResponse.status > 200) {
|
||||
response.setHeader('content-type', authResponse.headers['content-type'])
|
||||
response.status(authResponse.status).send(authResponse.data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const crossServiceToken = authResponse.data.authToken
|
||||
|
||||
response.locals.authToken = crossServiceToken
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
|
||||
response.locals.freeUser =
|
||||
decodedToken.roles.length === 1 &&
|
||||
decodedToken.roles.find((role) => role.name === RoleName.CoreUser) !== undefined
|
||||
response.locals.userUuid = decodedToken.user.uuid
|
||||
response.locals.roles = decodedToken.roles
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
|
||||
this.logger.error(
|
||||
`Could not pass the request to ${this.authServerUrl}/sockets/tokens/validate on underlying service: ${errorMessage}`,
|
||||
)
|
||||
|
||||
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
|
||||
|
||||
if ((error as AxiosError).response?.headers['content-type']) {
|
||||
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
|
||||
}
|
||||
|
||||
const errorCode =
|
||||
(error as AxiosError).isAxiosError && !isNaN(+((error as AxiosError).code as string))
|
||||
? +((error as AxiosError).code as string)
|
||||
: 500
|
||||
|
||||
response.status(errorCode).send(errorMessage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
@@ -29,34 +29,4 @@ export class ActionsController extends BaseHttpController {
|
||||
async methods(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/methods', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/failed-backups-emails/mute/:settingUuid')
|
||||
async muteFailedBackupsEmails(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`internal/settings/email_backup/${request.params.settingUuid}/mute`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/sign-in-emails/mute/:settingUuid')
|
||||
async muteSignInEmails(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`internal/settings/sign_in/${request.params.settingUuid}/mute`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/marketing-emails/mute/:settingUuid')
|
||||
async muteMarketingEmails(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`internal/settings/marketing-emails/${request.params.settingUuid}/mute`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,11 +70,6 @@ export class PaymentsController extends BaseHttpController {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/events/registration', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/admin/graphql')
|
||||
async adminGraphql(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/graphql', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/admin/auth/login')
|
||||
async adminLogin(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/auth/login', request.body)
|
||||
|
||||
@@ -15,7 +15,12 @@ export class WebSocketsController extends BaseHttpController {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.AuthMiddleware)
|
||||
@httpPost('/tokens', TYPES.AuthMiddleware)
|
||||
async createWebSocketConnectionToken(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'sockets/tokens', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/connections', TYPES.WebSocketAuthMiddleware)
|
||||
async createWebSocketConnection(request: Request, response: Response): Promise<void> {
|
||||
if (!request.headers.connectionid) {
|
||||
this.logger.error('Could not create a websocket connection. Missing connection id header.')
|
||||
@@ -25,10 +30,15 @@ export class WebSocketsController extends BaseHttpController {
|
||||
return
|
||||
}
|
||||
|
||||
await this.httpService.callAuthServer(request, response, `sockets/${request.headers.connectionid}`, request.body)
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`sockets/connections/${request.headers.connectionid}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpDelete('/')
|
||||
@httpDelete('/connections')
|
||||
async deleteWebSocketConnection(request: Request, response: Response): Promise<void> {
|
||||
if (!request.headers.connectionid) {
|
||||
this.logger.error('Could not delete a websocket connection. Missing connection id header.')
|
||||
@@ -38,6 +48,11 @@ export class WebSocketsController extends BaseHttpController {
|
||||
return
|
||||
}
|
||||
|
||||
await this.httpService.callAuthServer(request, response, `sockets/${request.headers.connectionid}`, request.body)
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`sockets/connections/${request.headers.connectionid}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,5 +66,8 @@ SENTRY_ENVIRONMENT=
|
||||
VALET_TOKEN_SECRET=
|
||||
VALET_TOKEN_TTL=
|
||||
|
||||
WEB_SOCKET_CONNECTION_TOKEN_SECRET=
|
||||
WEB_SOCKET_CONNECTION_TOKEN_TTL=
|
||||
|
||||
# (Optional) Analytics
|
||||
ANALYTICS_ENABLED=false
|
||||
|
||||
@@ -3,6 +3,166 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.32.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.8...@standardnotes/auth-server@1.32.9) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.32.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.7...@standardnotes/auth-server@1.32.8) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** ttl for lock counter on login lockout ([54da5de](https://github.com/standardnotes/server/commit/54da5def4bbfbb4f74cbf02ae23e45103d250dd9))
|
||||
|
||||
## [1.32.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.6...@standardnotes/auth-server@1.32.7) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** allow resending canceled subscription invites ([b190931](https://github.com/standardnotes/server/commit/b19093179baaa1fb8cdf3f9d9bee20e625ed0b9b))
|
||||
|
||||
## [1.32.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.5...@standardnotes/auth-server@1.32.6) (2022-09-22)
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "fix(auth): subscription token ttl" ([644c52a](https://github.com/standardnotes/server/commit/644c52ae36d3720dee0712e2cb826c7e617ab7b7))
|
||||
* Revert "fix(auth): increase subscription token ttl" ([2554273](https://github.com/standardnotes/server/commit/2554273a3f85a968fed4286d109bed5413ef9908))
|
||||
* Revert "tmp(auth): disable expiring of subscription tokens" ([a8ee149](https://github.com/standardnotes/server/commit/a8ee149d7ac78775bf447ab924458b116414a15e))
|
||||
|
||||
## [1.32.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.4...@standardnotes/auth-server@1.32.5) (2022-09-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.32.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.3...@standardnotes/auth-server@1.32.4) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** increase subscription token ttl ([07def20](https://github.com/standardnotes/server/commit/07def20f6b47f9d1c678cfe5206b924dd5e6014a))
|
||||
|
||||
## [1.32.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.2...@standardnotes/auth-server@1.32.3) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** subscription token ttl ([6efd336](https://github.com/standardnotes/server/commit/6efd336f3407e7204a0c5d385ea9df5c02c7e5f5))
|
||||
|
||||
## [1.32.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.1...@standardnotes/auth-server@1.32.2) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add throwing an error if the subscription token was not persisted ([76cee6d](https://github.com/standardnotes/server/commit/76cee6dbad9bff041d8d5a1d4435046509c14f71))
|
||||
|
||||
## [1.32.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.0...@standardnotes/auth-server@1.32.1) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** settings and subscription settings projection ([d6cf8d4](https://github.com/standardnotes/server/commit/d6cf8d400a0177ee9030a171cf2ca47ade293fd9))
|
||||
|
||||
# [1.32.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.31.2...@standardnotes/auth-server@1.32.0) (2022-09-22)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** remove muting emails by use case in favor of updating user settings ([d3f36c0](https://github.com/standardnotes/server/commit/d3f36c05dfc114098a6c231d81149ebd1a959b74))
|
||||
|
||||
## [1.31.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.31.1...@standardnotes/auth-server@1.31.2) (2022-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** response wrapping on web socket connection token creation ([413a276](https://github.com/standardnotes/server/commit/413a276d205d53c316f7d0af8aed422001a6c1ab))
|
||||
|
||||
## [1.31.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.31.0...@standardnotes/auth-server@1.31.1) (2022-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** web sockets routes ([875edce](https://github.com/standardnotes/server/commit/875edce5b1dc134b4e22702354b29303fab3c910))
|
||||
|
||||
# [1.31.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.30.1...@standardnotes/auth-server@1.31.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add creating cross service token in exchange for web socket connection token ([965ae79](https://github.com/standardnotes/server/commit/965ae79414e25d0959f67e16dcbb054229013e1c))
|
||||
|
||||
## [1.30.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.30.0...@standardnotes/auth-server@1.30.1) (2022-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** missing injectable annotation ([d935157](https://github.com/standardnotes/server/commit/d935157ee8425d427fa52465e766d18e29332b5b))
|
||||
|
||||
# [1.30.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.29.1...@standardnotes/auth-server@1.30.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add creating web socket connection tokens ([8033177](https://github.com/standardnotes/server/commit/8033177f48dc961194f24fb7daa1073b8b697b74))
|
||||
|
||||
## [1.29.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.29.0...@standardnotes/auth-server@1.29.1) (2022-09-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** uuid validator binding ([db6f966](https://github.com/standardnotes/server/commit/db6f966045d51e59555740c9e009bf66b629673c))
|
||||
|
||||
# [1.29.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.4...@standardnotes/auth-server@1.29.0) (2022-09-19)
|
||||
|
||||
### Features
|
||||
|
||||
* **files:** add validating remote identifiers ([db15457](https://github.com/standardnotes/server/commit/db15457ce4eb533ec822cf93c3ed83eafe9e64d5))
|
||||
|
||||
## [1.28.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.3...@standardnotes/auth-server@1.28.4) (2022-09-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** feature service spec ([c207c3f](https://github.com/standardnotes/server/commit/c207c3fc8442eec9b8c3150f09ecccfdd6a5ed50))
|
||||
|
||||
## [1.28.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.2...@standardnotes/auth-server@1.28.3) (2022-09-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** change remaining subscription time stats to percentage ([5eb957c](https://github.com/standardnotes/server/commit/5eb957c82a8cc5fdcb6815e2cd30e49cd2b1e8ac))
|
||||
|
||||
## [1.28.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.1...@standardnotes/auth-server@1.28.2) (2022-09-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/server/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
|
||||
|
||||
## [1.28.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.0...@standardnotes/auth-server@1.28.1) (2022-09-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** missing injectable annotation ([d851524](https://github.com/standardnotes/server/commit/d85152429ca379d3d0314a9864cc46ebee541958))
|
||||
|
||||
# [1.28.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.27.0...@standardnotes/auth-server@1.28.0) (2022-09-15)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add remaining subscription time stats ([a59ba08](https://github.com/standardnotes/server/commit/a59ba083397c75960af0e8a102b617bf5baa287f))
|
||||
|
||||
# [1.27.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.26.1...@standardnotes/auth-server@1.27.0) (2022-09-15)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** implement subscription server interface on server side ([5d812be](https://github.com/standardnotes/server/commit/5d812befc4733954919eef0d3718ae6f8eb81654))
|
||||
|
||||
## [1.26.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.26.0...@standardnotes/auth-server@1.26.1) (2022-09-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** disallow duplicating subscription invites ([531f13f](https://github.com/standardnotes/server/commit/531f13fe1f4bdfb8d27f5e3c07ec0b15d36ad413))
|
||||
|
||||
# [1.26.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.13...@standardnotes/auth-server@1.26.0) (2022-09-13)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add subscription sharing permission ([f45320e](https://github.com/standardnotes/server/commit/f45320e5ed8948a432029586c05284f4d640de5b))
|
||||
|
||||
## [1.25.13](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.12...@standardnotes/auth-server@1.25.13) (2022-09-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add debug logs for canceling shared subscription invitations ([dd13e2e](https://github.com/standardnotes/server/commit/dd13e2eaf74de56a3c8c30c236c32c6dc0c560f2))
|
||||
|
||||
## [1.25.12](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.11...@standardnotes/auth-server@1.25.12) (2022-09-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** allow canceling shared subscription invitation before it was accepted ([0dab31f](https://github.com/standardnotes/server/commit/0dab31f9936bfd5081a87eef9701a268b8dec88c))
|
||||
|
||||
## [1.25.11](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.10...@standardnotes/auth-server@1.25.11) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -10,17 +10,17 @@ import '../src/Controller/SessionsController'
|
||||
import '../src/Controller/UsersController'
|
||||
import '../src/Controller/SettingsController'
|
||||
import '../src/Controller/FeaturesController'
|
||||
import '../src/Controller/WebSocketsController'
|
||||
import '../src/Controller/AdminController'
|
||||
import '../src/Controller/InternalController'
|
||||
import '../src/Controller/SubscriptionTokensController'
|
||||
import '../src/Controller/OfflineController'
|
||||
import '../src/Controller/ValetTokenController'
|
||||
import '../src/Controller/ListedController'
|
||||
import '../src/Controller/SubscriptionInvitesController'
|
||||
import '../src/Controller/SubscriptionSettingsController'
|
||||
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'
|
||||
|
||||
import * as cors from 'cors'
|
||||
import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addSubscriptionSharingPermission1663073954000 implements MigrationInterface {
|
||||
name = 'addSubscriptionSharingPermission1663073954000'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `permissions` (uuid, name) VALUES ("3aeaf12e-380f-4f21-97b9-d862d63874f6", "server:subscription-sharing")',
|
||||
)
|
||||
|
||||
// Pro User Permissions
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES \
|
||||
("8047edbb-a10a-4ff8-8d53-c2cae600a8e8", "3aeaf12e-380f-4f21-97b9-d862d63874f6") \
|
||||
',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addRenewedAtColumn1663321030000 implements MigrationInterface {
|
||||
name = 'addRenewedAtColumn1663321030000'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `user_subscriptions` ADD `renewed_at` bigint NULL')
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.25.11",
|
||||
"version": "1.32.9",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -34,11 +34,11 @@
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/analytics": "workspace:*",
|
||||
"@standardnotes/api": "^1.1.19",
|
||||
"@standardnotes/api": "^1.8.1",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/features": "^1.47.0",
|
||||
"@standardnotes/features": "^1.52.1",
|
||||
"@standardnotes/predicates": "workspace:*",
|
||||
"@standardnotes/responses": "^1.6.39",
|
||||
"@standardnotes/security": "workspace:*",
|
||||
|
||||
@@ -130,13 +130,19 @@ import { RedisOfflineSubscriptionTokenRepository } from '../Infra/Redis/RedisOff
|
||||
import { CreateOfflineSubscriptionToken } from '../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
|
||||
import { AuthenticateOfflineSubscriptionToken } from '../Domain/UseCase/AuthenticateOfflineSubscriptionToken/AuthenticateOfflineSubscriptionToken'
|
||||
import { SubscriptionCancelledEventHandler } from '../Domain/Handler/SubscriptionCancelledEventHandler'
|
||||
import { ContentDecoder, ContentDecoderInterface, ProtocolVersion } from '@standardnotes/common'
|
||||
import {
|
||||
ContentDecoder,
|
||||
ContentDecoderInterface,
|
||||
ProtocolVersion,
|
||||
Uuid,
|
||||
UuidValidator,
|
||||
ValidatorInterface,
|
||||
} from '@standardnotes/common'
|
||||
import { GetUserOfflineSubscription } from '../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
|
||||
import { ApiGatewayOfflineAuthMiddleware } from '../Controller/ApiGatewayOfflineAuthMiddleware'
|
||||
import { UserEmailChangedEventHandler } from '../Domain/Handler/UserEmailChangedEventHandler'
|
||||
import { SettingsAssociationServiceInterface } from '../Domain/Setting/SettingsAssociationServiceInterface'
|
||||
import { SettingsAssociationService } from '../Domain/Setting/SettingsAssociationService'
|
||||
import { MuteFailedBackupsEmails } from '../Domain/UseCase/MuteFailedBackupsEmails/MuteFailedBackupsEmails'
|
||||
import { SubscriptionSyncRequestedEventHandler } from '../Domain/Handler/SubscriptionSyncRequestedEventHandler'
|
||||
import {
|
||||
CrossServiceTokenData,
|
||||
@@ -149,13 +155,13 @@ import {
|
||||
TokenEncoder,
|
||||
TokenEncoderInterface,
|
||||
ValetTokenData,
|
||||
WebSocketConnectionTokenData,
|
||||
} from '@standardnotes/security'
|
||||
import { FileUploadedEventHandler } from '../Domain/Handler/FileUploadedEventHandler'
|
||||
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||
import { CreateListedAccount } from '../Domain/UseCase/CreateListedAccount/CreateListedAccount'
|
||||
import { ListedAccountCreatedEventHandler } from '../Domain/Handler/ListedAccountCreatedEventHandler'
|
||||
import { ListedAccountDeletedEventHandler } from '../Domain/Handler/ListedAccountDeletedEventHandler'
|
||||
import { MuteSignInEmails } from '../Domain/UseCase/MuteSignInEmails/MuteSignInEmails'
|
||||
import { FileRemovedEventHandler } from '../Domain/Handler/FileRemovedEventHandler'
|
||||
import { UserDisabledSessionUserAgentLoggingEventHandler } from '../Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler'
|
||||
import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface'
|
||||
@@ -196,10 +202,14 @@ import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUser
|
||||
import { AuthController } from '../Controller/AuthController'
|
||||
import { VerifyPredicate } from '../Domain/UseCase/VerifyPredicate/VerifyPredicate'
|
||||
import { PredicateVerificationRequestedEventHandler } from '../Domain/Handler/PredicateVerificationRequestedEventHandler'
|
||||
import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteMarketingEmails'
|
||||
import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventHandler'
|
||||
import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEventHandler'
|
||||
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
|
||||
import { SubscriptionInvitesController } from '../Controller/SubscriptionInvitesController'
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
import { WebSocketsController } from '../Controller/WebSocketsController'
|
||||
import { WebSocketServerInterface } from '@standardnotes/api'
|
||||
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -262,6 +272,8 @@ export class ContainerConfigLoader {
|
||||
|
||||
// Controller
|
||||
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
|
||||
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
|
||||
container.bind<WebSocketServerInterface>(TYPES.WebSocketsController).to(WebSocketsController)
|
||||
|
||||
// Repositories
|
||||
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)
|
||||
@@ -360,6 +372,12 @@ export class ContainerConfigLoader {
|
||||
container.bind(TYPES.AUTH_JWT_TTL).toConstantValue(+env.get('AUTH_JWT_TTL'))
|
||||
container.bind(TYPES.VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET', true))
|
||||
container.bind(TYPES.VALET_TOKEN_TTL).toConstantValue(+env.get('VALET_TOKEN_TTL', true))
|
||||
container
|
||||
.bind(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)
|
||||
.toConstantValue(env.get('WEB_SOCKET_CONNECTION_TOKEN_SECRET', true))
|
||||
container
|
||||
.bind(TYPES.WEB_SOCKET_CONNECTION_TOKEN_TTL)
|
||||
.toConstantValue(+env.get('WEB_SOCKET_CONNECTION_TOKEN_TTL', true))
|
||||
container.bind(TYPES.ENCRYPTION_SERVER_KEY).toConstantValue(env.get('ENCRYPTION_SERVER_KEY'))
|
||||
container.bind(TYPES.ACCESS_TOKEN_AGE).toConstantValue(env.get('ACCESS_TOKEN_AGE'))
|
||||
container.bind(TYPES.REFRESH_TOKEN_AGE).toConstantValue(env.get('REFRESH_TOKEN_AGE'))
|
||||
@@ -418,9 +436,6 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<CreateOfflineSubscriptionToken>(TYPES.CreateOfflineSubscriptionToken)
|
||||
.to(CreateOfflineSubscriptionToken)
|
||||
container.bind<MuteFailedBackupsEmails>(TYPES.MuteFailedBackupsEmails).to(MuteFailedBackupsEmails)
|
||||
container.bind<MuteSignInEmails>(TYPES.MuteSignInEmails).to(MuteSignInEmails)
|
||||
container.bind<MuteMarketingEmails>(TYPES.MuteMarketingEmails).to(MuteMarketingEmails)
|
||||
container.bind<CreateValetToken>(TYPES.CreateValetToken).to(CreateValetToken)
|
||||
container.bind<CreateListedAccount>(TYPES.CreateListedAccount).to(CreateListedAccount)
|
||||
container.bind<InviteToSharedSubscription>(TYPES.InviteToSharedSubscription).to(InviteToSharedSubscription)
|
||||
@@ -439,6 +454,10 @@ export class ContainerConfigLoader {
|
||||
container.bind<GetSubscriptionSetting>(TYPES.GetSubscriptionSetting).to(GetSubscriptionSetting)
|
||||
container.bind<GetUserAnalyticsId>(TYPES.GetUserAnalyticsId).to(GetUserAnalyticsId)
|
||||
container.bind<VerifyPredicate>(TYPES.VerifyPredicate).to(VerifyPredicate)
|
||||
container
|
||||
.bind<CreateWebSocketConnectionToken>(TYPES.CreateWebSocketConnectionToken)
|
||||
.to(CreateWebSocketConnectionToken)
|
||||
container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
|
||||
|
||||
// Handlers
|
||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||
@@ -511,6 +530,11 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<TokenDecoderInterface<OfflineUserTokenData>>(TYPES.OfflineUserTokenDecoder)
|
||||
.toConstantValue(new TokenDecoder<OfflineUserTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
|
||||
container
|
||||
.bind<TokenDecoderInterface<WebSocketConnectionTokenData>>(TYPES.WebSocketConnectionTokenDecoder)
|
||||
.toConstantValue(
|
||||
new TokenDecoder<WebSocketConnectionTokenData>(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)),
|
||||
)
|
||||
container
|
||||
.bind<TokenEncoderInterface<OfflineUserTokenData>>(TYPES.OfflineUserTokenEncoder)
|
||||
.toConstantValue(new TokenEncoder<OfflineUserTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
|
||||
@@ -523,6 +547,11 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<TokenEncoderInterface<ValetTokenData>>(TYPES.ValetTokenEncoder)
|
||||
.toConstantValue(new TokenEncoder<ValetTokenData>(container.get(TYPES.VALET_TOKEN_SECRET)))
|
||||
container
|
||||
.bind<TokenEncoderInterface<WebSocketConnectionTokenData>>(TYPES.WebSocketConnectionTokenEncoder)
|
||||
.toConstantValue(
|
||||
new TokenEncoder<WebSocketConnectionTokenData>(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)),
|
||||
)
|
||||
container.bind<AuthenticationMethodResolver>(TYPES.AuthenticationMethodResolver).to(AuthenticationMethodResolver)
|
||||
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
|
||||
@@ -557,6 +586,7 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
||||
.toConstantValue(new RedisStatisticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
|
||||
container.bind<ValidatorInterface<Uuid>>(TYPES.UuidValidator).toConstantValue(new UuidValidator())
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
container
|
||||
|
||||
@@ -5,6 +5,8 @@ const TYPES = {
|
||||
SQS: Symbol.for('SQS'),
|
||||
// Controller
|
||||
AuthController: Symbol.for('AuthController'),
|
||||
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
|
||||
WebSocketsController: Symbol.for('WebSocketsController'),
|
||||
// Repositories
|
||||
UserRepository: Symbol.for('UserRepository'),
|
||||
SessionRepository: Symbol.for('SessionRepository'),
|
||||
@@ -59,6 +61,8 @@ const TYPES = {
|
||||
AUTH_JWT_TTL: Symbol.for('AUTH_JWT_TTL'),
|
||||
VALET_TOKEN_SECRET: Symbol.for('VALET_TOKEN_SECRET'),
|
||||
VALET_TOKEN_TTL: Symbol.for('VALET_TOKEN_TTL'),
|
||||
WEB_SOCKET_CONNECTION_TOKEN_SECRET: Symbol.for('WEB_SOCKET_CONNECTION_TOKEN_SECRET'),
|
||||
WEB_SOCKET_CONNECTION_TOKEN_TTL: Symbol.for('WEB_SOCKET_CONNECTION_TOKEN_TTL'),
|
||||
ENCRYPTION_SERVER_KEY: Symbol.for('ENCRYPTION_SERVER_KEY'),
|
||||
ACCESS_TOKEN_AGE: Symbol.for('ACCESS_TOKEN_AGE'),
|
||||
REFRESH_TOKEN_AGE: Symbol.for('REFRESH_TOKEN_AGE'),
|
||||
@@ -111,9 +115,6 @@ const TYPES = {
|
||||
AuthenticateSubscriptionToken: Symbol.for('AuthenticateSubscriptionToken'),
|
||||
CreateOfflineSubscriptionToken: Symbol.for('CreateOfflineSubscriptionToken'),
|
||||
AuthenticateOfflineSubscriptionToken: Symbol.for('AuthenticateOfflineSubscriptionToken'),
|
||||
MuteFailedBackupsEmails: Symbol.for('MuteFailedBackupsEmails'),
|
||||
MuteSignInEmails: Symbol.for('MuteSignInEmails'),
|
||||
MuteMarketingEmails: Symbol.for('MuteMarketingEmails'),
|
||||
CreateValetToken: Symbol.for('CreateValetToken'),
|
||||
CreateListedAccount: Symbol.for('CreateListedAccount'),
|
||||
InviteToSharedSubscription: Symbol.for('InviteToSharedSubscription'),
|
||||
@@ -124,6 +125,8 @@ const TYPES = {
|
||||
GetSubscriptionSetting: Symbol.for('GetSubscriptionSetting'),
|
||||
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
||||
VerifyPredicate: Symbol.for('VerifyPredicate'),
|
||||
CreateWebSocketConnectionToken: Symbol.for('CreateWebSocketConnectionToken'),
|
||||
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
|
||||
// Handlers
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
@@ -165,6 +168,8 @@ const TYPES = {
|
||||
CrossServiceTokenEncoder: Symbol.for('CrossServiceTokenEncoder'),
|
||||
SessionTokenEncoder: Symbol.for('SessionTokenEncoder'),
|
||||
ValetTokenEncoder: Symbol.for('ValetTokenEncoder'),
|
||||
WebSocketConnectionTokenEncoder: Symbol.for('WebSocketConnectionTokenEncoder'),
|
||||
WebSocketConnectionTokenDecoder: Symbol.for('WebSocketConnectionTokenDecoder'),
|
||||
AuthenticationMethodResolver: Symbol.for('AuthenticationMethodResolver'),
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
@@ -188,6 +193,7 @@ const TYPES = {
|
||||
UserSubscriptionService: Symbol.for('UserSubscriptionService'),
|
||||
AnalyticsStore: Symbol.for('AnalyticsStore'),
|
||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||
UuidValidator: Symbol.for('UuidValidator'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
|
||||
@@ -7,23 +7,16 @@ import { results } from 'inversify-express-utils'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { GetUserFeatures } from '../Domain/UseCase/GetUserFeatures/GetUserFeatures'
|
||||
import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { MuteFailedBackupsEmails } from '../Domain/UseCase/MuteFailedBackupsEmails/MuteFailedBackupsEmails'
|
||||
import { MuteSignInEmails } from '../Domain/UseCase/MuteSignInEmails/MuteSignInEmails'
|
||||
import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteMarketingEmails'
|
||||
|
||||
describe('InternalController', () => {
|
||||
let getUserFeatures: GetUserFeatures
|
||||
let getSetting: GetSetting
|
||||
let muteFailedBackupsEmails: MuteFailedBackupsEmails
|
||||
let muteSignInEmails: MuteSignInEmails
|
||||
let muteMarketingEmails: MuteMarketingEmails
|
||||
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
let user: User
|
||||
|
||||
const createController = () =>
|
||||
new InternalController(getUserFeatures, getSetting, muteFailedBackupsEmails, muteSignInEmails, muteMarketingEmails)
|
||||
const createController = () => new InternalController(getUserFeatures, getSetting)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
@@ -35,15 +28,6 @@ describe('InternalController', () => {
|
||||
getSetting = {} as jest.Mocked<GetSetting>
|
||||
getSetting.execute = jest.fn()
|
||||
|
||||
muteFailedBackupsEmails = {} as jest.Mocked<MuteFailedBackupsEmails>
|
||||
muteFailedBackupsEmails.execute = jest.fn()
|
||||
|
||||
muteSignInEmails = {} as jest.Mocked<MuteSignInEmails>
|
||||
muteSignInEmails.execute = jest.fn()
|
||||
|
||||
muteMarketingEmails = {} as jest.Mocked<MuteMarketingEmails>
|
||||
muteMarketingEmails.execute = jest.fn()
|
||||
|
||||
request = {
|
||||
headers: {},
|
||||
body: {},
|
||||
@@ -120,83 +104,4 @@ describe('InternalController', () => {
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should mute failed backup emails user setting', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteFailedBackupsEmails.execute = jest.fn().mockReturnValue({ success: true })
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().muteFailedBackupsEmails(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(muteFailedBackupsEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('should not mute failed backup emails user setting if it does not exist', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteFailedBackupsEmails.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().muteFailedBackupsEmails(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(muteFailedBackupsEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(404)
|
||||
})
|
||||
|
||||
it('should mute sign in emails user setting', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteSignInEmails.execute = jest.fn().mockReturnValue({ success: true })
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().muteSignInEmails(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(muteSignInEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('should not mute sign in emails user setting if it does not exist', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteSignInEmails.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().muteSignInEmails(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(muteSignInEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(404)
|
||||
})
|
||||
|
||||
it('should mute marketing emails user setting', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteMarketingEmails.execute = jest.fn().mockReturnValue({ success: true, message: 'foobar' })
|
||||
|
||||
await createController().muteMarketingEmails(request, response)
|
||||
|
||||
expect(muteMarketingEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledWith('content-type', 'text/html')
|
||||
expect(response.send).toHaveBeenCalledWith('foobar')
|
||||
})
|
||||
|
||||
it('should not mute marketing emails user setting if it does not exist', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteMarketingEmails.execute = jest.fn().mockReturnValue({ success: false, message: 'foobar' })
|
||||
|
||||
await createController().muteMarketingEmails(request, response)
|
||||
|
||||
expect(muteMarketingEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledWith('content-type', 'text/html')
|
||||
expect(response.status).toHaveBeenCalledWith(404)
|
||||
expect(response.send).toHaveBeenCalledWith('foobar')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { Request } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
@@ -10,18 +10,12 @@ import {
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { GetUserFeatures } from '../Domain/UseCase/GetUserFeatures/GetUserFeatures'
|
||||
import { MuteFailedBackupsEmails } from '../Domain/UseCase/MuteFailedBackupsEmails/MuteFailedBackupsEmails'
|
||||
import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteMarketingEmails'
|
||||
import { MuteSignInEmails } from '../Domain/UseCase/MuteSignInEmails/MuteSignInEmails'
|
||||
|
||||
@controller('/internal')
|
||||
export class InternalController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserFeatures) private doGetUserFeatures: GetUserFeatures,
|
||||
@inject(TYPES.GetSetting) private doGetSetting: GetSetting,
|
||||
@inject(TYPES.MuteFailedBackupsEmails) private doMuteFailedBackupsEmails: MuteFailedBackupsEmails,
|
||||
@inject(TYPES.MuteSignInEmails) private doMuteSignInEmails: MuteSignInEmails,
|
||||
@inject(TYPES.MuteMarketingEmails) private doMuteMarketingEmails: MuteMarketingEmails,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -54,50 +48,4 @@ export class InternalController extends BaseHttpController {
|
||||
|
||||
return this.json(result, 400)
|
||||
}
|
||||
|
||||
@httpGet('/settings/email_backup/:settingUuid/mute')
|
||||
async muteFailedBackupsEmails(request: Request): Promise<results.JsonResult> {
|
||||
const { settingUuid } = request.params
|
||||
const result = await this.doMuteFailedBackupsEmails.execute({
|
||||
settingUuid,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json({ message: result.message })
|
||||
}
|
||||
|
||||
return this.json({ message: result.message }, 404)
|
||||
}
|
||||
|
||||
@httpGet('/settings/sign_in/:settingUuid/mute')
|
||||
async muteSignInEmails(request: Request): Promise<results.JsonResult> {
|
||||
const { settingUuid } = request.params
|
||||
const result = await this.doMuteSignInEmails.execute({
|
||||
settingUuid,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json({ message: result.message })
|
||||
}
|
||||
|
||||
return this.json({ message: result.message }, 404)
|
||||
}
|
||||
|
||||
@httpGet('/settings/marketing-emails/:settingUuid/mute')
|
||||
async muteMarketingEmails(request: Request, response: Response): Promise<void> {
|
||||
const { settingUuid } = request.params
|
||||
const result = await this.doMuteMarketingEmails.execute({
|
||||
settingUuid,
|
||||
})
|
||||
|
||||
response.setHeader('content-type', 'text/html')
|
||||
|
||||
if (result.success) {
|
||||
response.send(result.message)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.status(404).send(result.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,43 +9,25 @@ import { ProjectorInterface } from '../Projection/ProjectorInterface'
|
||||
import { GetActiveSessionsForUser } from '../Domain/UseCase/GetActiveSessionsForUser'
|
||||
import { AuthenticateRequest } from '../Domain/UseCase/AuthenticateRequest'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { Role } from '../Domain/Role/Role'
|
||||
import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
|
||||
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
|
||||
describe('SessionsController', () => {
|
||||
let getActiveSessionsForUser: GetActiveSessionsForUser
|
||||
let authenticateRequest: AuthenticateRequest
|
||||
let userProjector: ProjectorInterface<User>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
const jwtTTL = 60
|
||||
let sessionProjector: ProjectorInterface<Session>
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let session: Session
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
let user: User
|
||||
let role: Role
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let createCrossServiceToken: CreateCrossServiceToken
|
||||
|
||||
const createController = () =>
|
||||
new SessionsController(
|
||||
getActiveSessionsForUser,
|
||||
authenticateRequest,
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
getUserAnalyticsId,
|
||||
true,
|
||||
jwtTTL,
|
||||
)
|
||||
new SessionsController(getActiveSessionsForUser, authenticateRequest, sessionProjector, createCrossServiceToken)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
user.roles = Promise.resolve([role])
|
||||
|
||||
getActiveSessionsForUser = {} as jest.Mocked<GetActiveSessionsForUser>
|
||||
getActiveSessionsForUser.execute = jest.fn().mockReturnValue({ sessions: [session] })
|
||||
@@ -53,21 +35,11 @@ describe('SessionsController', () => {
|
||||
authenticateRequest = {} as jest.Mocked<AuthenticateRequest>
|
||||
authenticateRequest.execute = jest.fn()
|
||||
|
||||
userProjector = {} as jest.Mocked<ProjectorInterface<User>>
|
||||
userProjector.projectSimple = jest.fn().mockReturnValue({ bar: 'baz' })
|
||||
|
||||
roleProjector = {} as jest.Mocked<ProjectorInterface<Role>>
|
||||
roleProjector.projectSimple = jest.fn().mockReturnValue({ name: 'role1', uuid: '1-3-4' })
|
||||
|
||||
sessionProjector = {} as jest.Mocked<ProjectorInterface<Session>>
|
||||
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
sessionProjector.projectSimple = jest.fn().mockReturnValue({ test: 'test' })
|
||||
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<CrossServiceTokenData>>
|
||||
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 123 })
|
||||
createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken>
|
||||
createCrossServiceToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
|
||||
|
||||
request = {
|
||||
params: {},
|
||||
@@ -114,75 +86,6 @@ describe('SessionsController', () => {
|
||||
const httpResponseContent = await result.content.readAsStringAsync()
|
||||
const httpResponseJSON = JSON.parse(httpResponseContent)
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
|
||||
expect(httpResponseJSON.authToken).toEqual('foobar')
|
||||
})
|
||||
|
||||
it('should validate a session from an incoming request - disabled analytics', async () => {
|
||||
authenticateRequest.execute = jest.fn().mockReturnValue({
|
||||
success: true,
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
request.headers.authorization = 'test'
|
||||
|
||||
const controller = new SessionsController(
|
||||
getActiveSessionsForUser,
|
||||
authenticateRequest,
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
getUserAnalyticsId,
|
||||
false,
|
||||
jwtTTL,
|
||||
)
|
||||
|
||||
const httpResponse = await controller.validate(request)
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.JsonResult)
|
||||
|
||||
const result = await httpResponse.executeAsync()
|
||||
const httpResponseContent = await result.content.readAsStringAsync()
|
||||
const httpResponseJSON = JSON.parse(httpResponseContent)
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
|
||||
expect(httpResponseJSON.authToken).toEqual('foobar')
|
||||
})
|
||||
|
||||
|
||||
@@ -12,26 +12,18 @@ import TYPES from '../Bootstrap/Types'
|
||||
import { Session } from '../Domain/Session/Session'
|
||||
import { AuthenticateRequest } from '../Domain/UseCase/AuthenticateRequest'
|
||||
import { GetActiveSessionsForUser } from '../Domain/UseCase/GetActiveSessionsForUser'
|
||||
import { Role } from '../Domain/Role/Role'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { ProjectorInterface } from '../Projection/ProjectorInterface'
|
||||
import { SessionProjector } from '../Projection/SessionProjector'
|
||||
import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
|
||||
@controller('/sessions')
|
||||
export class SessionsController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.GetActiveSessionsForUser) private getActiveSessionsForUser: GetActiveSessionsForUser,
|
||||
@inject(TYPES.AuthenticateRequest) private authenticateRequest: AuthenticateRequest,
|
||||
@inject(TYPES.UserProjector) private userProjector: ProjectorInterface<User>,
|
||||
@inject(TYPES.SessionProjector) private sessionProjector: ProjectorInterface<Session>,
|
||||
@inject(TYPES.RoleProjector) private roleProjector: ProjectorInterface<Role>,
|
||||
@inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.ANALYTICS_ENABLED) private analyticsEnabled: boolean,
|
||||
@inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number,
|
||||
@inject(TYPES.CreateCrossServiceToken) private createCrossServiceToken: CreateCrossServiceToken,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -56,25 +48,12 @@ export class SessionsController extends BaseHttpController {
|
||||
|
||||
const user = authenticateRequestResponse.user as User
|
||||
|
||||
const roles = await user.roles
|
||||
const result = await this.createCrossServiceToken.execute({
|
||||
user,
|
||||
session: authenticateRequestResponse.session,
|
||||
})
|
||||
|
||||
const authTokenData: CrossServiceTokenData = {
|
||||
user: this.projectUser(user),
|
||||
roles: this.projectRoles(roles),
|
||||
}
|
||||
|
||||
if (this.analyticsEnabled) {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
authTokenData.analyticsId = analyticsId
|
||||
}
|
||||
|
||||
if (authenticateRequestResponse.session !== undefined) {
|
||||
authTokenData.session = this.projectSession(authenticateRequestResponse.session)
|
||||
}
|
||||
|
||||
const authToken = this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL)
|
||||
|
||||
return this.json({ authToken })
|
||||
return this.json({ authToken: result.token })
|
||||
}
|
||||
|
||||
@httpGet('/', TYPES.AuthMiddleware, TYPES.SessionMiddleware)
|
||||
@@ -93,36 +72,4 @@ export class SessionsController extends BaseHttpController {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private projectUser(user: User): { uuid: string; email: string } {
|
||||
return <{ uuid: string; email: string }>this.userProjector.projectSimple(user)
|
||||
}
|
||||
|
||||
private projectSession(session: Session): {
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
} {
|
||||
return <
|
||||
{
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
}
|
||||
>this.sessionProjector.projectSimple(session)
|
||||
}
|
||||
|
||||
private projectRoles(roles: Array<Role>): Array<{ uuid: string; name: RoleName }> {
|
||||
return roles.map((role) => <{ uuid: string; name: RoleName }>this.roleProjector.projectSimple(role))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as express from 'express'
|
||||
|
||||
import { SubscriptionInvitesController } from './SubscriptionInvitesController'
|
||||
import { results } from 'inversify-express-utils'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { InviteToSharedSubscription } from '../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
|
||||
import { AcceptSharedSubscriptionInvitation } from '../Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation'
|
||||
import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSharedSubscriptionInvitation/DeclineSharedSubscriptionInvitation'
|
||||
import { CancelSharedSubscriptionInvitation } from '../Domain/UseCase/CancelSharedSubscriptionInvitation/CancelSharedSubscriptionInvitation'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
|
||||
import { ApiVersion } from '@standardnotes/api'
|
||||
|
||||
describe('SubscriptionInvitesController', () => {
|
||||
let inviteToSharedSubscription: InviteToSharedSubscription
|
||||
@@ -19,8 +16,6 @@ describe('SubscriptionInvitesController', () => {
|
||||
let cancelSharedSubscriptionInvitation: CancelSharedSubscriptionInvitation
|
||||
let listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations
|
||||
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
let user: User
|
||||
|
||||
const createController = () =>
|
||||
@@ -51,25 +46,6 @@ describe('SubscriptionInvitesController', () => {
|
||||
|
||||
listSharedSubscriptionInvitations = {} as jest.Mocked<ListSharedSubscriptionInvitations>
|
||||
listSharedSubscriptionInvitations.execute = jest.fn()
|
||||
|
||||
request = {
|
||||
headers: {},
|
||||
body: {},
|
||||
params: {},
|
||||
} as jest.Mocked<express.Request>
|
||||
|
||||
response = {
|
||||
locals: {},
|
||||
} as jest.Mocked<express.Response>
|
||||
response.locals.user = {
|
||||
email: 'test@test.te',
|
||||
}
|
||||
response.locals.roles = [
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
name: RoleName.CoreUser,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
it('should get invitations to subscription sharing', async () => {
|
||||
@@ -77,128 +53,127 @@ describe('SubscriptionInvitesController', () => {
|
||||
invitations: [],
|
||||
})
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().listInvites(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().listInvites({ api: ApiVersion.v0, inviterEmail: 'test@test.te' })
|
||||
|
||||
expect(listSharedSubscriptionInvitations.execute).toHaveBeenCalledWith({
|
||||
inviterEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
expect(result.status).toEqual(200)
|
||||
})
|
||||
|
||||
it('should cancel invitation to subscription sharing', async () => {
|
||||
request.params.inviteUuid = '1-2-3'
|
||||
|
||||
cancelSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
|
||||
success: true,
|
||||
})
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().cancelSubscriptionSharing(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().cancelInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(cancelSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
expect(result.status).toEqual(200)
|
||||
})
|
||||
|
||||
it('should not cancel invitation to subscription sharing if the workflow fails', async () => {
|
||||
request.params.inviteUuid = '1-2-3'
|
||||
|
||||
cancelSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
|
||||
success: false,
|
||||
})
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().cancelSubscriptionSharing(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().cancelInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
expect(result.status).toEqual(400)
|
||||
})
|
||||
|
||||
it('should decline invitation to subscription sharing', async () => {
|
||||
request.params.inviteUuid = '1-2-3'
|
||||
|
||||
declineSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
|
||||
success: true,
|
||||
})
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().declineInvite(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().declineInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(declineSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
expect(result.status).toEqual(200)
|
||||
})
|
||||
|
||||
it('should not decline invitation to subscription sharing if the workflow fails', async () => {
|
||||
request.params.inviteUuid = '1-2-3'
|
||||
|
||||
declineSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
|
||||
success: false,
|
||||
})
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().declineInvite(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().declineInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(declineSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
expect(result.status).toEqual(400)
|
||||
})
|
||||
|
||||
it('should accept invitation to subscription sharing', async () => {
|
||||
request.params.inviteUuid = '1-2-3'
|
||||
|
||||
acceptSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
|
||||
success: true,
|
||||
})
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().acceptInvite(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().acceptInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(acceptSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
expect(result.status).toEqual(200)
|
||||
})
|
||||
|
||||
it('should not accept invitation to subscription sharing if the workflow fails', async () => {
|
||||
request.params.inviteUuid = '1-2-3'
|
||||
|
||||
acceptSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
|
||||
success: false,
|
||||
})
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().acceptInvite(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().acceptInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(acceptSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
expect(result.status).toEqual(400)
|
||||
})
|
||||
|
||||
it('should invite to user subscription', async () => {
|
||||
request.body.identifier = 'invitee@test.te'
|
||||
response.locals.user = {
|
||||
uuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
}
|
||||
|
||||
inviteToSharedSubscription.execute = jest.fn().mockReturnValue({
|
||||
success: true,
|
||||
})
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().inviteToSubscriptionSharing(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().invite({
|
||||
api: ApiVersion.v0,
|
||||
identifier: 'invitee@test.te',
|
||||
inviterUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
inviterRoles: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(inviteToSharedSubscription.execute).toHaveBeenCalledWith({
|
||||
inviterEmail: 'test@test.te',
|
||||
@@ -207,37 +182,36 @@ describe('SubscriptionInvitesController', () => {
|
||||
inviterRoles: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
expect(result.status).toEqual(200)
|
||||
})
|
||||
|
||||
it('should not invite to user subscription if the identifier is missing in request', async () => {
|
||||
response.locals.user = {
|
||||
uuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
}
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().inviteToSubscriptionSharing(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().invite({
|
||||
api: ApiVersion.v0,
|
||||
identifier: '',
|
||||
inviterUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
inviterRoles: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(inviteToSharedSubscription.execute).not.toHaveBeenCalled()
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
expect(result.status).toEqual(400)
|
||||
})
|
||||
|
||||
it('should not invite to user subscription if the workflow does not run', async () => {
|
||||
request.body.identifier = 'invitee@test.te'
|
||||
response.locals.user = {
|
||||
uuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
}
|
||||
|
||||
inviteToSharedSubscription.execute = jest.fn().mockReturnValue({
|
||||
success: false,
|
||||
})
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().inviteToSubscriptionSharing(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
const result = await createController().invite({
|
||||
api: ApiVersion.v0,
|
||||
identifier: 'invitee@test.te',
|
||||
inviterUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
inviterRoles: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
expect(result.status).toEqual(400)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { Role } from '@standardnotes/security'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
httpDelete,
|
||||
httpGet,
|
||||
httpPost,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
HttpStatusCode,
|
||||
SubscriptionInviteAcceptRequestParams,
|
||||
SubscriptionInviteAcceptResponse,
|
||||
SubscriptionInviteCancelRequestParams,
|
||||
SubscriptionInviteCancelResponse,
|
||||
SubscriptionInviteDeclineRequestParams,
|
||||
SubscriptionInviteDeclineResponse,
|
||||
SubscriptionInviteListRequestParams,
|
||||
SubscriptionInviteListResponse,
|
||||
SubscriptionInviteRequestParams,
|
||||
SubscriptionInviteResponse,
|
||||
SubscriptionServerInterface,
|
||||
} from '@standardnotes/api'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { AcceptSharedSubscriptionInvitation } from '../Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation'
|
||||
@@ -18,8 +22,8 @@ import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSh
|
||||
import { InviteToSharedSubscription } from '../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
|
||||
import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
|
||||
|
||||
@controller('/subscription-invites')
|
||||
export class SubscriptionInvitesController extends BaseHttpController {
|
||||
@injectable()
|
||||
export class SubscriptionInvitesController implements SubscriptionServerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.InviteToSharedSubscription) private inviteToSharedSubscription: InviteToSharedSubscription,
|
||||
@inject(TYPES.AcceptSharedSubscriptionInvitation)
|
||||
@@ -30,75 +34,103 @@ export class SubscriptionInvitesController extends BaseHttpController {
|
||||
private cancelSharedSubscriptionInvitation: CancelSharedSubscriptionInvitation,
|
||||
@inject(TYPES.ListSharedSubscriptionInvitations)
|
||||
private listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
) {}
|
||||
|
||||
@httpGet('/:inviteUuid/accept')
|
||||
async acceptInvite(request: Request): Promise<results.JsonResult> {
|
||||
async acceptInvite(params: SubscriptionInviteAcceptRequestParams): Promise<SubscriptionInviteAcceptResponse> {
|
||||
const result = await this.acceptSharedSubscriptionInvitation.execute({
|
||||
sharedSubscriptionInvitationUuid: request.params.inviteUuid,
|
||||
sharedSubscriptionInvitationUuid: params.inviteUuid,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
@httpGet('/:inviteUuid/decline')
|
||||
async declineInvite(request: Request): Promise<results.JsonResult> {
|
||||
async declineInvite(params: SubscriptionInviteDeclineRequestParams): Promise<SubscriptionInviteDeclineResponse> {
|
||||
const result = await this.declineSharedSubscriptionInvitation.execute({
|
||||
sharedSubscriptionInvitationUuid: request.params.inviteUuid,
|
||||
sharedSubscriptionInvitationUuid: params.inviteUuid,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.ApiGatewayAuthMiddleware)
|
||||
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (!request.body.identifier) {
|
||||
return this.json({ error: { message: 'Missing invitee identifier' } }, 400)
|
||||
async invite(params: SubscriptionInviteRequestParams): Promise<SubscriptionInviteResponse> {
|
||||
if (!params.identifier) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: 'Missing invitee identifier',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.inviteToSharedSubscription.execute({
|
||||
inviterEmail: response.locals.user.email,
|
||||
inviterUuid: response.locals.user.uuid,
|
||||
inviteeIdentifier: request.body.identifier,
|
||||
inviterRoles: response.locals.roles.map((role: Role) => role.name),
|
||||
inviterEmail: params.inviterEmail as string,
|
||||
inviterUuid: params.inviterUuid as string,
|
||||
inviteeIdentifier: params.identifier,
|
||||
inviterRoles: params.inviterRoles as RoleName[],
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
@httpDelete('/:inviteUuid', TYPES.ApiGatewayAuthMiddleware)
|
||||
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
async cancelInvite(params: SubscriptionInviteCancelRequestParams): Promise<SubscriptionInviteCancelResponse> {
|
||||
const result = await this.cancelSharedSubscriptionInvitation.execute({
|
||||
sharedSubscriptionInvitationUuid: request.params.inviteUuid,
|
||||
inviterEmail: response.locals.user.email,
|
||||
sharedSubscriptionInvitationUuid: params.inviteUuid,
|
||||
inviterEmail: params.inviterEmail as string,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
@httpGet('/', TYPES.ApiGatewayAuthMiddleware)
|
||||
async listInvites(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
async listInvites(params: SubscriptionInviteListRequestParams): Promise<SubscriptionInviteListResponse> {
|
||||
const result = await this.listSharedSubscriptionInvitations.execute({
|
||||
inviterEmail: response.locals.user.email,
|
||||
inviterEmail: params.inviterEmail as string,
|
||||
})
|
||||
|
||||
return this.json(result)
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,23 @@ import { Request, Response } from 'express'
|
||||
import { results } from 'inversify-express-utils'
|
||||
import { ValetTokenController } from './ValetTokenController'
|
||||
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||
import { Uuid, ValidatorInterface } from '@standardnotes/common'
|
||||
|
||||
describe('ValetTokenController', () => {
|
||||
let createValetToken: CreateValetToken
|
||||
let uuidValidator: ValidatorInterface<Uuid>
|
||||
let request: Request
|
||||
let response: Response
|
||||
|
||||
const createController = () => new ValetTokenController(createValetToken)
|
||||
const createController = () => new ValetTokenController(createValetToken, uuidValidator)
|
||||
|
||||
beforeEach(() => {
|
||||
createValetToken = {} as jest.Mocked<CreateValetToken>
|
||||
createValetToken.execute = jest.fn().mockReturnValue({ success: true, valetToken: 'foobar' })
|
||||
|
||||
uuidValidator = {} as jest.Mocked<ValidatorInterface<Uuid>>
|
||||
uuidValidator.validate = jest.fn().mockReturnValue(true)
|
||||
|
||||
request = {
|
||||
body: {
|
||||
operation: 'write',
|
||||
@@ -42,6 +47,17 @@ describe('ValetTokenController', () => {
|
||||
expect(await result.content.readAsStringAsync()).toEqual('{"success":true,"valetToken":"foobar"}')
|
||||
})
|
||||
|
||||
it('should not create a valet token if the remote resource identifier is not a valid uuid', async () => {
|
||||
uuidValidator.validate = jest.fn().mockReturnValue(false)
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().create(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(createValetToken.execute).not.toHaveBeenCalled()
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should create a read valet token for read only access session', async () => {
|
||||
response.locals.readOnlyAccess = true
|
||||
request.body.operation = 'read'
|
||||
|
||||
@@ -11,11 +11,15 @@ import { CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { ErrorTag, Uuid, ValidatorInterface } from '@standardnotes/common'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
|
||||
@controller('/valet-tokens', TYPES.ApiGatewayAuthMiddleware)
|
||||
export class ValetTokenController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.CreateValetToken) private createValetKey: CreateValetToken) {
|
||||
constructor(
|
||||
@inject(TYPES.CreateValetToken) private createValetKey: CreateValetToken,
|
||||
@inject(TYPES.UuidValidator) private uuidValitor: ValidatorInterface<Uuid>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@@ -35,9 +39,23 @@ export class ValetTokenController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
for (const resource of payload.resources) {
|
||||
if (!this.uuidValitor.validate(resource.remoteIdentifier)) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
tag: ErrorTag.ParametersInvalid,
|
||||
message: 'Invalid remote resource identifier.',
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const createValetKeyResponse = await this.createValetKey.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
operation: payload.operation,
|
||||
operation: payload.operation as ValetTokenOperation,
|
||||
resources: payload.resources,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,65 +1,28 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as express from 'express'
|
||||
import { results } from 'inversify-express-utils'
|
||||
|
||||
import { AddWebSocketsConnection } from '../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
|
||||
|
||||
import { WebSocketsController } from './WebSocketsController'
|
||||
import { RemoveWebSocketsConnection } from '../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
|
||||
describe('WebSocketsController', () => {
|
||||
let addWebSocketsConnection: AddWebSocketsConnection
|
||||
let removeWebSocketsConnection: RemoveWebSocketsConnection
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
let createWebSocketConnectionToken: CreateWebSocketConnectionToken
|
||||
|
||||
const createController = () => new WebSocketsController(addWebSocketsConnection, removeWebSocketsConnection)
|
||||
const createController = () => new WebSocketsController(createWebSocketConnectionToken)
|
||||
|
||||
beforeEach(() => {
|
||||
addWebSocketsConnection = {} as jest.Mocked<AddWebSocketsConnection>
|
||||
addWebSocketsConnection.execute = jest.fn()
|
||||
|
||||
removeWebSocketsConnection = {} as jest.Mocked<RemoveWebSocketsConnection>
|
||||
removeWebSocketsConnection.execute = jest.fn()
|
||||
|
||||
request = {
|
||||
body: {
|
||||
userUuid: '1-2-3',
|
||||
},
|
||||
params: {},
|
||||
headers: {},
|
||||
} as jest.Mocked<express.Request>
|
||||
request.params.connectionId = '2-3-4'
|
||||
|
||||
response = {
|
||||
locals: {},
|
||||
} as jest.Mocked<express.Response>
|
||||
response.locals.user = {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
createWebSocketConnectionToken = {} as jest.Mocked<CreateWebSocketConnectionToken>
|
||||
createWebSocketConnectionToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
|
||||
})
|
||||
|
||||
it('should persist an established web sockets connection', async () => {
|
||||
const httpResponse = await createController().storeWebSocketsConnection(request, response)
|
||||
it('should create a web sockets connection token', async () => {
|
||||
const response = await createController().createConnectionToken({ userUuid: '1-2-3' })
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.JsonResult)
|
||||
expect((<results.JsonResult>httpResponse).statusCode).toEqual(200)
|
||||
|
||||
expect(addWebSocketsConnection.execute).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
connectionId: '2-3-4',
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: { token: 'foobar' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove a disconnected web sockets connection', async () => {
|
||||
const httpResponse = await createController().deleteWebSocketsConnection(request)
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.JsonResult)
|
||||
expect((<results.JsonResult>httpResponse).statusCode).toEqual(200)
|
||||
|
||||
expect(removeWebSocketsConnection.execute).toHaveBeenCalledWith({
|
||||
connectionId: '2-3-4',
|
||||
expect(createWebSocketConnectionToken.execute).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,45 +1,29 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
httpDelete,
|
||||
httpPost,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
HttpStatusCode,
|
||||
WebSocketConnectionTokenRequestParams,
|
||||
WebSocketConnectionTokenResponse,
|
||||
WebSocketServerInterface,
|
||||
} from '@standardnotes/api'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { AddWebSocketsConnection } from '../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
|
||||
import { RemoveWebSocketsConnection } from '../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
|
||||
@controller('/sockets')
|
||||
export class WebSocketsController extends BaseHttpController {
|
||||
@injectable()
|
||||
export class WebSocketsController implements WebSocketServerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.AddWebSocketsConnection) private addWebSocketsConnection: AddWebSocketsConnection,
|
||||
@inject(TYPES.RemoveWebSocketsConnection) private removeWebSocketsConnection: RemoveWebSocketsConnection,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@inject(TYPES.CreateWebSocketConnectionToken)
|
||||
private createWebSocketConnectionToken: CreateWebSocketConnectionToken,
|
||||
) {}
|
||||
|
||||
@httpPost('/:connectionId', TYPES.ApiGatewayAuthMiddleware)
|
||||
async storeWebSocketsConnection(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.addWebSocketsConnection.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
connectionId: request.params.connectionId,
|
||||
})
|
||||
async createConnectionToken(
|
||||
params: WebSocketConnectionTokenRequestParams,
|
||||
): Promise<WebSocketConnectionTokenResponse> {
|
||||
const result = await this.createWebSocketConnectionToken.execute({ userUuid: params.userUuid as string })
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
|
||||
@httpDelete('/:connectionId')
|
||||
async deleteWebSocketsConnection(
|
||||
request: Request,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.removeWebSocketsConnection.execute({ connectionId: request.params.connectionId })
|
||||
|
||||
return this.json({ success: true })
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ describe('FeatureService', () => {
|
||||
uuid: 'subscription-1-1-1',
|
||||
createdAt: 111,
|
||||
updatedAt: 222,
|
||||
renewedAt: null,
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
endsAt: 555,
|
||||
user: Promise.resolve(user),
|
||||
@@ -95,6 +96,7 @@ describe('FeatureService', () => {
|
||||
uuid: 'subscription-2-2-2',
|
||||
createdAt: 222,
|
||||
updatedAt: 333,
|
||||
renewedAt: null,
|
||||
planName: SubscriptionName.ProPlan,
|
||||
endsAt: 777,
|
||||
user: Promise.resolve(user),
|
||||
@@ -108,6 +110,7 @@ describe('FeatureService', () => {
|
||||
uuid: 'subscription-3-3-3-canceled',
|
||||
createdAt: 111,
|
||||
updatedAt: 222,
|
||||
renewedAt: null,
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
endsAt: 333,
|
||||
user: Promise.resolve(user),
|
||||
@@ -121,6 +124,7 @@ describe('FeatureService', () => {
|
||||
uuid: 'subscription-4-4-4-canceled',
|
||||
createdAt: 111,
|
||||
updatedAt: 222,
|
||||
renewedAt: null,
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
endsAt: 333,
|
||||
user: Promise.resolve(user),
|
||||
@@ -240,6 +244,7 @@ describe('FeatureService', () => {
|
||||
uuid: 'subscription-1-1-1',
|
||||
createdAt: 111,
|
||||
updatedAt: 222,
|
||||
renewedAt: null,
|
||||
planName: 'non existing plan name' as SubscriptionName,
|
||||
endsAt: 555,
|
||||
user: Promise.resolve(user),
|
||||
|
||||
@@ -13,7 +13,6 @@ import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyti
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('SubscriptionCancelledEventHandler', () => {
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
@@ -24,7 +23,6 @@ describe('SubscriptionCancelledEventHandler', () => {
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
let timestamp: number
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionCancelledEventHandler(
|
||||
@@ -34,7 +32,6 @@ describe('SubscriptionCancelledEventHandler', () => {
|
||||
getUserAnalyticsId,
|
||||
analyticsStore,
|
||||
statisticsStore,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -75,9 +72,6 @@ describe('SubscriptionCancelledEventHandler', () => {
|
||||
offline: false,
|
||||
replaced: false,
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
})
|
||||
|
||||
it('should update subscription cancelled', async () => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/Offl
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -26,17 +25,8 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||
if (event.payload.offline) {
|
||||
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.updateSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
|
||||
|
||||
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
|
||||
if (user !== null) {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
@@ -50,16 +40,33 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
||||
if (subscriptions.length !== 0) {
|
||||
const lastSubscription = subscriptions.shift() as UserSubscription
|
||||
const subscriptionLength = event.payload.timestamp - lastSubscription.createdAt
|
||||
this.logger.info(
|
||||
`Canceling subscription ${lastSubscription.uuid} - lasted for ${subscriptionLength} microseconds`,
|
||||
)
|
||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
const lastPurchaseTime = lastSubscription.renewedAt ?? lastSubscription.updatedAt
|
||||
const remainingSubscriptionTime = lastSubscription.endsAt - event.payload.timestamp
|
||||
const totalSubscriptionTime = lastSubscription.endsAt - lastPurchaseTime
|
||||
|
||||
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
||||
|
||||
await this.statisticsStore.incrementMeasure(
|
||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
||||
remainingSubscriptionPercentage,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (event.payload.offline) {
|
||||
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.updateSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
|
||||
}
|
||||
|
||||
private async updateSubscriptionCancelled(subscriptionId: number, timestamp: number): Promise<void> {
|
||||
|
||||
+4
@@ -7,5 +7,9 @@ export interface SharedSubscriptionInvitationRepositoryInterface {
|
||||
findOneByUuidAndStatus(uuid: Uuid, status: InvitationStatus): Promise<SharedSubscriptionInvitation | null>
|
||||
findOneByUuid(uuid: Uuid): Promise<SharedSubscriptionInvitation | null>
|
||||
findByInviterEmail(inviterEmail: string): Promise<SharedSubscriptionInvitation[]>
|
||||
findOneByInviteeAndInviterEmail(
|
||||
inviteeEmail: string,
|
||||
inviterEmail: string,
|
||||
): Promise<SharedSubscriptionInvitation | null>
|
||||
countByInviterEmailAndStatus(inviterEmail: Uuid, statuses: InvitationStatus[]): Promise<number>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ import { Uuid } from '@standardnotes/common'
|
||||
import { SubscriptionToken } from './SubscriptionToken'
|
||||
|
||||
export interface SubscriptionTokenRepositoryInterface {
|
||||
save(subscriptionToken: SubscriptionToken): Promise<void>
|
||||
save(subscriptionToken: SubscriptionToken): Promise<boolean>
|
||||
getUserUuidByToken(token: string): Promise<Uuid | undefined>
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ export class UserSubscription {
|
||||
@Index('updated_at')
|
||||
declare updatedAt: number
|
||||
|
||||
@Column({
|
||||
name: 'renewed_at',
|
||||
type: 'bigint',
|
||||
nullable: true,
|
||||
})
|
||||
declare renewedAt: number | null
|
||||
|
||||
@Column({
|
||||
type: 'tinyint',
|
||||
width: 1,
|
||||
|
||||
+20
-13
@@ -16,6 +16,7 @@ import { DomainEventPublisherInterface, SharedSubscriptionInvitationCanceledEven
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { InviterIdentifierType } from '../../SharedSubscription/InviterIdentifierType'
|
||||
import { InviteeIdentifierType } from '../../SharedSubscription/InviteeIdentifierType'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('CancelSharedSubscriptionInvitation', () => {
|
||||
let sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface
|
||||
@@ -28,6 +29,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
let invitation: SharedSubscriptionInvitation
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () =>
|
||||
new CancelSharedSubscriptionInvitation(
|
||||
@@ -38,6 +40,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
timer,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -60,6 +63,9 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
inviteeIdentifierType: InviteeIdentifierType.Email,
|
||||
} as jest.Mocked<SharedSubscriptionInvitation>
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
|
||||
sharedSubscriptionInvitationRepository = {} as jest.Mocked<SharedSubscriptionInvitationRepositoryInterface>
|
||||
sharedSubscriptionInvitationRepository.findOneByUuid = jest.fn().mockReturnValue(invitation)
|
||||
sharedSubscriptionInvitationRepository.save = jest.fn()
|
||||
@@ -126,7 +132,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should cancel a shared subscription invitation without subscription removal is subscription is not found', async () => {
|
||||
it('should cancel a shared subscription invitation without subscription removal if subscription is not found', async () => {
|
||||
userSubscriptionRepository.findOneByUserUuidAndSubscriptionId = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
@@ -175,7 +181,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should not cancel a shared subscription invitation if invitee is not found', async () => {
|
||||
it('should cancel a shared subscription invitation without subscription removal if invitee is not found', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
@@ -183,20 +189,21 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
inviterEmail: 'test@test.te',
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
success: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not cancel a shared subscription invitation if invitee is not found', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
expect(sharedSubscriptionInvitationRepository.save).toHaveBeenCalledWith({
|
||||
status: 'canceled',
|
||||
subscriptionId: 3,
|
||||
updatedAt: 1,
|
||||
inviterIdentifier: 'test@test.te',
|
||||
uuid: '1-2-3',
|
||||
inviterIdentifierType: 'email',
|
||||
inviteeIdentifier: 'invitee@test.te',
|
||||
inviteeIdentifierType: 'email',
|
||||
})
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not cancel a shared subscription invitation if inviter subscription is not found', async () => {
|
||||
|
||||
+27
-18
@@ -2,6 +2,7 @@ import { SubscriptionName } from '@standardnotes/common'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
@@ -29,6 +30,7 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: CancelSharedSubscriptionInvitationDTO): Promise<CancelSharedSubscriptionInvitationResponse> {
|
||||
@@ -36,29 +38,34 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
dto.sharedSubscriptionInvitationUuid,
|
||||
)
|
||||
if (sharedSubscriptionInvitation === null) {
|
||||
this.logger.debug(
|
||||
`Could not find a shared subscription invitation with uuid ${dto.sharedSubscriptionInvitationUuid}`,
|
||||
)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.inviterEmail !== sharedSubscriptionInvitation.inviterIdentifier) {
|
||||
this.logger.debug(
|
||||
`Subscription belongs to a different inviter (${sharedSubscriptionInvitation.inviterIdentifier}). Modifier: ${dto.inviterEmail}`,
|
||||
)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
|
||||
const invitee = await this.userRepository.findOneByEmail(sharedSubscriptionInvitation.inviteeIdentifier)
|
||||
if (invitee === null) {
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
|
||||
const inviterUserSubscriptions = await this.userSubscriptionRepository.findBySubscriptionIdAndType(
|
||||
sharedSubscriptionInvitation.subscriptionId,
|
||||
UserSubscriptionType.Regular,
|
||||
)
|
||||
if (inviterUserSubscriptions.length !== 1) {
|
||||
if (inviterUserSubscriptions.length === 0) {
|
||||
this.logger.debug(`Could not find a regular subscription with id ${sharedSubscriptionInvitation.subscriptionId}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
@@ -70,20 +77,22 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
|
||||
await this.sharedSubscriptionInvitationRepository.save(sharedSubscriptionInvitation)
|
||||
|
||||
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
|
||||
if (invitee !== null) {
|
||||
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
|
||||
|
||||
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
|
||||
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
|
||||
inviteeIdentifier: invitee.uuid,
|
||||
inviteeIdentifierType: InviteeIdentifierType.Uuid,
|
||||
inviterEmail: sharedSubscriptionInvitation.inviterIdentifier,
|
||||
inviterSubscriptionId: sharedSubscriptionInvitation.subscriptionId,
|
||||
inviterSubscriptionUuid: inviterUserSubscription.uuid,
|
||||
sharedSubscriptionInvitationUuid: sharedSubscriptionInvitation.uuid,
|
||||
}),
|
||||
)
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
|
||||
inviteeIdentifier: invitee.uuid,
|
||||
inviteeIdentifierType: InviteeIdentifierType.Uuid,
|
||||
inviterEmail: sharedSubscriptionInvitation.inviterIdentifier,
|
||||
inviterSubscriptionId: sharedSubscriptionInvitation.subscriptionId,
|
||||
inviterSubscriptionUuid: inviterUserSubscription.uuid,
|
||||
sharedSubscriptionInvitationUuid: sharedSubscriptionInvitation.uuid,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { Role } from '../../Role/Role'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { GetUserAnalyticsId } from '../GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
|
||||
|
||||
describe('CreateCrossServiceToken', () => {
|
||||
let userProjector: ProjectorInterface<User>
|
||||
let sessionProjector: ProjectorInterface<Session>
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let userRepository: UserRepositoryInterface
|
||||
const jwtTTL = 60
|
||||
|
||||
let session: Session
|
||||
let user: User
|
||||
let role: Role
|
||||
|
||||
const createUseCase = (analyticsEnabled = true) =>
|
||||
new CreateCrossServiceToken(
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
getUserAnalyticsId,
|
||||
userRepository,
|
||||
analyticsEnabled,
|
||||
jwtTTL,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
user.roles = Promise.resolve([role])
|
||||
|
||||
userProjector = {} as jest.Mocked<ProjectorInterface<User>>
|
||||
userProjector.projectSimple = jest.fn().mockReturnValue({ bar: 'baz' })
|
||||
|
||||
roleProjector = {} as jest.Mocked<ProjectorInterface<Role>>
|
||||
roleProjector.projectSimple = jest.fn().mockReturnValue({ name: 'role1', uuid: '1-3-4' })
|
||||
|
||||
sessionProjector = {} as jest.Mocked<ProjectorInterface<Session>>
|
||||
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
sessionProjector.projectSimple = jest.fn().mockReturnValue({ test: 'test' })
|
||||
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<CrossServiceTokenData>>
|
||||
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 123 })
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user', async () => {
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user - analytics disabled', async () => {
|
||||
await createUseCase(false).execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user without a session', async () => {
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user by user uuid', async () => {
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if user does not exist', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
|
||||
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
|
||||
import { Role } from '../../Role/Role'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { GetUserAnalyticsId } from '../GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
|
||||
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
|
||||
|
||||
@injectable()
|
||||
export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.UserProjector) private userProjector: ProjectorInterface<User>,
|
||||
@inject(TYPES.SessionProjector) private sessionProjector: ProjectorInterface<Session>,
|
||||
@inject(TYPES.RoleProjector) private roleProjector: ProjectorInterface<Role>,
|
||||
@inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.ANALYTICS_ENABLED) private analyticsEnabled: boolean,
|
||||
@inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
|
||||
let user: User | undefined | null = dto.user
|
||||
if (user === undefined && dto.userUuid !== undefined) {
|
||||
user = await this.userRepository.findOneByUuid(dto.userUuid)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with uuid ${dto.userUuid}`)
|
||||
}
|
||||
|
||||
const roles = await user.roles
|
||||
|
||||
const authTokenData: CrossServiceTokenData = {
|
||||
user: this.projectUser(user),
|
||||
roles: this.projectRoles(roles),
|
||||
}
|
||||
|
||||
if (this.analyticsEnabled) {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
authTokenData.analyticsId = analyticsId
|
||||
}
|
||||
|
||||
if (dto.session !== undefined) {
|
||||
authTokenData.session = this.projectSession(dto.session)
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
|
||||
}
|
||||
}
|
||||
|
||||
private projectUser(user: User): { uuid: string; email: string } {
|
||||
return <{ uuid: string; email: string }>this.userProjector.projectSimple(user)
|
||||
}
|
||||
|
||||
private projectSession(session: Session): {
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
} {
|
||||
return <
|
||||
{
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
}
|
||||
>this.sessionProjector.projectSimple(session)
|
||||
}
|
||||
|
||||
private projectRoles(roles: Array<Role>): Array<{ uuid: string; name: RoleName }> {
|
||||
return roles.map((role) => <{ uuid: string; name: RoleName }>this.roleProjector.projectSimple(role))
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Either, Uuid } from '@standardnotes/common'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
|
||||
export type CreateCrossServiceTokenDTO = Either<
|
||||
{
|
||||
user: User
|
||||
session?: Session
|
||||
},
|
||||
{
|
||||
userUuid: Uuid
|
||||
}
|
||||
>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export type CreateCrossServiceTokenResponse = {
|
||||
token: string
|
||||
}
|
||||
+22
-2
@@ -5,17 +5,19 @@ import { TimerInterface } from '@standardnotes/time'
|
||||
import { SubscriptionTokenRepositoryInterface } from '../../Subscription/SubscriptionTokenRepositoryInterface'
|
||||
|
||||
import { CreateSubscriptionToken } from './CreateSubscriptionToken'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('CreateSubscriptionToken', () => {
|
||||
let subscriptionTokenRepository: SubscriptionTokenRepositoryInterface
|
||||
let cryptoNode: CryptoNode
|
||||
let timer: TimerInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new CreateSubscriptionToken(subscriptionTokenRepository, cryptoNode, timer)
|
||||
const createUseCase = () => new CreateSubscriptionToken(subscriptionTokenRepository, cryptoNode, timer, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionTokenRepository = {} as jest.Mocked<SubscriptionTokenRepositoryInterface>
|
||||
subscriptionTokenRepository.save = jest.fn()
|
||||
subscriptionTokenRepository.save = jest.fn().mockReturnValue(true)
|
||||
|
||||
cryptoNode = {} as jest.Mocked<CryptoNode>
|
||||
cryptoNode.generateRandomKey = jest.fn().mockReturnValueOnce('random-string')
|
||||
@@ -23,6 +25,9 @@ describe('CreateSubscriptionToken', () => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(1)
|
||||
timer.getUTCDateNHoursAhead = jest.fn().mockReturnValue(new Date(1))
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should create an subscription token and persist it', async () => {
|
||||
@@ -36,4 +41,19 @@ describe('CreateSubscriptionToken', () => {
|
||||
expiresAt: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if the subscription token was not created', async () => {
|
||||
subscriptionTokenRepository.save = jest.fn().mockReturnValue(false)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CryptoNode } from '@standardnotes/sncrypto-node'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { SubscriptionTokenRepositoryInterface } from '../../Subscription/SubscriptionTokenRepositoryInterface'
|
||||
@@ -15,6 +16,7 @@ export class CreateSubscriptionToken implements UseCaseInterface {
|
||||
private subscriptionTokenRepository: SubscriptionTokenRepositoryInterface,
|
||||
@inject(TYPES.CryptoNode) private cryptoNode: CryptoNode,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateSubscriptionTokenDTO): Promise<CreateSubscriptionTokenResponse> {
|
||||
@@ -26,7 +28,13 @@ export class CreateSubscriptionToken implements UseCaseInterface {
|
||||
expiresAt: this.timer.convertStringDateToMicroseconds(this.timer.getUTCDateNHoursAhead(3).toString()),
|
||||
}
|
||||
|
||||
await this.subscriptionTokenRepository.save(subscriptionToken)
|
||||
const subscriptionTokenWasSaved = await this.subscriptionTokenRepository.save(subscriptionToken)
|
||||
|
||||
if (!subscriptionTokenWasSaved) {
|
||||
this.logger.error(`Could not create subscription token for user ${dto.userUuid}`)
|
||||
|
||||
throw new Error('Could not create subscription token')
|
||||
}
|
||||
|
||||
return {
|
||||
subscriptionToken,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
|
||||
import { TokenEncoderInterface, ValetTokenData, ValetTokenOperation } from '@standardnotes/security'
|
||||
import { CreateValetToken } from './CreateValetToken'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
@@ -70,7 +70,7 @@ describe('CreateValetToken', () => {
|
||||
|
||||
it('should create a read valet token', async () => {
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'read',
|
||||
operation: ValetTokenOperation.Read,
|
||||
userUuid: '1-2-3',
|
||||
resources: [
|
||||
{
|
||||
@@ -92,7 +92,7 @@ describe('CreateValetToken', () => {
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'read',
|
||||
operation: ValetTokenOperation.Read,
|
||||
userUuid: '1-2-3',
|
||||
resources: [
|
||||
{
|
||||
@@ -117,7 +117,7 @@ describe('CreateValetToken', () => {
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(150)
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'read',
|
||||
operation: ValetTokenOperation.Read,
|
||||
userUuid: '1-2-3',
|
||||
resources: [
|
||||
{
|
||||
@@ -135,7 +135,7 @@ describe('CreateValetToken', () => {
|
||||
|
||||
it('should not create a write valet token if unencrypted file size has not been provided for a resource', async () => {
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
resources: [
|
||||
{
|
||||
remoteIdentifier: '2-3-4',
|
||||
@@ -152,7 +152,7 @@ describe('CreateValetToken', () => {
|
||||
|
||||
it('should create a write valet token', async () => {
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
resources: [
|
||||
{
|
||||
remoteIdentifier: '2-3-4',
|
||||
@@ -192,7 +192,7 @@ describe('CreateValetToken', () => {
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription })
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
resources: [
|
||||
{
|
||||
remoteIdentifier: '2-3-4',
|
||||
@@ -232,7 +232,7 @@ describe('CreateValetToken', () => {
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription })
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
resources: [
|
||||
{
|
||||
remoteIdentifier: '2-3-4',
|
||||
@@ -252,7 +252,7 @@ describe('CreateValetToken', () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
userUuid: '1-2-3',
|
||||
resources: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
|
||||
export type CreateValetTokenDTO = CreateValetTokenPayload & {
|
||||
export type CreateValetTokenDTO = {
|
||||
operation: ValetTokenOperation
|
||||
resources: Array<{
|
||||
remoteIdentifier: string
|
||||
unencryptedFileSize?: number
|
||||
}>
|
||||
userUuid: string
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TokenEncoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
|
||||
import { CreateWebSocketConnectionToken } from './CreateWebSocketConnectionToken'
|
||||
|
||||
describe('CreateWebSocketConnection', () => {
|
||||
let tokenEncoder: TokenEncoderInterface<WebSocketConnectionTokenData>
|
||||
const tokenTTL = 30
|
||||
|
||||
const createUseCase = () => new CreateWebSocketConnectionToken(tokenEncoder, tokenTTL)
|
||||
|
||||
beforeEach(() => {
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<WebSocketConnectionTokenData>>
|
||||
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
|
||||
})
|
||||
|
||||
it('should create a web socket connection token', async () => {
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3' })
|
||||
|
||||
expect(result.token).toEqual('foobar')
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith({ userUuid: '1-2-3' }, 30)
|
||||
})
|
||||
})
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export type CreateWebSocketConnectionDTO = {
|
||||
userUuid: string
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export type CreateWebSocketConnectionResponse = {
|
||||
token: string
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { TokenEncoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateWebSocketConnectionDTO } from './CreateWebSocketConnectionDTO'
|
||||
import { CreateWebSocketConnectionResponse } from './CreateWebSocketConnectionResponse'
|
||||
|
||||
@injectable()
|
||||
export class CreateWebSocketConnectionToken implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.WebSocketConnectionTokenEncoder)
|
||||
private tokenEncoder: TokenEncoderInterface<WebSocketConnectionTokenData>,
|
||||
@inject(TYPES.WEB_SOCKET_CONNECTION_TOKEN_TTL) private tokenTTL: number,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateWebSocketConnectionDTO): Promise<CreateWebSocketConnectionResponse> {
|
||||
const data: WebSocketConnectionTokenData = {
|
||||
userUuid: dto.userUuid,
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.tokenEncoder.encodeExpirableToken(data, this.tokenTTL),
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -10,6 +10,8 @@ import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubs
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
|
||||
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
|
||||
|
||||
describe('InviteToSharedSubscription', () => {
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
@@ -40,6 +42,7 @@ describe('InviteToSharedSubscription', () => {
|
||||
sharedSubscriptionInvitationRepository = {} as jest.Mocked<SharedSubscriptionInvitationRepositoryInterface>
|
||||
sharedSubscriptionInvitationRepository.save = jest.fn().mockImplementation((same) => ({ ...same, uuid: '1-2-3' }))
|
||||
sharedSubscriptionInvitationRepository.countByInviterEmailAndStatus = jest.fn().mockReturnValue(2)
|
||||
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
@@ -181,4 +184,49 @@ describe('InviteToSharedSubscription', () => {
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create an invitation if it already exists', async () => {
|
||||
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest
|
||||
.fn()
|
||||
.mockReturnValue({ status: InvitationStatus.Sent } as jest.Mocked<SharedSubscriptionInvitation>)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
inviteeIdentifier: 'invitee@test.te',
|
||||
inviterUuid: '1-2-3',
|
||||
inviterEmail: 'inviter@test.te',
|
||||
inviterRoles: [RoleName.ProUser],
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
})
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
|
||||
expect(domainEventFactory.createSharedSubscriptionInvitationCreatedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create an invitation if it already exists but was canceled', async () => {
|
||||
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest
|
||||
.fn()
|
||||
.mockReturnValue({ status: InvitationStatus.Canceled } as jest.Mocked<SharedSubscriptionInvitation>)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
inviteeIdentifier: 'invitee@test.te',
|
||||
inviterUuid: '1-2-3',
|
||||
inviterEmail: 'inviter@test.te',
|
||||
inviterRoles: [RoleName.ProUser],
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).toHaveBeenCalled()
|
||||
|
||||
expect(domainEventFactory.createSharedSubscriptionInvitationCreatedEvent).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
+10
@@ -53,6 +53,16 @@ export class InviteToSharedSubscription implements UseCaseInterface {
|
||||
}
|
||||
}
|
||||
|
||||
const existingInvitation = await this.sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail(
|
||||
dto.inviteeIdentifier,
|
||||
dto.inviterEmail,
|
||||
)
|
||||
if (existingInvitation !== null && existingInvitation.status !== InvitationStatus.Canceled) {
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
|
||||
const sharedSubscriptionInvition = new SharedSubscriptionInvitation()
|
||||
sharedSubscriptionInvition.inviterIdentifier = dto.inviterEmail
|
||||
sharedSubscriptionInvition.inviterIdentifierType = InviterIdentifierType.Email
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
|
||||
import { MuteFailedBackupsEmails } from './MuteFailedBackupsEmails'
|
||||
|
||||
describe('MuteFailedBackupsEmails', () => {
|
||||
let settingRepository: SettingRepositoryInterface
|
||||
|
||||
const createUseCase = () => new MuteFailedBackupsEmails(settingRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
const setting = {} as jest.Mocked<Setting>
|
||||
|
||||
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(setting)
|
||||
settingRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should not succeed if extension setting is not found', async () => {
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should update mute email setting on extension setting', async () => {
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
})
|
||||
|
||||
expect(settingRepository.save).toHaveBeenCalledWith({
|
||||
value: 'muted',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
import { MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { MuteFailedBackupsEmailsDTO } from './MuteFailedBackupsEmailsDTO'
|
||||
import { MuteFailedBackupsEmailsResponse } from './MuteFailedBackupsEmailsResponse'
|
||||
|
||||
@injectable()
|
||||
export class MuteFailedBackupsEmails implements UseCaseInterface {
|
||||
constructor(@inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface) {}
|
||||
|
||||
async execute(dto: MuteFailedBackupsEmailsDTO): Promise<MuteFailedBackupsEmailsResponse> {
|
||||
const setting = await this.settingRepository.findOneByUuidAndNames(dto.settingUuid, [
|
||||
SettingName.MuteFailedBackupsEmails,
|
||||
SettingName.MuteFailedCloudBackupsEmails,
|
||||
])
|
||||
|
||||
if (setting === null) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
}
|
||||
}
|
||||
|
||||
setting.value = MuteFailedBackupsEmailsOption.Muted
|
||||
|
||||
await this.settingRepository.save(setting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type MuteFailedBackupsEmailsDTO = {
|
||||
settingUuid: string
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export type MuteFailedBackupsEmailsResponse = {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
|
||||
import { MuteMarketingEmails } from './MuteMarketingEmails'
|
||||
|
||||
describe('MuteMarketingEmails', () => {
|
||||
let settingRepository: SettingRepositoryInterface
|
||||
|
||||
const createUseCase = () => new MuteMarketingEmails(settingRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
const setting = {} as jest.Mocked<Setting>
|
||||
|
||||
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(setting)
|
||||
settingRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should not succeed if extension setting is not found', async () => {
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should update mute email setting on extension setting', async () => {
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
})
|
||||
|
||||
expect(settingRepository.save).toHaveBeenCalledWith({
|
||||
value: 'muted',
|
||||
serverEncryptionVersion: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
import { MuteMarketingEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { MuteMarketingEmailsDTO } from './MuteMarketingEmailsDTO'
|
||||
import { MuteMarketingEmailsResponse } from './MuteMarketingEmailsResponse'
|
||||
|
||||
@injectable()
|
||||
export class MuteMarketingEmails implements UseCaseInterface {
|
||||
constructor(@inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface) {}
|
||||
|
||||
async execute(dto: MuteMarketingEmailsDTO): Promise<MuteMarketingEmailsResponse> {
|
||||
const setting = await this.settingRepository.findOneByUuidAndNames(dto.settingUuid, [
|
||||
SettingName.MuteMarketingEmails,
|
||||
])
|
||||
|
||||
if (setting === null) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
}
|
||||
}
|
||||
|
||||
setting.value = MuteMarketingEmailsOption.Muted
|
||||
setting.serverEncryptionVersion = EncryptionVersion.Unencrypted
|
||||
|
||||
await this.settingRepository.save(setting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type MuteMarketingEmailsDTO = {
|
||||
settingUuid: string
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export type MuteMarketingEmailsResponse = {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
|
||||
import { MuteSignInEmails } from './MuteSignInEmails'
|
||||
|
||||
describe('MuteSignInEmails', () => {
|
||||
let settingRepository: SettingRepositoryInterface
|
||||
|
||||
const createUseCase = () => new MuteSignInEmails(settingRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
const setting = {} as jest.Mocked<Setting>
|
||||
|
||||
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(setting)
|
||||
settingRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should not succeed if extension setting is not found', async () => {
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should update mute email setting on extension setting', async () => {
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
})
|
||||
|
||||
expect(settingRepository.save).toHaveBeenCalledWith({
|
||||
value: 'muted',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { MuteSignInEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { MuteSignInEmailsDTO } from './MuteSignInEmailsDTO'
|
||||
import { MuteSignInEmailsResponse } from './MuteSignInEmailsResponse'
|
||||
|
||||
@injectable()
|
||||
export class MuteSignInEmails implements UseCaseInterface {
|
||||
constructor(@inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface) {}
|
||||
|
||||
async execute(dto: MuteSignInEmailsDTO): Promise<MuteSignInEmailsResponse> {
|
||||
const setting = await this.settingRepository.findOneByUuidAndNames(dto.settingUuid, [SettingName.MuteSignInEmails])
|
||||
|
||||
if (setting === null) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
}
|
||||
}
|
||||
|
||||
setting.value = MuteSignInEmailsOption.Muted
|
||||
|
||||
await this.settingRepository.save(setting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type MuteSignInEmailsDTO = {
|
||||
settingUuid: string
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export type MuteSignInEmailsResponse = {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { ApiVersion } from '@standardnotes/api'
|
||||
import { Role } from '@standardnotes/security'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
httpDelete,
|
||||
httpGet,
|
||||
httpPost,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { SubscriptionInvitesController } from '../../Controller/SubscriptionInvitesController'
|
||||
|
||||
@controller('/subscription-invites')
|
||||
export class InversifyExpressSubscriptionInvitesController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.SubscriptionInvitesController) private subscriptionInvitesController: SubscriptionInvitesController,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpGet('/:inviteUuid/accept')
|
||||
async acceptInvite(request: Request): Promise<results.JsonResult> {
|
||||
const response = await this.subscriptionInvitesController.acceptInvite({
|
||||
api: request.query.api as ApiVersion,
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
})
|
||||
|
||||
return this.json(response.data, response.status)
|
||||
}
|
||||
|
||||
@httpGet('/:inviteUuid/decline')
|
||||
async declineInvite(request: Request): Promise<results.JsonResult> {
|
||||
const response = await this.subscriptionInvitesController.declineInvite({
|
||||
api: request.query.api as ApiVersion,
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
})
|
||||
|
||||
return this.json(response.data, response.status)
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.ApiGatewayAuthMiddleware)
|
||||
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.subscriptionInvitesController.invite({
|
||||
...request.body,
|
||||
inviterEmail: response.locals.user.email,
|
||||
inviterUuid: response.locals.user.uuid,
|
||||
inviterRoles: response.locals.roles.map((role: Role) => role.name),
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
@httpDelete('/:inviteUuid', TYPES.ApiGatewayAuthMiddleware)
|
||||
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.subscriptionInvitesController.cancelInvite({
|
||||
...request.body,
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
inviterEmail: response.locals.user.email,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
@httpGet('/', TYPES.ApiGatewayAuthMiddleware)
|
||||
async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.subscriptionInvitesController.listInvites({
|
||||
...request.body,
|
||||
inviterEmail: response.locals.user.email,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { WebSocketServerInterface } from '@standardnotes/api'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
httpDelete,
|
||||
httpPost,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AddWebSocketsConnection } from '../../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
|
||||
import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
import { RemoveWebSocketsConnection } from '../../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
|
||||
|
||||
@controller('/sockets')
|
||||
export class InversifyExpressWebSocketsController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.AddWebSocketsConnection) private addWebSocketsConnection: AddWebSocketsConnection,
|
||||
@inject(TYPES.RemoveWebSocketsConnection) private removeWebSocketsConnection: RemoveWebSocketsConnection,
|
||||
@inject(TYPES.CreateCrossServiceToken) private createCrossServiceToken: CreateCrossServiceToken,
|
||||
@inject(TYPES.WebSocketsController) private webSocketsController: WebSocketServerInterface,
|
||||
@inject(TYPES.WebSocketConnectionTokenDecoder)
|
||||
private tokenDecoder: TokenDecoderInterface<WebSocketConnectionTokenData>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/tokens', TYPES.ApiGatewayAuthMiddleware)
|
||||
async createConnectionToken(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.webSocketsController.createConnectionToken({
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
@httpPost('/tokens/validate')
|
||||
async validateToken(request: Request): Promise<results.JsonResult> {
|
||||
if (!request.headers.authorization) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
tag: ErrorTag.AuthInvalid,
|
||||
message: 'Invalid authorization token.',
|
||||
},
|
||||
},
|
||||
401,
|
||||
)
|
||||
}
|
||||
|
||||
const token: WebSocketConnectionTokenData | undefined = this.tokenDecoder.decodeToken(request.headers.authorization)
|
||||
|
||||
if (token === undefined) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
tag: ErrorTag.AuthInvalid,
|
||||
message: 'Invalid authorization token.',
|
||||
},
|
||||
},
|
||||
401,
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.createCrossServiceToken.execute({
|
||||
userUuid: token.userUuid,
|
||||
})
|
||||
|
||||
return this.json({ authToken: result.token })
|
||||
}
|
||||
|
||||
@httpPost('/connections/:connectionId', TYPES.ApiGatewayAuthMiddleware)
|
||||
async storeWebSocketsConnection(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.addWebSocketsConnection.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
connectionId: request.params.connectionId,
|
||||
})
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
|
||||
@httpDelete('/connections/:connectionId')
|
||||
async deleteWebSocketsConnection(
|
||||
request: Request,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.removeWebSocketsConnection.execute({ connectionId: request.params.connectionId })
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,23 @@ describe('MySQLSharedSubscriptionInvitationRepository', () => {
|
||||
expect(result).toEqual(invitation)
|
||||
})
|
||||
|
||||
it('should find one invitation by invitee and inviter email', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
|
||||
|
||||
const result = await createRepository().findOneByInviteeAndInviterEmail('invitee@test.te', 'inviter@test.te')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith(
|
||||
'invitation.inviter_identifier = :inviterEmail AND invitation.invitee_identifier = :inviteeEmail',
|
||||
{
|
||||
inviterEmail: 'inviter@test.te',
|
||||
inviteeEmail: 'invitee@test.te',
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).toEqual(invitation)
|
||||
})
|
||||
|
||||
it('should find one invitation by uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
|
||||
|
||||
@@ -13,6 +13,19 @@ export class MySQLSharedSubscriptionInvitationRepository implements SharedSubscr
|
||||
private ormRepository: Repository<SharedSubscriptionInvitation>,
|
||||
) {}
|
||||
|
||||
async findOneByInviteeAndInviterEmail(
|
||||
inviteeEmail: string,
|
||||
inviterEmail: string,
|
||||
): Promise<SharedSubscriptionInvitation | null> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('invitation')
|
||||
.where('invitation.inviter_identifier = :inviterEmail AND invitation.invitee_identifier = :inviteeEmail', {
|
||||
inviterEmail,
|
||||
inviteeEmail,
|
||||
})
|
||||
.getOne()
|
||||
}
|
||||
|
||||
async save(sharedSubscriptionInvitation: SharedSubscriptionInvitation): Promise<SharedSubscriptionInvitation> {
|
||||
return this.ormRepository.save(sharedSubscriptionInvitation)
|
||||
}
|
||||
|
||||
@@ -138,7 +138,8 @@ describe('MySQLUserSubscriptionRepository', () => {
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
updatedAt: expect.any(Number),
|
||||
updatedAt: 1000,
|
||||
renewedAt: 1000,
|
||||
endsAt: 1000,
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
|
||||
@@ -189,6 +190,7 @@ describe('MySQLUserSubscriptionRepository', () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
|
||||
|
||||
selectQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
|
||||
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
|
||||
|
||||
const result = await createRepository().findBySubscriptionIdAndType(123, UserSubscriptionType.Regular)
|
||||
@@ -200,6 +202,7 @@ describe('MySQLUserSubscriptionRepository', () => {
|
||||
type: 'regular',
|
||||
},
|
||||
)
|
||||
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('created_at', 'DESC')
|
||||
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
|
||||
expect(result).toEqual([subscription])
|
||||
})
|
||||
|
||||
@@ -44,6 +44,7 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
|
||||
subscriptionId,
|
||||
type,
|
||||
})
|
||||
.orderBy('created_at', 'DESC')
|
||||
.getMany()
|
||||
}
|
||||
|
||||
@@ -87,13 +88,14 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
|
||||
return null
|
||||
}
|
||||
|
||||
async updateEndsAt(subscriptionId: number, endsAt: number, updatedAt: number): Promise<void> {
|
||||
async updateEndsAt(subscriptionId: number, endsAt: number, timestamp: number): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({
|
||||
endsAt,
|
||||
updatedAt,
|
||||
updatedAt: timestamp,
|
||||
renewedAt: timestamp,
|
||||
})
|
||||
.where('subscription_id = :subscriptionId', {
|
||||
subscriptionId,
|
||||
|
||||
@@ -15,7 +15,6 @@ describe('LockRepository', () => {
|
||||
redisClient.expire = jest.fn()
|
||||
redisClient.del = jest.fn()
|
||||
redisClient.get = jest.fn()
|
||||
redisClient.set = jest.fn()
|
||||
redisClient.setex = jest.fn()
|
||||
})
|
||||
|
||||
@@ -88,6 +87,6 @@ describe('LockRepository', () => {
|
||||
it('should update a lock counter', async () => {
|
||||
await createRepository().updateLockCounter('123', 3)
|
||||
|
||||
expect(redisClient.set).toHaveBeenCalledWith('lock:123', 3)
|
||||
expect(redisClient.setex).toHaveBeenCalledWith('lock:123', 120, 3)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,7 +30,7 @@ export class LockRepository implements LockRepositoryInterface {
|
||||
}
|
||||
|
||||
async updateLockCounter(userIdentifier: string, counter: number): Promise<void> {
|
||||
await this.redisClient.set(`${this.PREFIX}:${userIdentifier}`, counter)
|
||||
await this.redisClient.setex(`${this.PREFIX}:${userIdentifier}`, this.failedLoginLockout, counter)
|
||||
}
|
||||
|
||||
async getLockCounter(userIdentifier: string): Promise<number> {
|
||||
|
||||
@@ -14,9 +14,9 @@ describe('RedisSubscriptionTokenRepository', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
redisClient = {} as jest.Mocked<IORedis.Redis>
|
||||
redisClient.set = jest.fn()
|
||||
redisClient.set = jest.fn().mockReturnValue('OK')
|
||||
redisClient.get = jest.fn()
|
||||
redisClient.expireat = jest.fn()
|
||||
redisClient.expireat = jest.fn().mockReturnValue(1)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(1)
|
||||
@@ -45,7 +45,23 @@ describe('RedisSubscriptionTokenRepository', () => {
|
||||
expiresAt: 123,
|
||||
}
|
||||
|
||||
await createRepository().save(subscriptionToken)
|
||||
expect(await createRepository().save(subscriptionToken)).toBeTruthy()
|
||||
|
||||
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')
|
||||
|
||||
expect(redisClient.expireat).toHaveBeenCalledWith('subscription-token:random-string', 1)
|
||||
})
|
||||
|
||||
it('should indicate subscription token was not saved', async () => {
|
||||
redisClient.set = jest.fn().mockReturnValue(null)
|
||||
|
||||
const subscriptionToken: SubscriptionToken = {
|
||||
userUuid: '1-2-3',
|
||||
token: 'random-string',
|
||||
expiresAt: 123,
|
||||
}
|
||||
|
||||
expect(await createRepository().save(subscriptionToken)).toBeFalsy()
|
||||
|
||||
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')
|
||||
|
||||
|
||||
@@ -24,11 +24,13 @@ export class RedisSubscriptionTokenRepository implements SubscriptionTokenReposi
|
||||
return userUuid
|
||||
}
|
||||
|
||||
async save(subscriptionToken: SubscriptionToken): Promise<void> {
|
||||
async save(subscriptionToken: SubscriptionToken): Promise<boolean> {
|
||||
const key = `${this.PREFIX}:${subscriptionToken.token}`
|
||||
const expiresAtTimestampInSeconds = this.timer.convertMicrosecondsToSeconds(subscriptionToken.expiresAt)
|
||||
|
||||
await this.redisClient.set(key, subscriptionToken.userUuid)
|
||||
await this.redisClient.expireat(key, expiresAtTimestampInSeconds)
|
||||
const wasSet = await this.redisClient.set(key, subscriptionToken.userUuid)
|
||||
const timeoutWasSet = await this.redisClient.expireat(key, expiresAtTimestampInSeconds)
|
||||
|
||||
return wasSet === 'OK' && timeoutWasSet !== 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,28 +17,31 @@ describe('SettingProjector', () => {
|
||||
serverEncryptionVersion: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
} as jest.Mocked<Setting>
|
||||
})
|
||||
|
||||
it('should create a simple projection of a setting', async () => {
|
||||
const projection = await createProjector().projectSimple(setting)
|
||||
expect(projection).toEqual({
|
||||
expect(projection).toStrictEqual({
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
})
|
||||
})
|
||||
it('should create a simple projection of list of settings', async () => {
|
||||
const projection = await createProjector().projectManySimple([setting])
|
||||
expect(projection).toEqual([
|
||||
expect(projection).toStrictEqual([
|
||||
{
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -6,16 +6,16 @@ import { SimpleSetting } from '../Domain/Setting/SimpleSetting'
|
||||
@injectable()
|
||||
export class SettingProjector {
|
||||
async projectSimple(setting: Setting): Promise<SimpleSetting> {
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
user,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
serverEncryptionVersion,
|
||||
...rest
|
||||
} = setting
|
||||
|
||||
return rest
|
||||
return {
|
||||
uuid: setting.uuid,
|
||||
name: setting.name,
|
||||
value: setting.value,
|
||||
createdAt: setting.createdAt,
|
||||
updatedAt: setting.updatedAt,
|
||||
sensitive: setting.sensitive,
|
||||
}
|
||||
}
|
||||
|
||||
async projectManySimple(settings: Setting[]): Promise<SimpleSetting[]> {
|
||||
return Promise.all(
|
||||
settings.map(async (setting) => {
|
||||
|
||||
@@ -17,28 +17,31 @@ describe('SubscriptionSettingProjector', () => {
|
||||
serverEncryptionVersion: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
} as jest.Mocked<SubscriptionSetting>
|
||||
})
|
||||
|
||||
it('should create a simple projection of a setting', async () => {
|
||||
const projection = await createProjector().projectSimple(setting)
|
||||
expect(projection).toEqual({
|
||||
expect(projection).toStrictEqual({
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
})
|
||||
})
|
||||
it('should create a simple projection of list of settings', async () => {
|
||||
const projection = await createProjector().projectManySimple([setting])
|
||||
expect(projection).toEqual([
|
||||
expect(projection).toStrictEqual([
|
||||
{
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -6,16 +6,16 @@ import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
|
||||
@injectable()
|
||||
export class SubscriptionSettingProjector {
|
||||
async projectSimple(setting: SubscriptionSetting): Promise<SimpleSubscriptionSetting> {
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
userSubscription,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
serverEncryptionVersion,
|
||||
...rest
|
||||
} = setting
|
||||
|
||||
return rest
|
||||
return {
|
||||
uuid: setting.uuid,
|
||||
name: setting.name,
|
||||
value: setting.value,
|
||||
createdAt: setting.createdAt,
|
||||
updatedAt: setting.updatedAt,
|
||||
sensitive: setting.sensitive,
|
||||
}
|
||||
}
|
||||
|
||||
async projectManySimple(settings: SubscriptionSetting[]): Promise<SimpleSubscriptionSetting[]> {
|
||||
return Promise.all(
|
||||
settings.map(async (setting) => {
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.33.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.32.0...@standardnotes/common@1.33.0) (2022-09-19)
|
||||
|
||||
### Features
|
||||
|
||||
* **files:** add validating remote identifiers ([db15457](https://github.com/standardnotes/server/commit/db15457ce4eb533ec822cf93c3ed83eafe9e64d5))
|
||||
|
||||
# [1.32.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.31.0...@standardnotes/common@1.32.0) (2022-09-09)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/common",
|
||||
"version": "1.32.0",
|
||||
"version": "1.33.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { UuidValidator } from './UuidValidator'
|
||||
|
||||
describe('UuidValidator', () => {
|
||||
const createValidator = () => new UuidValidator()
|
||||
|
||||
const validUuids = [
|
||||
'2221101c-1da9-4d2b-9b32-b8be2a8d1c82',
|
||||
'c08f2f29-a74b-42b4-aefd-98af9832391c',
|
||||
'b453fa64-1493-443b-b5bb-bca7b9c696c7',
|
||||
]
|
||||
|
||||
const invalidUuids = [
|
||||
123,
|
||||
'someone@127.0.0.1',
|
||||
'',
|
||||
null,
|
||||
'b453fa64-1493-443b-b5bb-ca7b9c696c7',
|
||||
'c08f*f29-a74b-42b4-aefd-98af9832391c',
|
||||
'c08f*f29-a74b-42b4-aefd-98af9832391c',
|
||||
'../../escaped.sh',
|
||||
]
|
||||
|
||||
it('should validate proper uuids', () => {
|
||||
for (const validUuid of validUuids) {
|
||||
expect(createValidator().validate(validUuid)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('should not validate invalid uuids', () => {
|
||||
for (const invalidUuid of invalidUuids) {
|
||||
expect(createValidator().validate(invalidUuid as string)).toBeFalsy()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Uuid } from '../DataType/Uuid'
|
||||
import { ValidatorInterface } from './ValidatorInterface'
|
||||
|
||||
export class UuidValidator implements ValidatorInterface<Uuid> {
|
||||
private readonly UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
|
||||
validate(data: Uuid): boolean {
|
||||
return String(data).toLowerCase().match(this.UUID_REGEX) !== null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ValidatorInterface<T> {
|
||||
validate(data: T): boolean
|
||||
}
|
||||
@@ -20,3 +20,5 @@ export * from './Role/RoleName'
|
||||
export * from './Subscription/SubscriptionName'
|
||||
export * from './Type/Either'
|
||||
export * from './Type/Only'
|
||||
export * from './Validator/UuidValidator'
|
||||
export * from './Validator/ValidatorInterface'
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.8.13](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.12...@standardnotes/domain-events-infra@1.8.13) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.8.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.11...@standardnotes/domain-events-infra@1.8.12) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.8.11](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.10...@standardnotes/domain-events-infra@1.8.11) (2022-09-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.8.10](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.9...@standardnotes/domain-events-infra@1.8.10) (2022-09-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.8.9](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.8...@standardnotes/domain-events-infra@1.8.9) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.8.9",
|
||||
"version": "1.8.13",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.60.7](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.6...@standardnotes/domain-events@2.60.7) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
## [2.60.6](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.5...@standardnotes/domain-events@2.60.6) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
## [2.60.5](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.4...@standardnotes/domain-events@2.60.5) (2022-09-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
## [2.60.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.3...@standardnotes/domain-events@2.60.4) (2022-09-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
## [2.60.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.2...@standardnotes/domain-events@2.60.3) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user