Compare commits

..

59 Commits

Author SHA1 Message Date
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
standardci
23f592ca24 chore(release): publish new version
- @standardnotes/api-gateway@1.16.8
 - @standardnotes/auth-server@1.25.4
 - @standardnotes/domain-events-infra@1.8.5
 - @standardnotes/domain-events@2.59.3
 - @standardnotes/event-store@1.3.8
 - @standardnotes/files-server@1.5.47
 - @standardnotes/scheduler-server@1.10.24
 - @standardnotes/syncing-server@1.6.67
2022-09-08 08:33:27 +00:00
Karol Sójko
fe4821d4f7 Revert "fix(domain-events): add boolean for sending emails on refund processed"
This reverts commit d7e6758089.
2022-09-08 10:32:00 +02:00
standardci
c338d4fec5 chore(release): publish new version
- @standardnotes/api-gateway@1.16.7
 - @standardnotes/auth-server@1.25.3
 - @standardnotes/domain-events-infra@1.8.4
 - @standardnotes/domain-events@2.59.2
 - @standardnotes/event-store@1.3.7
 - @standardnotes/files-server@1.5.46
 - @standardnotes/scheduler-server@1.10.23
 - @standardnotes/syncing-server@1.6.66
2022-09-08 08:05:47 +00:00
Karol Sójko
d7e6758089 fix(domain-events): add boolean for sending emails on refund processed 2022-09-08 10:03:41 +02:00
101 changed files with 1810 additions and 341 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.7.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.7.2-e68e7d4e63-bdfc414e6d.zip/node_modules/@standardnotes/api/",\
"packageDependencies": [\
["@standardnotes/api", "npm:1.1.19"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/api", "npm:1.7.2"],\
["@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.2"],\
["@standardnotes/models", "npm:1.18.2"],\
["@standardnotes/responses", "npm:1.10.1"],\
["@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.7.2"],\
["@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.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.15.2-ef86a8281d-6e8336f1e7.zip/node_modules/@standardnotes/encryption/",\
"packageDependencies": [\
["@standardnotes/encryption", "npm:1.12.0"],\
["@standardnotes/encryption", "npm:1.15.2"],\
["@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.2"],\
["@standardnotes/responses", "npm:1.10.1"],\
["@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.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.18.2-56f35bb72d-88180a93e5.zip/node_modules/@standardnotes/models/",\
"packageDependencies": [\
["@standardnotes/models", "npm:1.14.0"],\
["@standardnotes/models", "npm:1.18.2"],\
["@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.1"],\
["@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.1", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.1-9f82fff6c1-b84fb3f71c.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
["@standardnotes/responses", "npm:1.10.1"],\
["@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,86 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [1.16.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.6...@standardnotes/api-gateway@1.16.7) (2022-09-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.16.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.5...@standardnotes/api-gateway@1.16.6) (2022-09-07)
**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.6",
"version": "1.19.6",
"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,
@@ -91,13 +92,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

@@ -26,6 +26,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

@@ -3,6 +3,114 @@
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/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
## [1.25.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.2...@standardnotes/auth-server@1.25.3) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.1...@standardnotes/auth-server@1.25.2) (2022-09-07)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -17,10 +17,10 @@ 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 * 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.2",
"version": "1.29.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.7.2",
"@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'
@@ -200,6 +207,7 @@ 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'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -262,6 +270,7 @@ export class ContainerConfigLoader {
// Controller
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
// Repositories
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)
@@ -557,6 +566,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,7 @@ const TYPES = {
SQS: Symbol.for('SQS'),
// Controller
AuthController: Symbol.for('AuthController'),
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
// Repositories
UserRepository: Symbol.for('UserRepository'),
SessionRepository: Symbol.for('SessionRepository'),
@@ -188,6 +189,7 @@ const TYPES = {
UserSubscriptionService: Symbol.for('UserSubscriptionService'),
AnalyticsStore: Symbol.for('AnalyticsStore'),
StatisticsStore: Symbol.for('StatisticsStore'),
UuidValidator: Symbol.for('UuidValidator'),
}
export default TYPES

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

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

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

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

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

View File

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

View File

@@ -3,6 +3,48 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
* Revert "fix(domain-events): add boolean for sending emails on refund processed" ([fe4821d](https://github.com/standardnotes/server/commit/fe4821d4f7df38297cb92314b9cd3fde3d2c58b6))
## [2.59.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.59.1...@standardnotes/domain-events@2.59.2) (2022-09-08)
### Bug Fixes
* **domain-events:** add boolean for sending emails on refund processed ([d7e6758](https://github.com/standardnotes/server/commit/d7e6758089c5e9485fda345949ec8d58732afa90))
## [2.59.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.59.0...@standardnotes/domain-events@2.59.1) (2022-09-06)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.59.1",
"version": "2.60.5",
"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,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [1.3.7](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.6...@standardnotes/event-store@1.3.7) (2022-09-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.6](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.5...@standardnotes/event-store@1.3.6) (2022-09-06)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.3.6",
"version": "1.3.16",
"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,54 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [1.5.46](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.45...@standardnotes/files-server@1.5.46) (2022-09-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.5.45](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.44...@standardnotes/files-server@1.5.45) (2022-09-06)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.5.45",
"version": "1.6.2",
"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,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [1.10.23](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.22...@standardnotes/scheduler-server@1.10.23) (2022-09-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.22](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.21...@standardnotes/scheduler-server@1.10.22) (2022-09-06)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

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

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.3.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -1,10 +1,12 @@
import { Uuid } from '@standardnotes/common'
import { ValetTokenOperation } from './ValetTokenOperation'
export type ValetTokenData = {
userUuid: Uuid
sharedSubscriptionUuid: Uuid | undefined
regularSubscriptionUuid: Uuid
permittedOperation: 'read' | 'write' | 'delete'
permittedOperation: ValetTokenOperation
permittedResources: Array<{
remoteIdentifier: string
unencryptedFileSize?: number

View File

@@ -0,0 +1,5 @@
export enum ValetTokenOperation {
Read = 'read',
Write = 'write',
Delete = 'delete',
}

View File

@@ -11,3 +11,4 @@ export * from './Token/OfflineFeaturesTokenData'
export * from './Token/OfflineUserTokenData'
export * from './Token/SessionTokenData'
export * from './Token/ValetTokenData'
export * from './Token/ValetTokenOperation'

View File

@@ -3,6 +3,74 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.6...@standardnotes/syncing-server@1.8.7) (2022-09-20)
### Bug Fixes
* **syncing-server:** content size calculation and add syncing upper bound for limit paramter ([c2e9f3e](https://github.com/standardnotes/syncing-server-js/commit/c2e9f3e72b87c445a6f4d61cbf59621954187d21))
## [1.8.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.5...@standardnotes/syncing-server@1.8.6) (2022-09-19)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.4...@standardnotes/syncing-server@1.8.5) (2022-09-16)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.3...@standardnotes/syncing-server@1.8.4) (2022-09-16)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.2...@standardnotes/syncing-server@1.8.3) (2022-09-15)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.1...@standardnotes/syncing-server@1.8.2) (2022-09-15)
### Bug Fixes
* **syncing-server:** files count stats ([ecdfe9e](https://github.com/standardnotes/syncing-server-js/commit/ecdfe9ecc0bce882c1e3c6984f67b76862d76836))
## [1.8.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.0...@standardnotes/syncing-server@1.8.1) (2022-09-09)
**Note:** Version bump only for package @standardnotes/syncing-server
# [1.8.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.7.1...@standardnotes/syncing-server@1.8.0) (2022-09-09)
### Features
* **syncing-server:** add tracking files count in stats ([52cc646](https://github.com/standardnotes/syncing-server-js/commit/52cc6462a66dae3bd6c05f551d4ba661c8a9b8c8))
## [1.7.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.7.0...@standardnotes/syncing-server@1.7.1) (2022-09-09)
**Note:** Version bump only for package @standardnotes/syncing-server
# [1.7.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.70...@standardnotes/syncing-server@1.7.0) (2022-09-09)
### Features
* **syncing-server:** add statistics for notes count for free and paid users ([c9ec7b4](https://github.com/standardnotes/syncing-server-js/commit/c9ec7b492aea1911e441ed8ad9a155f871be2ef7))
## [1.6.70](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.69...@standardnotes/syncing-server@1.6.70) (2022-09-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.69](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.68...@standardnotes/syncing-server@1.6.69) (2022-09-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.68](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.67...@standardnotes/syncing-server@1.6.68) (2022-09-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.67](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.66...@standardnotes/syncing-server@1.6.67) (2022-09-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.66](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.65...@standardnotes/syncing-server@1.6.66) (2022-09-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.65](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.64...@standardnotes/syncing-server@1.6.65) (2022-09-07)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.6.65",
"version": "1.8.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -35,7 +35,7 @@ describe('AuthMiddleware', () => {
next = jest.fn()
})
it('should authorize user from an auth JWT token if present', async () => {
it('should authorize a paid user from an auth JWT token if present', async () => {
const authToken = sign(
{
user: { uuid: '123' },
@@ -66,6 +66,34 @@ describe('AuthMiddleware', () => {
expect(response.locals.session).toEqual({ uuid: '234' })
expect(response.locals.readOnlyAccess).toBeFalsy()
expect(response.locals.analyticsId).toEqual(123)
expect(response.locals.freeUser).toEqual(false)
expect(next).toHaveBeenCalled()
})
it('should authorize a free user from an auth JWT token if present', async () => {
const authToken = sign(
{
user: { uuid: '123' },
session: { uuid: '234' },
roles: [
{
uuid: '1-2-3',
name: RoleName.CoreUser,
},
],
analyticsId: 123,
permissions: [],
},
jwtSecret,
{ algorithm: 'HS256' },
)
request.header = jest.fn().mockReturnValue(authToken)
await createMiddleware().handler(request, response, next)
expect(response.locals.freeUser).toEqual(true)
expect(next).toHaveBeenCalled()
})

View File

@@ -5,6 +5,7 @@ import { verify } from 'jsonwebtoken'
import { CrossServiceTokenData } from '@standardnotes/security'
import * as winston from 'winston'
import TYPES from '../Bootstrap/Types'
import { RoleName } from '@standardnotes/common'
@injectable()
export class AuthMiddleware extends BaseMiddleware {
@@ -27,6 +28,8 @@ export class AuthMiddleware extends BaseMiddleware {
response.locals.user = decodedToken.user
response.locals.roleNames = decodedToken.roles.map((role) => role.name)
response.locals.freeUser =
response.locals.roleNames.length === 1 && response.locals.roleNames[0] === RoleName.CoreUser
response.locals.session = decodedToken.session
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
response.locals.analyticsId = decodedToken.analyticsId

View File

@@ -75,6 +75,7 @@ describe('ItemsController', () => {
uuid: '123',
}
response.locals.analyticsId = 123
response.locals.freeUser = false
syncResponse = {} as jest.Mocked<SyncResponse20200115>
@@ -132,6 +133,8 @@ describe('ItemsController', () => {
},
],
userUuid: '123',
analyticsId: 123,
freeUser: false,
})
expect(result.statusCode).toEqual(200)
@@ -147,6 +150,8 @@ describe('ItemsController', () => {
expect(checkIntegrity.execute).toHaveBeenCalledWith({
integrityPayloads: [],
userUuid: '123',
analyticsId: 123,
freeUser: false,
})
expect(result.statusCode).toEqual(200)

View File

@@ -62,6 +62,8 @@ export class ItemsController extends BaseHttpController {
const result = await this.checkIntegrity.execute({
userUuid: response.locals.user.uuid,
integrityPayloads,
analyticsId: response.locals.analyticsId,
freeUser: response.locals.freeUser,
})
return this.json(result)

View File

@@ -5,12 +5,16 @@ import { ContentType } from '@standardnotes/common'
import { ItemFactory } from './ItemFactory'
import { ItemHash } from './ItemHash'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
import { Item } from './Item'
describe('ItemFactory', () => {
let timer: TimerInterface
let itemProjector: ProjectorInterface<Item, ItemProjection>
let timeHelper: Timer
const createFactory = () => new ItemFactory(timer)
const createFactory = () => new ItemFactory(timer, itemProjector)
beforeEach(() => {
timeHelper = new Timer()
@@ -26,6 +30,23 @@ describe('ItemFactory', () => {
timer.convertStringDateToDate = jest
.fn()
.mockImplementation((date: string) => timeHelper.convertStringDateToDate(date))
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
itemProjector.projectFull = jest.fn().mockReturnValue({
uuid: '1-2-3',
items_key_id: 'foobar',
duplicate_of: null,
enc_item_key: 'foobar',
content: 'foobar',
content_type: ContentType.Note,
auth_hash: 'foobar',
deleted: false,
created_at: '2022-09-01 10:00:00',
created_at_timestamp: 123123123123123,
updated_at: '2022-09-01 10:00:00',
updated_at_timestamp: 123123123123123,
updated_with_session: '2-4-5',
})
})
it('should create an item based on item hash', () => {
@@ -43,7 +64,7 @@ describe('ItemFactory', () => {
updatedAtTimestamp: 1616164633241568,
userUuid: 'a-b-c',
uuid: '1-2-3',
contentSize: 0,
contentSize: 341,
})
})
@@ -64,7 +85,7 @@ describe('ItemFactory', () => {
userUuid: 'a-b-c',
uuid: '1-2-3',
content: null,
contentSize: 0,
contentSize: 341,
})
})
@@ -86,7 +107,7 @@ describe('ItemFactory', () => {
userUuid: 'a-b-c',
uuid: '1-2-3',
content: 'foobar',
contentSize: 6,
contentSize: 341,
})
})
@@ -106,7 +127,7 @@ describe('ItemFactory', () => {
userUuid: 'a-b-c',
uuid: '1-2-3',
content: null,
contentSize: 0,
contentSize: 341,
})
})
@@ -128,7 +149,7 @@ describe('ItemFactory', () => {
expect(item).toEqual({
content: 'asdqwe1',
contentSize: 7,
contentSize: 341,
contentType: 'Note',
createdAt: expect.any(Date),
updatedWithSession: '1-2-3',
@@ -161,7 +182,7 @@ describe('ItemFactory', () => {
expect(item).toEqual({
content: 'asdqwe1',
contentSize: 7,
contentSize: 341,
contentType: 'Note',
createdAt: expect.any(Date),
updatedWithSession: '1-2-3',

View File

@@ -3,13 +3,18 @@ import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { ItemProjection } from '../../Projection/ItemProjection'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Item } from './Item'
import { ItemFactoryInterface } from './ItemFactoryInterface'
import { ItemHash } from './ItemHash'
@injectable()
export class ItemFactory implements ItemFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
constructor(
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
) {}
createStub(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: Uuid | null }): Item {
const item = this.create(dto)
@@ -36,7 +41,6 @@ export class ItemFactory implements ItemFactoryInterface {
newItem.contentSize = 0
if (dto.itemHash.content) {
newItem.content = dto.itemHash.content
newItem.contentSize = Buffer.byteLength(dto.itemHash.content)
}
newItem.userUuid = dto.userUuid
if (dto.itemHash.content_type) {
@@ -75,6 +79,8 @@ export class ItemFactory implements ItemFactoryInterface {
newItem.createdAt = this.timer.convertStringDateToDate(dto.itemHash.created_at)
}
newItem.contentSize = Buffer.byteLength(JSON.stringify(this.itemProjector.projectFull(newItem)))
return newItem
}
}

View File

@@ -16,6 +16,8 @@ import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInt
import { ItemFactoryInterface } from './ItemFactoryInterface'
import { ItemConflict } from './ItemConflict'
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
describe('ItemService', () => {
let itemRepository: ItemRepositoryInterface
@@ -37,6 +39,7 @@ describe('ItemService', () => {
let itemFactory: ItemFactoryInterface
let timeHelper: Timer
let itemTransferCalculator: ItemTransferCalculatorInterface
let itemProjector: ProjectorInterface<Item, ItemProjection>
const createService = () =>
new ItemService(
@@ -50,6 +53,7 @@ describe('ItemService', () => {
contentSizeTransferLimit,
itemTransferCalculator,
timer,
itemProjector,
logger,
)
@@ -156,6 +160,24 @@ describe('ItemService', () => {
itemFactory = {} as jest.Mocked<ItemFactoryInterface>
itemFactory.create = jest.fn().mockReturnValue(newItem)
itemFactory.createStub = jest.fn().mockReturnValue(newItem)
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
itemProjector.projectFull = jest.fn().mockReturnValue({
uuid: '1-2-3',
items_key_id: 'foobar',
duplicate_of: null,
enc_item_key: 'foobar',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed viverra tellus in hac habitasse. Tortor posuere ac ut consequat semper. Ut diam quam nulla porttitor. Sapien pellentesque habitant morbi tristique senectus et netus et malesuada. Dapibus ultrices in iaculis nunc. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Faucibus et molestie ac feugiat sed lectus vestibulum mattis. Eu consequat ac felis donec. Eget velit aliquet sagittis id. Nullam eget felis eget nunc. Turpis in eu mi bibendum neque egestas congue.',
content_type: ContentType.Note,
auth_hash: 'foobar',
deleted: false,
created_at: '2022-09-01 10:00:00',
created_at_timestamp: 123123123123123,
updated_at: '2022-09-01 10:00:00',
updated_at_timestamp: 123123123123123,
updated_with_session: '2-4-5',
})
})
it('should retrieve all items for a user from last sync with sync token version 1', async () => {
@@ -214,6 +236,34 @@ describe('ItemService', () => {
})
})
it('should retrieve all items for a user from last sync with upper bound items limit', async () => {
expect(
await createService().getItems({
userUuid: '1-2-3',
syncToken,
contentType: ContentType.Note,
limit: 1000,
}),
).toEqual({
items: [item1, item2],
})
expect(itemRepository.countAll).toHaveBeenCalledWith({
contentType: 'Note',
lastSyncTime: 1616164633241564,
syncTimeComparison: '>',
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
userUuid: '1-2-3',
limit: 300,
})
expect(itemRepository.findAll).toHaveBeenCalledWith({
uuids: ['1-2-3', '2-3-4'],
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
})
})
it('should retrieve no items for a user if there are none from last sync', async () => {
itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockReturnValue([])
@@ -589,7 +639,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -625,7 +675,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -660,7 +710,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: 123,
createdAt: expect.any(Date),
@@ -696,7 +746,7 @@ describe('ItemService', () => {
conflicts: [],
savedItems: [
{
contentSize: 0,
contentSize: 950,
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
userUuid: '1-2-3',
@@ -726,7 +776,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -759,7 +809,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -794,7 +844,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -865,7 +915,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),

View File

@@ -21,10 +21,13 @@ import { SaveItemsResult } from './SaveItemsResult'
import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
import { ConflictType } from '@standardnotes/responses'
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
@injectable()
export class ItemService implements ItemServiceInterface {
private readonly DEFAULT_ITEMS_LIMIT = 150
private readonly MAX_ITEMS_LIMIT = 300
private readonly SYNC_TOKEN_VERSION = 2
constructor(
@@ -38,6 +41,7 @@ export class ItemService implements ItemServiceInterface {
@inject(TYPES.CONTENT_SIZE_TRANSFER_LIMIT) private contentSizeTransferLimit: number,
@inject(TYPES.ItemTransferCalculator) private itemTransferCalculator: ItemTransferCalculatorInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
@inject(TYPES.Logger) private logger: Logger,
) {}
@@ -54,7 +58,7 @@ export class ItemService implements ItemServiceInterface {
deleted: lastSyncTime ? undefined : false,
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
limit,
limit: limit < this.MAX_ITEMS_LIMIT ? limit : this.MAX_ITEMS_LIMIT,
}
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
@@ -196,7 +200,6 @@ export class ItemService implements ItemServiceInterface {
dto.existingItem.contentSize = 0
if (dto.itemHash.content) {
dto.existingItem.content = dto.itemHash.content
dto.existingItem.contentSize = Buffer.byteLength(dto.itemHash.content)
}
if (dto.itemHash.content_type) {
dto.existingItem.contentType = dto.itemHash.content_type
@@ -219,14 +222,6 @@ export class ItemService implements ItemServiceInterface {
dto.existingItem.itemsKeyId = dto.itemHash.items_key_id
}
if (dto.itemHash.deleted === true) {
dto.existingItem.deleted = true
dto.existingItem.content = null
;(dto.existingItem.contentSize = 0), (dto.existingItem.encItemKey = null)
dto.existingItem.authHash = null
dto.existingItem.itemsKeyId = null
}
const updatedAt = this.timer.getTimestampInMicroseconds()
const secondsFromLastUpdate = this.timer.convertMicrosecondsToSeconds(
updatedAt - dto.existingItem.updatedAtTimestamp,
@@ -243,6 +238,17 @@ export class ItemService implements ItemServiceInterface {
dto.existingItem.updatedAtTimestamp = updatedAt
dto.existingItem.updatedAt = this.timer.convertMicrosecondsToDate(updatedAt)
dto.existingItem.contentSize = Buffer.byteLength(JSON.stringify(this.itemProjector.projectFull(dto.existingItem)))
if (dto.itemHash.deleted === true) {
dto.existingItem.deleted = true
dto.existingItem.content = null
dto.existingItem.contentSize = 0
dto.existingItem.encItemKey = null
dto.existingItem.authHash = null
dto.existingItem.itemsKeyId = null
}
const savedItem = await this.itemRepository.save(dto.existingItem)
if (secondsFromLastUpdate >= this.revisionFrequency) {

View File

@@ -1,6 +1,6 @@
import 'reflect-metadata'
import { StatisticsStoreInterface } from '@standardnotes/analytics'
import { AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
import { ItemRepositoryInterface } from '../../Item/ItemRepositoryInterface'
@@ -10,8 +10,9 @@ import { ContentType } from '@standardnotes/common'
describe('CheckIntegrity', () => {
let itemRepository: ItemRepositoryInterface
let statisticsStore: StatisticsStoreInterface
let analyticsStore: AnalyticsStoreInterface
const createUseCase = () => new CheckIntegrity(itemRepository, statisticsStore)
const createUseCase = () => new CheckIntegrity(itemRepository, statisticsStore, analyticsStore)
beforeEach(() => {
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
@@ -36,16 +37,28 @@ describe('CheckIntegrity', () => {
updated_at_timestamp: 4,
content_type: ContentType.ItemsKey,
},
{
uuid: '5-6-7',
updated_at_timestamp: 5,
content_type: ContentType.File,
},
])
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementOutOfSyncIncidents = jest.fn()
statisticsStore.incrementMeasure = jest.fn()
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(false)
analyticsStore.markActivity = jest.fn()
})
it('should return an empty result if there are no integrity mismatches', async () => {
expect(
await createUseCase().execute({
userUuid: '1-2-3',
analyticsId: 1,
freeUser: false,
integrityPayloads: [
{
uuid: '1-2-3',
@@ -59,6 +72,10 @@ describe('CheckIntegrity', () => {
uuid: '3-4-5',
updated_at_timestamp: 3,
},
{
uuid: '5-6-7',
updated_at_timestamp: 5,
},
],
}),
).toEqual({
@@ -70,6 +87,8 @@ describe('CheckIntegrity', () => {
expect(
await createUseCase().execute({
userUuid: '1-2-3',
analyticsId: 1,
freeUser: false,
integrityPayloads: [
{
uuid: '1-2-3',
@@ -83,6 +102,10 @@ describe('CheckIntegrity', () => {
uuid: '3-4-5',
updated_at_timestamp: 3,
},
{
uuid: '5-6-7',
updated_at_timestamp: 5,
},
],
}),
).toEqual({
@@ -101,6 +124,8 @@ describe('CheckIntegrity', () => {
expect(
await createUseCase().execute({
userUuid: '1-2-3',
analyticsId: 1,
freeUser: false,
integrityPayloads: [
{
uuid: '1-2-3',
@@ -110,6 +135,10 @@ describe('CheckIntegrity', () => {
uuid: '2-3-4',
updated_at_timestamp: 2,
},
{
uuid: '5-6-7',
updated_at_timestamp: 5,
},
],
}),
).toEqual({
@@ -121,4 +150,87 @@ describe('CheckIntegrity', () => {
],
})
})
it('should count notes for statistics of free users', async () => {
await createUseCase().execute({
userUuid: '1-2-3',
analyticsId: 1,
freeUser: true,
integrityPayloads: [
{
uuid: '1-2-3',
updated_at_timestamp: 1,
},
{
uuid: '2-3-4',
updated_at_timestamp: 1,
},
{
uuid: '3-4-5',
updated_at_timestamp: 3,
},
],
})
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith('notes-count-free-users', 3, [
Period.Today,
Period.ThisMonth,
])
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['checking-integrity'], 1, [Period.Today])
})
it('should count notes for statistics of paid users', async () => {
await createUseCase().execute({
userUuid: '1-2-3',
analyticsId: 1,
freeUser: false,
integrityPayloads: [
{
uuid: '1-2-3',
updated_at_timestamp: 1,
},
{
uuid: '2-3-4',
updated_at_timestamp: 1,
},
{
uuid: '3-4-5',
updated_at_timestamp: 3,
},
],
})
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith('notes-count-paid-users', 3, [
Period.Today,
Period.ThisMonth,
])
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['checking-integrity'], 1, [Period.Today])
})
it('should not count notes for statistics if they were already counted today', async () => {
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(true)
await createUseCase().execute({
userUuid: '1-2-3',
analyticsId: 1,
freeUser: false,
integrityPayloads: [
{
uuid: '1-2-3',
updated_at_timestamp: 1,
},
{
uuid: '2-3-4',
updated_at_timestamp: 1,
},
{
uuid: '3-4-5',
updated_at_timestamp: 3,
},
],
})
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
})
})

View File

@@ -1,6 +1,12 @@
import { inject, injectable } from 'inversify'
import { IntegrityPayload } from '@standardnotes/payloads'
import { StatisticsStoreInterface } from '@standardnotes/analytics'
import {
AnalyticsActivity,
AnalyticsStoreInterface,
Period,
StatisticsMeasure,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
import TYPES from '../../../Bootstrap/Types'
import { ItemRepositoryInterface } from '../../Item/ItemRepositoryInterface'
@@ -15,16 +21,27 @@ export class CheckIntegrity implements UseCaseInterface {
constructor(
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
) {}
async execute(dto: CheckIntegrityDTO): Promise<CheckIntegrityResponse> {
const serverItemIntegrityPayloads = await this.itemRepository.findItemsForComputingIntegrityPayloads(dto.userUuid)
let notesCount = 0
let filesCount = 0
const serverItemIntegrityPayloadsMap = new Map<string, ExtendedIntegrityPayload>()
for (const serverItemIntegrityPayload of serverItemIntegrityPayloads) {
serverItemIntegrityPayloadsMap.set(serverItemIntegrityPayload.uuid, serverItemIntegrityPayload)
if (serverItemIntegrityPayload.content_type === ContentType.Note) {
notesCount++
}
if (serverItemIntegrityPayload.content_type === ContentType.File) {
filesCount++
}
}
await this.saveNotesCountStatistics(dto.freeUser, dto.analyticsId, { notes: notesCount, files: filesCount })
const clientItemIntegrityPayloadsMap = new Map<string, number>()
for (const clientItemIntegrityPayload of dto.integrityPayloads) {
clientItemIntegrityPayloadsMap.set(
@@ -74,4 +91,33 @@ export class CheckIntegrity implements UseCaseInterface {
mismatches,
}
}
private async saveNotesCountStatistics(
freeUser: boolean,
analyticsId: number,
counts: { notes: number; files: number },
) {
const integrityWasCheckedToday = await this.analyticsStore.wasActivityDone(
AnalyticsActivity.CheckingIntegrity,
analyticsId,
Period.Today,
)
if (!integrityWasCheckedToday) {
await this.analyticsStore.markActivity([AnalyticsActivity.CheckingIntegrity], analyticsId, [Period.Today])
await this.statisticsStore.incrementMeasure(
freeUser ? StatisticsMeasure.NotesCountFreeUsers : StatisticsMeasure.NotesCountPaidUsers,
counts.notes,
[Period.Today, Period.ThisMonth],
)
if (!freeUser) {
await this.statisticsStore.incrementMeasure(StatisticsMeasure.FilesCount, counts.files, [
Period.Today,
Period.ThisMonth,
])
}
}
}
}

View File

@@ -4,4 +4,6 @@ import { IntegrityPayload } from '@standardnotes/payloads'
export type CheckIntegrityDTO = {
userUuid: Uuid
integrityPayloads: IntegrityPayload[]
freeUser: boolean
analyticsId: number
}

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