Compare commits

..

65 Commits

Author SHA1 Message Date
standardci
65675a21d6 chore(release): publish new version
- @standardnotes/api-gateway@1.21.1
2022-09-21 13:56:25 +00:00
Karol Sójko
d35de38289 fix(api-gateway): web socket connection routing 2022-09-21 15:54:57 +02:00
standardci
83e1baa978 chore(release): publish new version
- @standardnotes/auth-server@1.31.1
2022-09-21 13:53:16 +00:00
Karol Sójko
875edce5b1 fix(auth): web sockets routes 2022-09-21 15:51:46 +02:00
standardci
1baa504728 chore(release): publish new version
- @standardnotes/api-gateway@1.21.0
 - @standardnotes/auth-server@1.31.0
2022-09-21 11:57:48 +00:00
Karol Sójko
965ae79414 feat(auth): add creating cross service token in exchange for web socket connection token 2022-09-21 13:56:17 +02:00
standardci
7a8448c116 chore(release): publish new version
- @standardnotes/auth-server@1.30.1
2022-09-21 09:15:22 +00:00
Karol Sójko
d935157ee8 fix(auth): missing injectable annotation 2022-09-21 11:13:24 +02:00
standardci
9313e6b568 chore(release): publish new version
- @standardnotes/api-gateway@1.20.0
 - @standardnotes/auth-server@1.30.0
 - @standardnotes/domain-events-infra@1.8.12
 - @standardnotes/domain-events@2.60.6
 - @standardnotes/event-store@1.3.17
 - @standardnotes/files-server@1.6.3
 - @standardnotes/scheduler-server@1.10.31
 - @standardnotes/security@1.4.0
 - @standardnotes/syncing-server@1.8.8
2022-09-21 09:00:32 +00:00
Karol Sójko
8033177f48 feat(auth): add creating web socket connection tokens 2022-09-21 10:58:39 +02:00
standardci
11011fa15d chore(release): publish new version
- @standardnotes/syncing-server@1.8.7
2022-09-20 08:01:52 +00:00
Karol Sójko
c2e9f3e72b fix(syncing-server): content size calculation and add syncing upper bound for limit paramter 2022-09-20 09:59:40 +02:00
standardci
f0fb7fd1cd chore(release): publish new version
- @standardnotes/files-server@1.6.2
2022-09-19 11:55:08 +00:00
Karol Sójko
15e342fd51 Merge pull request #224 from standardnotes/fs_dos
fix: add upper bound for FS file chunk upload
2022-09-19 13:53:39 +02:00
Karol Sójko
dfa7e06f87 fix: add upper bound for FS file chunk upload 2022-09-19 13:44:37 +02:00
standardci
a9aef5521b chore(release): publish new version
- @standardnotes/auth-server@1.29.1
 - @standardnotes/files-server@1.6.1
2022-09-19 07:59:14 +00:00
Karol Sójko
a628bdc44e fix(files): uuid validator binding 2022-09-19 09:57:17 +02:00
Karol Sójko
db6f966045 fix(auth): uuid validator binding 2022-09-19 09:57:10 +02:00
standardci
9b602ed405 chore(release): publish new version
- @standardnotes/api-gateway@1.19.6
 - @standardnotes/auth-server@1.29.0
 - @standardnotes/common@1.33.0
 - @standardnotes/domain-events-infra@1.8.11
 - @standardnotes/domain-events@2.60.5
 - @standardnotes/event-store@1.3.16
 - @standardnotes/files-server@1.6.0
 - @standardnotes/predicates@1.4.2
 - @standardnotes/scheduler-server@1.10.30
 - @standardnotes/security@1.3.3
 - @standardnotes/syncing-server@1.8.6
2022-09-19 07:45:26 +00:00
Karol Sójko
db15457ce4 feat(files): add validating remote identifiers 2022-09-19 09:43:46 +02:00
standardci
719d8558a3 chore(release): publish new version
- @standardnotes/auth-server@1.28.4
2022-09-16 10:36:18 +00:00
Karol Sójko
c207c3fc84 fix(auth): feature service spec 2022-09-16 12:34:43 +02:00
standardci
4bde4758c3 chore(release): publish new version
- @standardnotes/analytics@1.29.1
 - @standardnotes/api-gateway@1.19.5
 - @standardnotes/auth-server@1.28.3
 - @standardnotes/syncing-server@1.8.5
2022-09-16 10:19:03 +00:00
Karol Sójko
5eb957c82a fix(auth): change remaining subscription time stats to percentage 2022-09-16 12:17:34 +02:00
standardci
0b38617acf chore(release): publish new version
- @standardnotes/api-gateway@1.19.4
 - @standardnotes/auth-server@1.28.2
 - @standardnotes/domain-events-infra@1.8.10
 - @standardnotes/domain-events@2.60.4
 - @standardnotes/event-store@1.3.15
 - @standardnotes/files-server@1.5.52
 - @standardnotes/scheduler-server@1.10.29
 - @standardnotes/security@1.3.2
 - @standardnotes/syncing-server@1.8.4
2022-09-16 08:55:36 +00:00
Karol Sójko
377d32c449 fix(files): add verifying permitted operation on valet token 2022-09-16 10:52:25 +02:00
standardci
cdfb0c2603 chore(release): publish new version
- @standardnotes/auth-server@1.28.1
2022-09-15 12:19:43 +00:00
Karol Sójko
d85152429c fix(auth): missing injectable annotation 2022-09-15 14:17:56 +02:00
standardci
422e596fc7 chore(release): publish new version
- @standardnotes/api-gateway@1.19.3
2022-09-15 10:39:57 +00:00
Karol Sójko
89334c9022 fix(api-gateway): add remaining subscription time to stats 2022-09-15 12:38:28 +02:00
standardci
f5a0e88ab9 chore(release): publish new version
- @standardnotes/analytics@1.29.0
 - @standardnotes/api-gateway@1.19.2
 - @standardnotes/auth-server@1.28.0
 - @standardnotes/syncing-server@1.8.3
2022-09-15 10:23:29 +00:00
Karol Sójko
a59ba08339 feat(auth): add remaining subscription time stats 2022-09-15 12:21:59 +02:00
standardci
2641056c51 chore(release): publish new version
- @standardnotes/auth-server@1.27.0
2022-09-15 10:14:51 +00:00
Karol Sójko
5d812befc4 feat(auth): implement subscription server interface on server side 2022-09-15 12:12:50 +02:00
standardci
1c592d6f96 chore(release): publish new version
- @standardnotes/auth-server@1.26.1
2022-09-15 08:44:32 +00:00
Karol Sójko
531f13fe1f fix(auth): disallow duplicating subscription invites 2022-09-15 10:43:07 +02:00
standardci
4757cc8dae chore(release): publish new version
- @standardnotes/syncing-server@1.8.2
2022-09-15 08:27:49 +00:00
Karol Sójko
ecdfe9ecc0 fix(syncing-server): files count stats 2022-09-15 10:25:55 +02:00
standardci
d19cb08e9c chore(release): publish new version
- @standardnotes/auth-server@1.26.0
2022-09-13 13:48:14 +00:00
Karol Sójko
f45320e5ed feat(auth): add subscription sharing permission 2022-09-13 15:46:11 +02:00
standardci
93ded34de9 chore(release): publish new version
- @standardnotes/auth-server@1.25.13
2022-09-12 18:08:27 +00:00
Karol Sójko
dd13e2eaf7 fix(auth): add debug logs for canceling shared subscription invitations 2022-09-12 20:06:36 +02:00
standardci
1405c6f260 chore(release): publish new version
- @standardnotes/auth-server@1.25.12
2022-09-12 12:26:19 +00:00
Karol Sójko
0dab31f993 fix(auth): allow canceling shared subscription invitation before it was accepted 2022-09-12 14:24:52 +02:00
standardci
8070c70152 chore(release): publish new version
- @standardnotes/api-gateway@1.19.1
 - @standardnotes/auth-server@1.25.11
 - @standardnotes/common@1.32.0
 - @standardnotes/domain-events-infra@1.8.9
 - @standardnotes/domain-events@2.60.3
 - @standardnotes/event-store@1.3.14
 - @standardnotes/files-server@1.5.51
 - @standardnotes/predicates@1.4.1
 - @standardnotes/scheduler-server@1.10.28
 - @standardnotes/security@1.3.1
 - @standardnotes/syncing-server@1.8.1
2022-09-09 12:04:24 +00:00
Karol Sójko
c3ebb321cf feat(common): add either and only types 2022-09-09 14:02:25 +02:00
standardci
e54deb594a chore(release): publish new version
- @standardnotes/event-store@1.3.13
2022-09-09 10:13:14 +00:00
Karol Sójko
432d071ec8 fix(event-store): add missing event subscriptions 2022-09-09 12:11:45 +02:00
standardci
b9c06f1f5d chore(release): publish new version
- @standardnotes/analytics@1.28.0
 - @standardnotes/api-gateway@1.19.0
 - @standardnotes/auth-server@1.25.10
 - @standardnotes/syncing-server@1.8.0
2022-09-09 09:46:00 +00:00
Karol Sójko
52cc6462a6 feat(syncing-server): add tracking files count in stats 2022-09-09 11:44:02 +02:00
standardci
35c2afef67 chore(release): publish new version
- @standardnotes/analytics@1.27.0
 - @standardnotes/api-gateway@1.18.0
 - @standardnotes/auth-server@1.25.9
 - @standardnotes/syncing-server@1.7.1
2022-09-09 09:02:40 +00:00
Karol Sójko
339c86fca0 fix(api-gateway): add general activity breakdown to yesterdays report stats 2022-09-09 11:01:06 +02:00
Karol Sójko
0afd3de977 feat(api-gateway): add tracking general activity for free and paid users breakdown 2022-09-09 10:59:46 +02:00
standardci
e699569d46 chore(release): publish new version
- @standardnotes/api-gateway@1.17.4
2022-09-09 08:51:10 +00:00
Karol Sójko
ced852d9db fix(api-gateway): add notes count statistics to report 2022-09-09 10:49:15 +02:00
standardci
a63612613e chore(release): publish new version
- @standardnotes/analytics@1.26.0
 - @standardnotes/api-gateway@1.17.3
 - @standardnotes/auth-server@1.25.8
 - @standardnotes/syncing-server@1.7.0
2022-09-09 08:44:23 +00:00
Karol Sójko
c9ec7b492a feat(syncing-server): add statistics for notes count for free and paid users 2022-09-09 10:42:12 +02:00
standardci
bf8ffc07ee chore(release): publish new version
- @standardnotes/event-store@1.3.12
2022-09-08 14:11:09 +00:00
Karol Sójko
73e1ea7f93 fix(event-store): add listening to refund processed event 2022-09-08 16:08:57 +02:00
standardci
5979b99398 chore(release): publish new version
- @standardnotes/api-gateway@1.17.2
 - @standardnotes/auth-server@1.25.7
 - @standardnotes/domain-events-infra@1.8.8
 - @standardnotes/domain-events@2.60.2
 - @standardnotes/event-store@1.3.11
 - @standardnotes/files-server@1.5.50
 - @standardnotes/scheduler-server@1.10.27
 - @standardnotes/syncing-server@1.6.70
2022-09-08 13:27:38 +00:00
Karol Sójko
50ddb918cc fix(api-gateway): retention data structure to include both period keys 2022-09-08 15:25:41 +02:00
standardci
6b19eb8876 chore(release): publish new version
- @standardnotes/api-gateway@1.17.1
 - @standardnotes/auth-server@1.25.6
 - @standardnotes/domain-events-infra@1.8.7
 - @standardnotes/domain-events@2.60.1
 - @standardnotes/event-store@1.3.10
 - @standardnotes/files-server@1.5.49
 - @standardnotes/scheduler-server@1.10.26
 - @standardnotes/syncing-server@1.6.69
2022-09-08 13:13:13 +00:00
Karol Sójko
47be0841fc fix(api-gateway): retention data structure 2022-09-08 15:11:12 +02:00
standardci
99c7bb70fc chore(release): publish new version
- @standardnotes/api-gateway@1.17.0
 - @standardnotes/auth-server@1.25.5
 - @standardnotes/domain-events-infra@1.8.6
 - @standardnotes/domain-events@2.60.0
 - @standardnotes/event-store@1.3.9
 - @standardnotes/files-server@1.5.48
 - @standardnotes/scheduler-server@1.10.25
 - @standardnotes/syncing-server@1.6.68
2022-09-08 09:54:52 +00:00
Karol Sójko
f139bb0036 feat(api-gateway): add registration-to-activity retention analytics to report 2022-09-08 11:52:44 +02:00
118 changed files with 2453 additions and 594 deletions

110
.pnp.cjs generated
View File

@@ -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"\
}]\
@@ -2506,6 +2507,7 @@ 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/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/security", "workspace:packages/security"],\
@@ -2561,7 +2563,7 @@ 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"],\
@@ -2686,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"\
@@ -2741,6 +2742,17 @@ 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"\
}]\
]],\
["@standardnotes/files-server", [\
@@ -2796,14 +2808,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"]\
],\
@@ -2839,6 +2851,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": [\
@@ -2931,21 +2954,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/",\
@@ -2959,6 +2967,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": [\
@@ -3070,6 +3086,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", [\
@@ -5843,6 +5870,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.

View File

@@ -3,6 +3,36 @@
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
* **syncing-server:** add tracking files count in stats ([52cc646](https://github.com/standardnotes/server/commit/52cc6462a66dae3bd6c05f551d4ba661c8a9b8c8))
# [1.27.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.26.0...@standardnotes/analytics@1.27.0) (2022-09-09)
### Features
* **api-gateway:** add tracking general activity for free and paid users breakdown ([0afd3de](https://github.com/standardnotes/server/commit/0afd3de9779e2abe10deede24626a3cbe6b15e6c))
# [1.26.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.25.0...@standardnotes/analytics@1.26.0) (2022-09-09)
### Features
* **syncing-server:** add statistics for notes count for free and paid users ([c9ec7b4](https://github.com/standardnotes/server/commit/c9ec7b492aea1911e441ed8ad9a155f871be2ef7))
# [1.25.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.24.0...@standardnotes/analytics@1.25.0) (2022-09-07)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "1.25.0",
"version": "1.29.1",
"engines": {
"node": ">=14.0.0 <17.0.0"
},

View File

@@ -1,6 +1,9 @@
export enum AnalyticsActivity {
GeneralActivity = 'general-activity',
GeneralActivityFreeUsers = 'general-activity-free-users',
GeneralActivityPaidUsers = 'general-activity-paid-users',
EditingItems = 'editing-items',
CheckingIntegrity = 'checking-integrity',
Login = 'login',
Register = 'register',
DeleteAccount = 'DeleteAccount',

View File

@@ -3,5 +3,9 @@ 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',
FilesCount = 'files-count',
}

View File

@@ -3,6 +3,96 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
# [1.19.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.18.0...@standardnotes/api-gateway@1.19.0) (2022-09-09)
### Features
* **syncing-server:** add tracking files count in stats ([52cc646](https://github.com/standardnotes/api-gateway/commit/52cc6462a66dae3bd6c05f551d4ba661c8a9b8c8))
# [1.18.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.4...@standardnotes/api-gateway@1.18.0) (2022-09-09)
### Bug Fixes
* **api-gateway:** add general activity breakdown to yesterdays report stats ([339c86f](https://github.com/standardnotes/api-gateway/commit/339c86fca073b02054260417b7519c08874e1e4e))
### Features
* **api-gateway:** add tracking general activity for free and paid users breakdown ([0afd3de](https://github.com/standardnotes/api-gateway/commit/0afd3de9779e2abe10deede24626a3cbe6b15e6c))
## [1.17.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.3...@standardnotes/api-gateway@1.17.4) (2022-09-09)
### Bug Fixes
* **api-gateway:** add notes count statistics to report ([ced852d](https://github.com/standardnotes/api-gateway/commit/ced852d9dbf8cab4c235b94a834968a5fc5e7d36))
## [1.17.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.2...@standardnotes/api-gateway@1.17.3) (2022-09-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.17.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.1...@standardnotes/api-gateway@1.17.2) (2022-09-08)
### Bug Fixes
* **api-gateway:** retention data structure to include both period keys ([50ddb91](https://github.com/standardnotes/api-gateway/commit/50ddb918ccc52bee4caad82504cb899bc5936150))
## [1.17.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.0...@standardnotes/api-gateway@1.17.1) (2022-09-08)
### Bug Fixes
* **api-gateway:** retention data structure ([47be084](https://github.com/standardnotes/api-gateway/commit/47be0841fc6d5fa00892e775bb3a40f404a6382b))
# [1.17.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.8...@standardnotes/api-gateway@1.17.0) (2022-09-08)
### Features
* **api-gateway:** add registration-to-activity retention analytics to report ([f139bb0](https://github.com/standardnotes/api-gateway/commit/f139bb003669bb41f98ad4bb59a036c489f43606))
## [1.16.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.7...@standardnotes/api-gateway@1.16.8) (2022-09-08)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -16,6 +16,7 @@ import {
AnalyticsActivity,
AnalyticsStoreInterface,
Period,
PeriodKeyGeneratorInterface,
StatisticsMeasure,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
@@ -24,6 +25,7 @@ const requestReport = async (
analyticsStore: AnalyticsStoreInterface,
statisticsStore: StatisticsStoreInterface,
domainEventPublisher: DomainEventPublisherInterface,
periodKeyGenerator: PeriodKeyGeneratorInterface,
): Promise<void> => {
const analyticsOverTime = []
@@ -68,6 +70,8 @@ const requestReport = async (
const yesterdayActivityNames = [
AnalyticsActivity.LimitedDiscountOfferPurchased,
AnalyticsActivity.GeneralActivity,
AnalyticsActivity.GeneralActivityFreeUsers,
AnalyticsActivity.GeneralActivityPaidUsers,
AnalyticsActivity.PaymentFailed,
AnalyticsActivity.PaymentSuccess,
]
@@ -90,6 +94,10 @@ const requestReport = async (
StatisticsMeasure.RegistrationLength,
StatisticsMeasure.SubscriptionLength,
StatisticsMeasure.RegistrationToSubscriptionTime,
StatisticsMeasure.RemainingSubscriptionTimePercentage,
StatisticsMeasure.NotesCountFreeUsers,
StatisticsMeasure.NotesCountPaidUsers,
StatisticsMeasure.FilesCount,
]
const statisticMeasures = []
for (const statisticMeasureName of statisticMeasureNames) {
@@ -103,6 +111,25 @@ const requestReport = async (
}
}
const periodKeys = periodKeyGenerator.getDiscretePeriodKeys(Period.Last7Days)
const retentionOverDays = []
for (let i = 0; i < periodKeys.length; i++) {
for (let j = 0; j < periodKeys.length - i; j++) {
const dailyRetention = await analyticsStore.calculateActivitiesRetention({
firstActivity: AnalyticsActivity.Register,
firstActivityPeriodKey: periodKeys[i],
secondActivity: AnalyticsActivity.GeneralActivity,
secondActivityPeriodKey: periodKeys[i + j],
})
retentionOverDays.push({
firstPeriodKey: periodKeys[i],
secondPeriodKey: periodKeys[i + j],
value: dailyRetention,
})
}
}
const event: DailyAnalyticsReportGeneratedEvent = {
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
createdAt: new Date(),
@@ -120,6 +147,16 @@ const requestReport = async (
activityStatistics: yesterdayActivityStatistics,
activityStatisticsOverTime: analyticsOverTime,
statisticMeasures,
retentionStatistics: [
{
firstActivity: AnalyticsActivity.Register,
secondActivity: AnalyticsActivity.GeneralActivity,
retention: {
periodKeys,
values: retentionOverDays,
},
},
],
},
}
@@ -138,8 +175,9 @@ void container.load().then((container) => {
const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore)
const statisticsStore: StatisticsStoreInterface = container.get(TYPES.StatisticsStore)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
Promise.resolve(requestReport(analyticsStore, statisticsStore, domainEventPublisher))
Promise.resolve(requestReport(analyticsStore, statisticsStore, domainEventPublisher, periodKeyGenerator))
.then(() => {
logger.info('Usage report generation complete')

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.16.8",
"version": "1.21.1",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -25,6 +25,7 @@
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.3.0",
"@standardnotes/analytics": "workspace:*",
"@standardnotes/common": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/security": "workspace:*",

View File

@@ -6,6 +6,7 @@ import * as AWS from 'aws-sdk'
import {
AnalyticsStoreInterface,
PeriodKeyGenerator,
PeriodKeyGeneratorInterface,
RedisAnalyticsStore,
RedisStatisticsStore,
StatisticsStoreInterface,
@@ -22,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')
@@ -84,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)
@@ -91,13 +94,13 @@ export class ContainerConfigLoader {
// Services
container.bind<HttpServiceInterface>(TYPES.HTTPService).to(HttpService)
const periodKeyGenerator = new PeriodKeyGenerator()
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
container
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
.toConstantValue(new RedisAnalyticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
.toConstantValue(new RedisStatisticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache).to(RedisCrossServiceTokenCache)
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())

View File

@@ -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'),
@@ -26,6 +27,7 @@ const TYPES = {
StatisticsStore: Symbol.for('StatisticsStore'),
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
Timer: Symbol.for('Timer'),
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
}
export default TYPES

View File

@@ -1,4 +1,5 @@
import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/common'
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Request, Response } from 'express'
@@ -75,9 +76,20 @@ export class AuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
await this.analyticsStore.markActivity([AnalyticsActivity.GeneralActivity], decodedToken.analyticsId as number, [
Period.Today,
])
response.locals.freeUser =
decodedToken.roles.length === 1 &&
decodedToken.roles.find((role) => role.name === RoleName.CoreUser) !== undefined
await this.analyticsStore.markActivity(
[
AnalyticsActivity.GeneralActivity,
response.locals.freeUser
? AnalyticsActivity.GeneralActivityFreeUsers
: AnalyticsActivity.GeneralActivityPaidUsers,
],
decodedToken.analyticsId as number,
[Period.Today],
)
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,130 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [1.25.10](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.9...@standardnotes/auth-server@1.25.10) (2022-09-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.8...@standardnotes/auth-server@1.25.9) (2022-09-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.7...@standardnotes/auth-server@1.25.8) (2022-09-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.6...@standardnotes/auth-server@1.25.7) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.5...@standardnotes/auth-server@1.25.6) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.4...@standardnotes/auth-server@1.25.5) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.3...@standardnotes/auth-server@1.25.4) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.25.4",
"version": "1.31.1",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -34,7 +34,7 @@
"@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:*",

View File

@@ -130,7 +130,14 @@ 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'
@@ -149,6 +156,7 @@ import {
TokenEncoder,
TokenEncoderInterface,
ValetTokenData,
WebSocketConnectionTokenData,
} from '@standardnotes/security'
import { FileUploadedEventHandler } from '../Domain/Handler/FileUploadedEventHandler'
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
@@ -200,6 +208,11 @@ import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteM
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 +275,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 +375,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'))
@@ -439,6 +460,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 +536,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 +553,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 +592,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

View File

@@ -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'),
@@ -124,6 +128,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 +171,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 +196,7 @@ const TYPES = {
UserSubscriptionService: Symbol.for('UserSubscriptionService'),
AnalyticsStore: Symbol.for('AnalyticsStore'),
StatisticsStore: Symbol.for('StatisticsStore'),
UuidValidator: Symbol.for('UuidValidator'),
}
export default TYPES

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
export type CreateCrossServiceTokenResponse = {
token: string
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
export type CreateWebSocketConnectionDTO = {
userUuid: string
}

View File

@@ -0,0 +1,3 @@
export type CreateWebSocketConnectionResponse = {
token: string
}

View File

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

View File

@@ -10,6 +10,7 @@ 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'
describe('InviteToSharedSubscription', () => {
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
@@ -40,6 +41,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 +183,26 @@ describe('InviteToSharedSubscription', () => {
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not create an invitation if it already exists', async () => {
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest
.fn()
.mockReturnValue({} 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()
})
})

View File

@@ -53,6 +53,16 @@ export class InviteToSharedSubscription implements UseCaseInterface {
}
}
const existingInvitation = await this.sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail(
dto.inviteeIdentifier,
dto.inviterEmail,
)
if (existingInvitation !== null) {
return {
success: false,
}
}
const sharedSubscriptionInvition = new SharedSubscriptionInvitation()
sharedSubscriptionInvition.inviterIdentifier = dto.inviterEmail
sharedSubscriptionInvition.inviterIdentifierType = InviterIdentifierType.Email

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.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
* **common:** add either and only types ([c3ebb32](https://github.com/standardnotes/server/commit/c3ebb321cfacd20769ebfd99413e283859b6e260))
# [1.31.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.30.0...@standardnotes/common@1.31.0) (2022-09-05)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/common",
"version": "1.31.0",
"version": "1.33.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -0,0 +1,3 @@
import { Only } from './Only'
export type Either<T, U> = Only<T, U> | Only<U, T>

View File

@@ -0,0 +1,5 @@
export type Only<T, U> = {
[P in keyof T]: T[P]
} & {
[P in keyof U]?: never
}

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
export interface ValidatorInterface<T> {
validate(data: T): boolean
}

View File

@@ -18,3 +18,7 @@ export * from './Protocol/ProtocolVersion'
export * from './Role/PaidRoles'
export * from './Role/RoleName'
export * from './Subscription/SubscriptionName'
export * from './Type/Either'
export * from './Type/Only'
export * from './Validator/UuidValidator'
export * from './Validator/ValidatorInterface'

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.8.8](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.7...@standardnotes/domain-events-infra@1.8.8) (2022-09-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.8.7](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.6...@standardnotes/domain-events-infra@1.8.7) (2022-09-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.8.6](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.5...@standardnotes/domain-events-infra@1.8.6) (2022-09-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.8.5](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.4...@standardnotes/domain-events-infra@1.8.5) (2022-09-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.8.5",
"version": "1.8.12",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [2.60.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.1...@standardnotes/domain-events@2.60.2) (2022-09-08)
### Bug Fixes
* **api-gateway:** retention data structure to include both period keys ([50ddb91](https://github.com/standardnotes/server/commit/50ddb918ccc52bee4caad82504cb899bc5936150))
## [2.60.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.0...@standardnotes/domain-events@2.60.1) (2022-09-08)
### Bug Fixes
* **api-gateway:** retention data structure ([47be084](https://github.com/standardnotes/server/commit/47be0841fc6d5fa00892e775bb3a40f404a6382b))
# [2.60.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.59.3...@standardnotes/domain-events@2.60.0) (2022-09-08)
### Features
* **api-gateway:** add registration-to-activity retention analytics to report ([f139bb0](https://github.com/standardnotes/server/commit/f139bb003669bb41f98ad4bb59a036c489f43606))
## [2.59.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.59.2...@standardnotes/domain-events@2.59.3) (2022-09-08)
### Reverts

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.59.3",
"version": "2.60.6",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -28,4 +28,16 @@ export interface DailyAnalyticsReportGeneratedEventPayload {
totalCount: number
}>
outOfSyncIncidents: number
retentionStatistics: Array<{
firstActivity: string
secondActivity: string
retention: {
periodKeys: Array<string>
values: Array<{
firstPeriodKey: string
secondPeriodKey: string
value: number
}>
}
}>
}

View File

@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.17](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.16...@standardnotes/event-store@1.3.17) (2022-09-21)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.16](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.15...@standardnotes/event-store@1.3.16) (2022-09-19)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.15](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.14...@standardnotes/event-store@1.3.15) (2022-09-16)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.14](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.13...@standardnotes/event-store@1.3.14) (2022-09-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.13](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.12...@standardnotes/event-store@1.3.13) (2022-09-09)
### Bug Fixes
* **event-store:** add missing event subscriptions ([432d071](https://github.com/standardnotes/server/commit/432d071ec88a49f90513be6c55a06005a471b174))
## [1.3.12](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.11...@standardnotes/event-store@1.3.12) (2022-09-08)
### Bug Fixes
* **event-store:** add listening to refund processed event ([73e1ea7](https://github.com/standardnotes/server/commit/73e1ea7f93b7d7956dd4a82298098e81ff9c85b1))
## [1.3.11](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.10...@standardnotes/event-store@1.3.11) (2022-09-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.10](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.9...@standardnotes/event-store@1.3.10) (2022-09-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.9](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.8...@standardnotes/event-store@1.3.9) (2022-09-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.8](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.7...@standardnotes/event-store@1.3.8) (2022-09-08)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.3.8",
"version": "1.3.17",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -80,6 +80,12 @@ export class ContainerConfigLoader {
['PAYMENT_SUCCESS', container.get(TYPES.EventHandler)],
['ACCOUNT_CLAIM_REQUESTED', container.get(TYPES.EventHandler)],
['SUBSCRIPTION_REVERT_REQUESTED', container.get(TYPES.EventHandler)],
['REFUND_PROCESSED', container.get(TYPES.EventHandler)],
['ACCOUNT_RESET_REQUESTED', container.get(TYPES.EventHandler)],
['DISCOUNT_APPLIED', container.get(TYPES.EventHandler)],
['SUBSCRIPTION_RATE_ADJUSTED', container.get(TYPES.EventHandler)],
['REFUND_REQUESTED', container.get(TYPES.EventHandler)],
['INVOICE_GENERATED', container.get(TYPES.EventHandler)],
])
container

View File

@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.2...@standardnotes/files-server@1.6.3) (2022-09-21)
**Note:** Version bump only for package @standardnotes/files-server
## [1.6.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.1...@standardnotes/files-server@1.6.2) (2022-09-19)
### Bug Fixes
* add upper bound for FS file chunk upload ([dfa7e06](https://github.com/standardnotes/files/commit/dfa7e06f8780bec21893ec77ab4a0945a6681545))
## [1.6.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.0...@standardnotes/files-server@1.6.1) (2022-09-19)
### Bug Fixes
* **files:** uuid validator binding ([a628bdc](https://github.com/standardnotes/files/commit/a628bdc44e97935b8a79460b74c30c0d29ef83bf))
# [1.6.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.52...@standardnotes/files-server@1.6.0) (2022-09-19)
### Features
* **files:** add validating remote identifiers ([db15457](https://github.com/standardnotes/files/commit/db15457ce4eb533ec822cf93c3ed83eafe9e64d5))
## [1.5.52](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.51...@standardnotes/files-server@1.5.52) (2022-09-16)
### Bug Fixes
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/files/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
## [1.5.51](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.50...@standardnotes/files-server@1.5.51) (2022-09-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.5.50](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.49...@standardnotes/files-server@1.5.50) (2022-09-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.5.49](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.48...@standardnotes/files-server@1.5.49) (2022-09-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.5.48](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.47...@standardnotes/files-server@1.5.48) (2022-09-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.5.47](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.46...@standardnotes/files-server@1.5.47) (2022-09-08)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.5.47",
"version": "1.6.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -44,6 +44,7 @@ import {
import { MarkFilesToBeRemoved } from '../Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
import { Uuid, UuidValidator, ValidatorInterface } from '@standardnotes/common'
export class ContainerConfigLoader {
async load(): Promise<Container> {
@@ -107,6 +108,7 @@ export class ContainerConfigLoader {
.toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger)))
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(FSFileRemover)
}
container.bind<ValidatorInterface<Uuid>>(TYPES.UuidValidator).toConstantValue(new UuidValidator())
if (env.get('SNS_AWS_REGION', true)) {
container.bind<AWS.SNS>(TYPES.SNS).toConstantValue(

View File

@@ -23,6 +23,7 @@ const TYPES = {
FileUploader: Symbol.for('FileUploader'),
FileDownloader: Symbol.for('FileDownloader'),
FileRemover: Symbol.for('FileRemover'),
UuidValidator: Symbol.for('UuidValidator'),
// repositories
UploadRepository: Symbol.for('UploadRepository'),

View File

@@ -11,6 +11,8 @@ import { FilesController } from './FilesController'
import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
import { results } from 'inversify-express-utils'
import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
import { ValetTokenOperation } from '@standardnotes/security'
import { BadRequestErrorMessageResult } from 'inversify-express-utils/lib/results'
describe('FilesController', () => {
let uploadFileChunk: UploadFileChunk
@@ -75,6 +77,8 @@ describe('FilesController', () => {
})
it('should return a writable stream upon file download', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['range'] = 'bytes=0-'
const result = (await createController().download(request, response)) as () => Writable
@@ -89,7 +93,19 @@ describe('FilesController', () => {
expect(result()).toBeInstanceOf(Writable)
})
it('should not allow download on invalid operation in the valet token', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
request.headers['range'] = 'bytes=0-'
const result = await createController().download(request, response)
expect(result).toBeInstanceOf(BadRequestErrorMessageResult)
})
it('should return proper byte range on consecutive calls', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['range'] = 'bytes=0-'
;(await createController().download(request, response)) as () => Writable
@@ -112,6 +128,8 @@ describe('FilesController', () => {
})
it('should return a writable stream with custom chunk size', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['x-chunk-size'] = '50000'
request.headers['range'] = 'bytes=0-'
@@ -128,6 +146,8 @@ describe('FilesController', () => {
})
it('should default to maximum chunk size if custom chunk size is too large', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['x-chunk-size'] = '200000'
request.headers['range'] = 'bytes=0-'
@@ -144,12 +164,16 @@ describe('FilesController', () => {
})
it('should not return a writable stream if bytes range is not provided', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const httpResponse = await createController().download(request, response)
expect(httpResponse).toBeInstanceOf(results.BadRequestErrorMessageResult)
})
it('should not return a writable stream if getting file metadata fails', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['range'] = 'bytes=0-'
getFileMetadata.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
@@ -160,6 +184,8 @@ describe('FilesController', () => {
})
it('should not return a writable stream if creating download stream fails', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
request.headers['range'] = 'bytes=0-'
streamDownloadFile.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
@@ -170,6 +196,8 @@ describe('FilesController', () => {
})
it('should create an upload session', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
await createController().startUpload(request, response)
expect(createUploadSession.execute).toHaveBeenCalledWith({
@@ -178,7 +206,17 @@ describe('FilesController', () => {
})
})
it('should not create an upload session on invalid operation in the valet token', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const result = await createController().startUpload(request, response)
expect(result).toBeInstanceOf(BadRequestErrorMessageResult)
})
it('should return bad request if upload session could not be created', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
createUploadSession.execute = jest.fn().mockReturnValue({ success: false })
const httpResponse = await createController().startUpload(request, response)
@@ -188,6 +226,8 @@ describe('FilesController', () => {
})
it('should finish an upload session', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
await createController().finishUpload(request, response)
expect(finishUploadSession.execute).toHaveBeenCalledWith({
@@ -196,7 +236,17 @@ describe('FilesController', () => {
})
})
it('should not finish an upload session on invalid operation in the valet token', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const result = await createController().finishUpload(request, response)
expect(result).toBeInstanceOf(BadRequestErrorMessageResult)
})
it('should return bad request if upload session could not be finished', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
finishUploadSession.execute = jest.fn().mockReturnValue({ success: false })
const httpResponse = await createController().finishUpload(request, response)
@@ -206,6 +256,8 @@ describe('FilesController', () => {
})
it('should remove a file', async () => {
response.locals.permittedOperation = ValetTokenOperation.Delete
await createController().remove(request, response)
expect(removeFile.execute).toHaveBeenCalledWith({
@@ -215,6 +267,8 @@ describe('FilesController', () => {
})
it('should return bad request if file removal could not be completed', async () => {
response.locals.permittedOperation = ValetTokenOperation.Delete
removeFile.execute = jest.fn().mockReturnValue({ success: false })
const httpResponse = await createController().remove(request, response)
@@ -223,7 +277,18 @@ describe('FilesController', () => {
expect(result.statusCode).toEqual(400)
})
it('should return bad request if file removal is not permitted on valet token', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const httpResponse = await createController().remove(request, response)
const result = await httpResponse.executeAsync()
expect(result.statusCode).toEqual(400)
})
it('should upload a chunk to an upload session', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
request.headers['x-chunk-id'] = '2'
request.body = Buffer.from([123])
@@ -233,11 +298,14 @@ describe('FilesController', () => {
chunkId: 2,
data: Buffer.from([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
})
})
it('should return bad request if chunk could not be uploaded', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
request.headers['x-chunk-id'] = '2'
request.body = Buffer.from([123])
uploadFileChunk.execute = jest.fn().mockReturnValue({ success: false })
@@ -248,7 +316,18 @@ describe('FilesController', () => {
expect(result.statusCode).toEqual(400)
})
it('should return bad request if valet token is not permitted', async () => {
response.locals.permittedOperation = ValetTokenOperation.Read
const httpResponse = await createController().uploadChunk(request, response)
const result = await httpResponse.executeAsync()
expect(result.statusCode).toEqual(400)
})
it('should return bad request if chunk id is missing', async () => {
response.locals.permittedOperation = ValetTokenOperation.Write
request.body = Buffer.from([123])
const httpResponse = await createController().uploadChunk(request, response)

View File

@@ -9,6 +9,7 @@ import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/Creat
import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
import { ValetTokenOperation } from '@standardnotes/security'
@controller('/v1/files', TYPES.ValetTokenAuthMiddleware)
export class FilesController extends BaseHttpController {
@@ -29,6 +30,10 @@ export class FilesController extends BaseHttpController {
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
if (response.locals.permittedOperation !== ValetTokenOperation.Write) {
return this.badRequest('Not permitted for this operation')
}
const result = await this.createUploadSession.execute({
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
@@ -46,6 +51,10 @@ export class FilesController extends BaseHttpController {
request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
if (response.locals.permittedOperation !== ValetTokenOperation.Write) {
return this.badRequest('Not permitted for this operation')
}
const chunkId = +(request.headers['x-chunk-id'] as string)
if (!chunkId) {
return this.badRequest('Missing x-chunk-id header in request.')
@@ -54,6 +63,7 @@ export class FilesController extends BaseHttpController {
const result = await this.uploadFileChunk.execute({
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
resourceUnencryptedFileSize: response.locals.permittedResources[0].unencryptedFileSize,
chunkId,
data: request.body,
})
@@ -70,6 +80,10 @@ export class FilesController extends BaseHttpController {
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
if (response.locals.permittedOperation !== ValetTokenOperation.Write) {
return this.badRequest('Not permitted for this operation')
}
const result = await this.finishUploadSession.execute({
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
@@ -89,6 +103,10 @@ export class FilesController extends BaseHttpController {
_request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
if (response.locals.permittedOperation !== ValetTokenOperation.Delete) {
return this.badRequest('Not permitted for this operation')
}
const result = await this.removeFile.execute({
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
@@ -107,6 +125,10 @@ export class FilesController extends BaseHttpController {
request: Request,
response: Response,
): Promise<results.BadRequestErrorMessageResult | (() => Writable)> {
if (response.locals.permittedOperation !== ValetTokenOperation.Read) {
return this.badRequest('Not permitted for this operation')
}
const range = request.headers['range']
if (!range) {
return this.badRequest('File download requires range header to be set.')

View File

@@ -4,9 +4,11 @@ import { ValetTokenAuthMiddleware } from './ValetTokenAuthMiddleware'
import { NextFunction, Request, Response } from 'express'
import { Logger } from 'winston'
import { TokenDecoderInterface, ValetTokenData } from '@standardnotes/security'
import { Uuid, ValidatorInterface } from '@standardnotes/common'
describe('ValetTokenAuthMiddleware', () => {
let tokenDecoder: TokenDecoderInterface<ValetTokenData>
let uuidValidator: ValidatorInterface<Uuid>
let request: Request
let response: Response
let next: NextFunction
@@ -15,7 +17,7 @@ describe('ValetTokenAuthMiddleware', () => {
debug: jest.fn(),
} as unknown as jest.Mocked<Logger>
const createMiddleware = () => new ValetTokenAuthMiddleware(tokenDecoder, logger)
const createMiddleware = () => new ValetTokenAuthMiddleware(tokenDecoder, uuidValidator, logger)
beforeEach(() => {
tokenDecoder = {} as jest.Mocked<TokenDecoderInterface<ValetTokenData>>
@@ -32,6 +34,9 @@ describe('ValetTokenAuthMiddleware', () => {
uploadBytesUsed: 80,
})
uuidValidator = {} as jest.Mocked<ValidatorInterface<Uuid>>
uuidValidator.validate = jest.fn().mockReturnValue(true)
request = {
headers: {},
query: {},
@@ -174,6 +179,30 @@ describe('ValetTokenAuthMiddleware', () => {
expect(next).not.toHaveBeenCalled()
})
it('should not authorize if valet token has an invalid remote resource identifier', async () => {
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
userUuid: '1-2-3',
permittedResources: [
{
remoteIdentifier: '1-2-3/2-3-4',
unencryptedFileSize: 30,
},
],
permittedOperation: 'write',
uploadBytesLimit: -1,
uploadBytesUsed: 80,
})
request.headers['x-valet-token'] = 'valet-token'
uuidValidator.validate = jest.fn().mockReturnValue(false)
await createMiddleware().handler(request, response, next)
expect(response.status).toHaveBeenCalledWith(401)
expect(next).not.toHaveBeenCalled()
})
it('should not authorize if auth valet token is malformed', async () => {
request.headers['x-valet-token'] = 'valet-token'

View File

@@ -1,3 +1,4 @@
import { Uuid, ValidatorInterface } from '@standardnotes/common'
import { TokenDecoderInterface, ValetTokenData } from '@standardnotes/security'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
@@ -9,6 +10,7 @@ import TYPES from '../Bootstrap/Types'
export class ValetTokenAuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.ValetTokenDecoder) private tokenDecoder: TokenDecoderInterface<ValetTokenData>,
@inject(TYPES.UuidValidator) private uuidValidator: ValidatorInterface<Uuid>,
@inject(TYPES.Logger) private logger: Logger,
) {
super()
@@ -45,6 +47,21 @@ export class ValetTokenAuthMiddleware extends BaseMiddleware {
return
}
for (const resource of valetTokenData.permittedResources) {
if (!this.uuidValidator.validate(resource.remoteIdentifier)) {
this.logger.debug('Invalid remote resource identifier in token.')
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid valet token.',
},
})
return
}
}
if (this.userHasNoSpaceToUpload(valetTokenData)) {
response.status(403).send({
error: {

View File

@@ -4,6 +4,12 @@ import { UploadId } from '../Upload/UploadId'
export interface FileUploaderInterface {
createUploadSession(filePath: string): Promise<UploadId>
uploadFileChunk(dto: { uploadId: string; data: Uint8Array; filePath: string; chunkId: ChunkId }): Promise<string>
uploadFileChunk(dto: {
uploadId: string
data: Uint8Array
filePath: string
chunkId: ChunkId
unencryptedFileSize: number
}): Promise<string>
finishUploadSession(uploadId: string, filePath: string, uploadChunkResults: Array<UploadChunkResult>): Promise<void>
}

View File

@@ -33,6 +33,7 @@ describe('UploadFileChunk', () => {
chunkId: 2,
data: new Uint8Array([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
})
@@ -50,6 +51,7 @@ describe('UploadFileChunk', () => {
chunkId: 2,
data: new Uint8Array([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
}),
).toEqual({
@@ -66,6 +68,7 @@ describe('UploadFileChunk', () => {
chunkId: 2,
data: new Uint8Array([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
})
@@ -74,6 +77,7 @@ describe('UploadFileChunk', () => {
data: new Uint8Array([123]),
filePath: '1-2-3/2-3-4',
uploadId: '123',
unencryptedFileSize: 123,
})
expect(uploadRepository.storeUploadChunkResult).toHaveBeenCalledWith('123', {
tag: 'ETag123',

View File

@@ -39,6 +39,7 @@ export class UploadFileChunk implements UseCaseInterface {
data: dto.data,
chunkId: dto.chunkId,
filePath,
unencryptedFileSize: dto.resourceUnencryptedFileSize,
})
await this.uploadRepository.storeUploadChunkResult(uploadId, {

View File

@@ -5,4 +5,5 @@ export type UploadFileChunkDTO = {
chunkId: ChunkId
userUuid: string
resourceRemoteIdentifier: string
resourceUnencryptedFileSize: number
}

View File

@@ -1,11 +1,12 @@
import { promises } from 'fs'
import { dirname } from 'path'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { FileUploaderInterface } from '../../Domain/Services/FileUploaderInterface'
import { UploadChunkResult } from '../../Domain/Upload/UploadChunkResult'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { ChunkId } from '../../Domain/Upload/ChunkId'
@injectable()
export class FSFileUploader implements FileUploaderInterface {
@@ -22,7 +23,8 @@ export class FSFileUploader implements FileUploaderInterface {
uploadId: string
data: Uint8Array
filePath: string
chunkId: number
chunkId: ChunkId
unencryptedFileSize: number
}): Promise<string> {
if (!this.inMemoryChunks.has(dto.uploadId)) {
this.inMemoryChunks.set(dto.uploadId, new Map<number, Uint8Array>())
@@ -30,6 +32,13 @@ export class FSFileUploader implements FileUploaderInterface {
const fileChunks = this.inMemoryChunks.get(dto.uploadId) as Map<number, Uint8Array>
const alreadyStoredBytes = this.accumulatedEncryptedFileSize(fileChunks)
if (alreadyStoredBytes >= dto.unencryptedFileSize) {
throw new Error(
`Could not finish chunk upload. Accumulated encrypted file size (${alreadyStoredBytes}B) already exceeds the unecrypted file size: ${dto.unencryptedFileSize}`,
)
}
this.logger.debug(`FS storing file chunk ${dto.chunkId} in memory for ${dto.uploadId}`)
fileChunks.set(dto.chunkId, dto.data)
@@ -64,4 +73,14 @@ export class FSFileUploader implements FileUploaderInterface {
return fullPath
}
private accumulatedEncryptedFileSize(fileChunks: Map<number, Uint8Array>): number {
let accumulatedSize = 0
for (const value of fileChunks.values()) {
accumulatedSize += value.byteLength
}
return accumulatedSize
}
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.2](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.4.1...@standardnotes/predicates@1.4.2) (2022-09-19)
**Note:** Version bump only for package @standardnotes/predicates
## [1.4.1](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.4.0...@standardnotes/predicates@1.4.1) (2022-09-09)
**Note:** Version bump only for package @standardnotes/predicates
# [1.4.0](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.3.0...@standardnotes/predicates@1.4.0) (2022-09-05)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/predicates",
"version": "1.4.0",
"version": "1.4.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.31](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.30...@standardnotes/scheduler-server@1.10.31) (2022-09-21)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.30](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.29...@standardnotes/scheduler-server@1.10.30) (2022-09-19)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.29](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.28...@standardnotes/scheduler-server@1.10.29) (2022-09-16)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.28](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.27...@standardnotes/scheduler-server@1.10.28) (2022-09-09)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.27](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.26...@standardnotes/scheduler-server@1.10.27) (2022-09-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.26](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.25...@standardnotes/scheduler-server@1.10.26) (2022-09-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.25](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.24...@standardnotes/scheduler-server@1.10.25) (2022-09-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.24](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.23...@standardnotes/scheduler-server@1.10.24) (2022-09-08)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.10.24",
"version": "1.10.31",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.4.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.3...@standardnotes/security@1.4.0) (2022-09-21)
### Features
* **auth:** add creating web socket connection tokens ([8033177](https://github.com/standardnotes/server/commit/8033177f48dc961194f24fb7daa1073b8b697b74))
## [1.3.3](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.2...@standardnotes/security@1.3.3) (2022-09-19)
**Note:** Version bump only for package @standardnotes/security
## [1.3.2](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.1...@standardnotes/security@1.3.2) (2022-09-16)
### Bug Fixes
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/server/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
## [1.3.1](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.0...@standardnotes/security@1.3.1) (2022-09-09)
**Note:** Version bump only for package @standardnotes/security
# [1.3.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.2.6...@standardnotes/security@1.3.0) (2022-09-05)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/security",
"version": "1.3.0",
"version": "1.4.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

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