Compare commits

...

43 Commits

Author SHA1 Message Date
Karol Sójko 55f8f65c3f wip 2022-12-13 13:27:19 +01:00
Karol Sójko 3953dbc6b4 feat(settings): add unsubscribe token for muting emails 2022-12-13 13:18:15 +01:00
Karol Sójko 0b205287d1 fix(settings): binding for controller 2022-12-13 13:18:15 +01:00
Karol Sójko 4f0bc57b1a feat(settings): add controller for muting all emails 2022-12-13 13:18:15 +01:00
Karol Sójko 7d43316597 feat(settings): add mutting all emails use case 2022-12-13 13:18:15 +01:00
Karol Sójko 65d31f011b chore: remove settings dependency from all packages 2022-12-13 13:18:13 +01:00
Karol Sójko 80dd6efae3 feat(settings): replace setting with a domain entity 2022-12-13 13:13:28 +01:00
standardci a96f2c9153 chore(release): publish new version
- @standardnotes/syncing-server@1.25.4
2022-12-13 11:14:35 +00:00
Karol Sójko 225e0aaf88 fix(syncing-server): logs on revisions procedure 2022-12-13 12:12:42 +01:00
standardci f0c85910bc chore(release): publish new version
- @standardnotes/syncing-server@1.25.3
2022-12-13 11:07:23 +00:00
Karol Sójko 124c443528 fix(syncing-server): revisions ownership procedure destructured 2022-12-13 12:05:10 +01:00
standardci 37c7f8d39f chore(release): publish new version
- @standardnotes/syncing-server@1.25.2
2022-12-13 07:19:55 +00:00
Karol Sójko c419f1ce22 fix(syncing-server): change revisions migration to notes 2022-12-13 08:17:55 +01:00
standardci 4949cdfe2f chore(release): publish new version
- @standardnotes/syncing-server@1.25.1
2022-12-13 06:03:06 +00:00
Karol Sójko cd101b96ea fix(syncing-server): revisions procedure properties 2022-12-13 07:01:06 +01:00
standardci 40d0e4631f chore(release): publish new version
- @standardnotes/syncing-server@1.25.0
2022-12-12 19:06:24 +00:00
Karol Sójko a55a995660 feat(syncing-server): fix streaming items for revisions update 2022-12-12 20:03:45 +01:00
standardci 1d576d48ad chore(release): publish new version
- @standardnotes/auth-server@1.67.1
 - @standardnotes/syncing-server@1.24.7
2022-12-12 13:20:47 +00:00
Karol Sójko 4ff8030f87 fix(syncing-server): revisions updating - select fields 2022-12-12 14:18:45 +01:00
Karol Sójko c15e2e2c8f fix: user signed in email template 2022-12-12 14:18:45 +01:00
standardci 41d31a8d75 chore(release): publish new version
- @standardnotes/auth-server@1.67.0
2022-12-12 13:00:40 +00:00
Karol Sójko 10e2a26352 feat(auth): add email subscription unsubscribed event handler 2022-12-12 13:58:35 +01:00
standardci 6e547f77d0 chore(release): publish new version
- @standardnotes/revisions-server@1.9.26
2022-12-12 12:14:52 +00:00
Karol Sójko 530a426601 fix(revisions): responses to match previous response structure 2022-12-12 13:12:46 +01:00
Karol Sójko 642d6bab77 chore: fix triggers for other repos dep 2022-12-12 12:56:28 +01:00
standardci 7980af3d82 chore(release): publish new version
- @standardnotes/analytics@2.12.25
 - @standardnotes/api-gateway@1.40.2
 - @standardnotes/auth-server@1.66.9
 - @standardnotes/domain-events-infra@1.9.56
 - @standardnotes/domain-events@2.104.1
 - @standardnotes/event-store@1.6.53
 - @standardnotes/files-server@1.8.52
 - @standardnotes/revisions-server@1.9.25
 - @standardnotes/scheduler-server@1.15.6
 - @standardnotes/syncing-server@1.24.6
 - @standardnotes/websockets-server@1.4.53
 - @standardnotes/workspace-server@1.18.4
2022-12-12 11:38:37 +00:00
Karol Sójko 2980c42e88 fix(domain-events): add additional domain event services 2022-12-12 12:36:09 +01:00
standardci b03994f9db chore(release): publish new version
- @standardnotes/analytics@2.12.24
2022-12-12 11:31:36 +00:00
Karol Sójko 41906ec2f9 fix(analytics): daily analytics report template 2022-12-12 12:29:16 +01:00
standardci 4d1e7ff2a5 chore(release): publish new version
- @standardnotes/analytics@2.12.23
 - @standardnotes/api-gateway@1.40.1
 - @standardnotes/auth-server@1.66.8
 - @standardnotes/domain-events-infra@1.9.55
 - @standardnotes/domain-events@2.104.0
 - @standardnotes/event-store@1.6.52
 - @standardnotes/files-server@1.8.51
 - @standardnotes/revisions-server@1.9.24
 - @standardnotes/scheduler-server@1.15.5
 - @standardnotes/syncing-server@1.24.5
 - @standardnotes/websockets-server@1.4.52
 - @standardnotes/workspace-server@1.18.3
2022-12-12 11:26:27 +00:00
Karol Sójko 7f18fcfc13 feat(domain-events): add event for email subscription unsubscribed 2022-12-12 12:24:31 +01:00
standardci ff02ce0747 chore(release): publish new version
- @standardnotes/analytics@2.12.22
2022-12-12 10:31:56 +00:00
Karol Sójko a6056600eb fix(analytics): report event publishing 2022-12-12 11:29:41 +01:00
standardci 24c94326d5 chore(release): publish new version
- @standardnotes/analytics@2.12.21
2022-12-12 09:51:01 +00:00
Karol Sójko 48c0cb5e62 fix(analytics): add debug logs for report 2022-12-12 10:49:11 +01:00
standardci 9968efe1b2 chore(release): publish new version
- @standardnotes/syncing-server@1.24.4
2022-12-12 08:49:03 +00:00
Karol Sójko 6368342149 fix(syncing-server): data integrity check on revisions fix 2022-12-12 09:46:35 +01:00
standardci b5f73db210 chore(release): publish new version
- @standardnotes/api-gateway@1.40.0
2022-12-12 04:12:20 +00:00
Karol Sójko 22d6a02d04 feat(api-gateway): add unsubscribe from emails endpoint 2022-12-12 05:10:18 +01:00
standardci 4e0bcfcccf chore(release): publish new version
- @standardnotes/auth-server@1.66.7
2022-12-09 14:30:03 +00:00
Karol Sójko 104313c15d fix(auth): linter issue 2022-12-09 15:27:39 +01:00
standardci 814289af46 chore(release): publish new version
- @standardnotes/analytics@2.12.20
 - @standardnotes/api-gateway@1.39.24
 - @standardnotes/auth-server@1.66.6
 - @standardnotes/domain-events-infra@1.9.54
 - @standardnotes/domain-events@2.103.2
 - @standardnotes/event-store@1.6.51
 - @standardnotes/files-server@1.8.50
 - @standardnotes/revisions-server@1.9.23
 - @standardnotes/scheduler-server@1.15.4
 - @standardnotes/syncing-server@1.24.3
 - @standardnotes/websockets-server@1.4.51
 - @standardnotes/workspace-server@1.18.2
2022-12-09 14:10:16 +00:00
Karol Sójko 3096cd98d5 feat(analytics) replace daily analytics report generated event with email requested 2022-12-09 15:08:17 +01:00
167 changed files with 3021 additions and 1036 deletions
@@ -187,7 +187,7 @@ jobs:
tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }} tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
- name: Run E2E test suite - name: Run E2E test suite
uses: convictional/trigger-workflow-and-wait@v1.6.3 uses: convictional/trigger-workflow-and-wait@master
with: with:
owner: standardnotes owner: standardnotes
repo: e2e repo: e2e
Generated
+120 -9
View File
@@ -66,7 +66,7 @@ const RAW_RUNTIME_STATE =
"reference": "workspace:packages/security"\ "reference": "workspace:packages/security"\
},\ },\
{\ {\
"name": "@standardnotes/settings",\ "name": "@standardnotes/settings-server",\
"reference": "workspace:packages/settings"\ "reference": "workspace:packages/settings"\
},\ },\
{\ {\
@@ -107,7 +107,7 @@ const RAW_RUNTIME_STATE =
["@standardnotes/scheduler-server", ["workspace:packages/scheduler"]],\ ["@standardnotes/scheduler-server", ["workspace:packages/scheduler"]],\
["@standardnotes/security", ["workspace:packages/security"]],\ ["@standardnotes/security", ["workspace:packages/security"]],\
["@standardnotes/server-monorepo", ["workspace:."]],\ ["@standardnotes/server-monorepo", ["workspace:."]],\
["@standardnotes/settings", ["workspace:packages/settings"]],\ ["@standardnotes/settings-server", ["workspace:packages/settings"]],\
["@standardnotes/sncrypto-node", ["workspace:packages/sncrypto-node"]],\ ["@standardnotes/sncrypto-node", ["workspace:packages/sncrypto-node"]],\
["@standardnotes/syncing-server", ["workspace:packages/syncing-server"]],\ ["@standardnotes/syncing-server", ["workspace:packages/syncing-server"]],\
["@standardnotes/time", ["workspace:packages/time"]],\ ["@standardnotes/time", ["workspace:packages/time"]],\
@@ -2582,6 +2582,20 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\ ["reflect-metadata", "npm:0.1.13"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\
["npm:1.20.13", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.20.13-3efe52d749-67bdb982ec.zip/node_modules/@standardnotes/api/",\
"packageDependencies": [\
["@standardnotes/api", "npm:1.20.13"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/encryption", "npm:1.19.21"],\
["@standardnotes/models", "npm:1.38.0"],\
["@standardnotes/responses", "npm:1.12.9"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/utils", "npm:1.13.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@standardnotes/api-gateway", [\ ["@standardnotes/api-gateway", [\
@@ -2656,7 +2670,6 @@ const RAW_RUNTIME_STATE =
["@standardnotes/predicates", "workspace:packages/predicates"],\ ["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/responses", "npm:1.11.1"],\ ["@standardnotes/responses", "npm:1.11.1"],\
["@standardnotes/security", "workspace:packages/security"],\ ["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/settings", "workspace:packages/settings"],\
["@standardnotes/sncrypto-common", "npm:1.13.0"],\ ["@standardnotes/sncrypto-common", "npm:1.13.0"],\
["@standardnotes/sncrypto-node", "workspace:packages/sncrypto-node"],\ ["@standardnotes/sncrypto-node", "workspace:packages/sncrypto-node"],\
["@standardnotes/time", "workspace:packages/time"],\ ["@standardnotes/time", "workspace:packages/time"],\
@@ -2812,6 +2825,19 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\ ["reflect-metadata", "npm:0.1.13"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\
["npm:1.19.21", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.19.21-dfa10f00e6-c8c2c27bfe.zip/node_modules/@standardnotes/encryption/",\
"packageDependencies": [\
["@standardnotes/encryption", "npm:1.19.21"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/models", "npm:1.38.0"],\
["@standardnotes/responses", "npm:1.12.9"],\
["@standardnotes/sncrypto-common", "npm:1.13.3"],\
["@standardnotes/utils", "npm:1.13.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@standardnotes/event-store", [\ ["@standardnotes/event-store", [\
@@ -2867,6 +2893,17 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\ ["reflect-metadata", "npm:0.1.13"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\
["npm:1.55.3", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.55.3-c124505183-b39fe2d49b.zip/node_modules/@standardnotes/features/",\
"packageDependencies": [\
["@standardnotes/features", "npm:1.55.3"],\
["@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", [\ ["@standardnotes/files-server", [\
@@ -2948,6 +2985,13 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\ ["reflect-metadata", "npm:0.1.13"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\
["npm:1.38.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.38.0-108f602f56-2dc2ac957e.zip/node_modules/@standardnotes/models/",\
"packageDependencies": [\
["@standardnotes/models", "npm:1.38.0"]\
],\
"linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@standardnotes/payloads", [\ ["@standardnotes/payloads", [\
@@ -3001,6 +3045,17 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\ ["reflect-metadata", "npm:0.1.13"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\
["npm:1.12.9", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.12.9-280dc75972-353fe1ca6d.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
["@standardnotes/responses", "npm:1.12.9"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.55.3"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@standardnotes/revisions-server", [\ ["@standardnotes/revisions-server", [\
@@ -3132,15 +3187,46 @@ const RAW_RUNTIME_STATE =
"linkType": "SOFT"\ "linkType": "SOFT"\
}]\ }]\
]],\ ]],\
["@standardnotes/settings", [\ ["@standardnotes/settings-server", [\
["workspace:packages/settings", {\ ["workspace:packages/settings", {\
"packageLocation": "./packages/settings/",\ "packageLocation": "./packages/settings/",\
"packageDependencies": [\ "packageDependencies": [\
["@standardnotes/settings", "workspace:packages/settings"],\ ["@standardnotes/settings-server", "workspace:packages/settings"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\ ["@sentry/node", "npm:7.19.0"],\
["@standardnotes/api", "npm:1.20.13"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/time", "workspace:packages/time"],\
["@types/cors", "npm:2.8.12"],\
["@types/dotenv", "npm:8.2.0"],\
["@types/express", "npm:4.17.14"],\
["@types/inversify-express-utils", "npm:2.0.0"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
["aws-sdk", "npm:2.1260.0"],\
["cors", "npm:2.8.5"],\
["dotenv", "npm:16.0.1"],\
["eslint", "npm:8.25.0"],\
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
["express", "npm:4.18.2"],\
["helmet", "npm:6.0.0"],\
["inversify", "npm:6.0.1"],\
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["mysql2", "npm:2.3.3"],\
["newrelic", "npm:9.6.0"],\
["npm-check-updates", "npm:16.0.1"],\
["reflect-metadata", "npm:0.1.13"],\ ["reflect-metadata", "npm:0.1.13"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"]\ ["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
["typeorm", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.10"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"],\
["winston", "npm:3.8.2"]\
],\ ],\
"linkType": "SOFT"\ "linkType": "SOFT"\
}]\ }]\
@@ -3153,6 +3239,14 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\ ["reflect-metadata", "npm:0.1.13"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\
["npm:1.13.3", {\
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.3-97ef3850ce-a73af90962.zip/node_modules/@standardnotes/sncrypto-common/",\
"packageDependencies": [\
["@standardnotes/sncrypto-common", "npm:1.13.3"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@standardnotes/sncrypto-node", [\ ["@standardnotes/sncrypto-node", [\
@@ -3189,7 +3283,6 @@ const RAW_RUNTIME_STATE =
["@standardnotes/payloads", "npm:1.5.1"],\ ["@standardnotes/payloads", "npm:1.5.1"],\
["@standardnotes/responses", "npm:1.11.1"],\ ["@standardnotes/responses", "npm:1.11.1"],\
["@standardnotes/security", "workspace:packages/security"],\ ["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/settings", "workspace:packages/settings"],\
["@standardnotes/time", "workspace:packages/time"],\ ["@standardnotes/time", "workspace:packages/time"],\
["@types/cors", "npm:2.8.12"],\ ["@types/cors", "npm:2.8.12"],\
["@types/dotenv", "npm:8.2.0"],\ ["@types/dotenv", "npm:8.2.0"],\
@@ -3273,6 +3366,17 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\ ["reflect-metadata", "npm:0.1.13"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\
["npm:1.13.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.13.0-28780a59f0-1578e8adb7.zip/node_modules/@standardnotes/utils/",\
"packageDependencies": [\
["@standardnotes/utils", "npm:1.13.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["dompurify", "npm:2.4.1"],\
["lodash", "npm:4.17.21"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@standardnotes/websockets-server", [\ ["@standardnotes/websockets-server", [\
@@ -6371,6 +6475,13 @@ const RAW_RUNTIME_STATE =
["dompurify", "npm:2.4.0"]\ ["dompurify", "npm:2.4.0"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\
["npm:2.4.1", {\
"packageLocation": "./.yarn/cache/dompurify-npm-2.4.1-1c79f22057-ddc0633356.zip/node_modules/dompurify/",\
"packageDependencies": [\
["dompurify", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\ }]\
]],\ ]],\
["dot-prop", [\ ["dot-prop", [\
Binary file not shown.
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.12.25](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.24...@standardnotes/analytics@2.12.25) (2022-12-12)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.24](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.23...@standardnotes/analytics@2.12.24) (2022-12-12)
### Bug Fixes
* **analytics:** daily analytics report template ([41906ec](https://github.com/standardnotes/server/commit/41906ec2f9fd4d605b1c002826173e14fb534e00))
## [2.12.23](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.22...@standardnotes/analytics@2.12.23) (2022-12-12)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.22](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.21...@standardnotes/analytics@2.12.22) (2022-12-12)
### Bug Fixes
* **analytics:** report event publishing ([a605660](https://github.com/standardnotes/server/commit/a6056600eb96bf175189ad6d62870c9d736f331b))
## [2.12.21](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.20...@standardnotes/analytics@2.12.21) (2022-12-12)
### Bug Fixes
* **analytics:** add debug logs for report ([48c0cb5](https://github.com/standardnotes/server/commit/48c0cb5e62dc8af930de191deaa1eb3ff6c5a29f))
## [2.12.20](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.19...@standardnotes/analytics@2.12.20) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.19](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.18...@standardnotes/analytics@2.12.19) (2022-12-09) ## [2.12.19](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.18...@standardnotes/analytics@2.12.19) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics **Note:** Version bump only for package @standardnotes/analytics
+34 -12
View File
@@ -4,6 +4,7 @@ import 'newrelic'
import { Logger } from 'winston' import { Logger } from 'winston'
import { EmailLevel } from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity' import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
import { Period } from '../src/Domain/Time/Period' import { Period } from '../src/Domain/Time/Period'
@@ -16,6 +17,8 @@ import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env' import { Env } from '../src/Bootstrap/Env'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface' import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue' import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
import { TimerInterface } from '@standardnotes/time'
const requestReport = async ( const requestReport = async (
analyticsStore: AnalyticsStoreInterface, analyticsStore: AnalyticsStoreInterface,
@@ -24,6 +27,8 @@ const requestReport = async (
domainEventPublisher: DomainEventPublisherInterface, domainEventPublisher: DomainEventPublisherInterface,
periodKeyGenerator: PeriodKeyGeneratorInterface, periodKeyGenerator: PeriodKeyGeneratorInterface,
calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue, calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
timer: TimerInterface,
adminEmails: string[],
): Promise<void> => { ): Promise<void> => {
await calculateMonthlyRecurringRevenue.execute({}) await calculateMonthlyRecurringRevenue.execute({})
@@ -213,18 +218,29 @@ const requestReport = async (
}) })
} }
const event = domainEventFactory.createDailyAnalyticsReportGeneratedEvent({ for (const adminEmail of adminEmails) {
activityStatistics: yesterdayActivityStatistics, await domainEventPublisher.publish(
activityStatisticsOverTime: analyticsOverTime, domainEventFactory.createEmailRequestedEvent({
statisticsOverTime, messageIdentifier: 'VERSION_ADOPTION_REPORT',
statisticMeasures, subject: getSubject(),
churn: { body: getBody(
periodKeys: monthlyPeriodKeys, {
values: churnRates, activityStatistics: yesterdayActivityStatistics,
}, activityStatisticsOverTime: analyticsOverTime,
}) statisticsOverTime,
statisticMeasures,
await domainEventPublisher.publish(event) churn: {
periodKeys: monthlyPeriodKeys,
values: churnRates,
},
},
timer,
),
level: EmailLevel.LEVELS.System,
userEmail: adminEmail,
}),
)
}
} }
const container = new ContainerConfigLoader() const container = new ContainerConfigLoader()
@@ -241,9 +257,13 @@ void container.load().then((container) => {
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory) const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher) const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator) const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
const timer: TimerInterface = container.get(TYPES.Timer)
const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get( const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
TYPES.CalculateMonthlyRecurringRevenue, TYPES.CalculateMonthlyRecurringRevenue,
) )
const adminEmails = container.get(TYPES.ADMIN_EMAILS) as string[]
logger.info(`Sending report to following admins: ${adminEmails}`)
Promise.resolve( Promise.resolve(
requestReport( requestReport(
@@ -253,6 +273,8 @@ void container.load().then((container) => {
domainEventPublisher, domainEventPublisher,
periodKeyGenerator, periodKeyGenerator,
calculateMonthlyRecurringRevenue, calculateMonthlyRecurringRevenue,
timer,
adminEmails,
), ),
) )
.then(() => { .then(() => {
+3 -3
View File
@@ -5,17 +5,17 @@ COMMAND=$1 && shift 1
case "$COMMAND" in case "$COMMAND" in
'start-worker' ) 'start-worker' )
echo "Starting Worker..." echo "[Docker] Starting Worker..."
yarn workspace @standardnotes/analytics worker yarn workspace @standardnotes/analytics worker
;; ;;
'report' ) 'report' )
echo "Starting Usage Report Generation..." echo "[Docker] Starting Usage Report Generation..."
yarn workspace @standardnotes/analytics report yarn workspace @standardnotes/analytics report
;; ;;
* ) * )
echo "Unknown command" echo "[Docker] Unknown command"
;; ;;
esac esac
+1 -1
View File
@@ -7,5 +7,5 @@ module.exports = {
transform: { transform: {
...tsjPreset.transform, ...tsjPreset.transform,
}, },
coveragePathIgnorePatterns: ['/Infra/'], coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/'],
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/analytics", "name": "@standardnotes/analytics",
"version": "2.12.19", "version": "2.12.25",
"engines": { "engines": {
"node": ">=18.0.0 <19.0.0" "node": ">=18.0.0 <19.0.0"
}, },
@@ -40,7 +40,7 @@
"@newrelic/winston-enricher": "^4.0.0", "@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0", "@sentry/node": "^7.19.0",
"@standardnotes/common": "workspace:*", "@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:*", "@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*", "@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*", "@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/time": "workspace:*", "@standardnotes/time": "workspace:*",
@@ -130,6 +130,7 @@ export class ContainerConfigLoader {
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true)) container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL')) container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true)) container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
// Repositories // Repositories
container container
@@ -11,6 +11,7 @@ const TYPES = {
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'), SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'), REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'), NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
ADMIN_EMAILS: Symbol.for('ADMIN_EMAILS'),
// Repositories // Repositories
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'), AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'), RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
@@ -0,0 +1,11 @@
import { TimerInterface } from '@standardnotes/time'
import { html } from './daily-analytics-report.html'
export function getSubject(): string {
return `Daily analytics report ${new Date().toLocaleDateString('en-US')}`
}
export function getBody(data: unknown, timer: TimerInterface): string {
return html(data, timer)
}
@@ -0,0 +1,966 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TimerInterface } from '@standardnotes/time'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { Period } from '../Time/Period'
const getChartUrls = (
data: any,
): {
subscriptions: string
users: string
quarterlyPerformance: string
churn: string
mrr: string
mrrMonthly: string
} => {
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
)
const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
)
const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
)
const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
)
const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
)
const subscriptionsLinerOverTimeConfig = {
type: 'line',
data: {
labels: subscriptionPurchasingOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
datasets: [
{
label: 'Subscription Purchases',
backgroundColor: 'rgb(25, 255, 140)',
borderColor: 'rgb(25, 255, 140)',
data: subscriptionPurchasingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Subscription Renewals',
backgroundColor: 'rgb(54, 162, 235)',
borderColor: 'rgb(54, 162, 235)',
data: subscriptionRenewingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Subscription Refunds',
backgroundColor: 'rgb(255, 221, 51)',
borderColor: 'rgb(255, 221, 51)',
data: subscriptionRefundingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Subscription Cancels',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: subscriptionCancelledOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Subscription Reactivations',
backgroundColor: 'rgb(221, 51, 255)',
borderColor: 'rgb(221, 51, 255)',
data: subscriptionReactivatedOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
],
},
}
const userRegistrationOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
)
const userDeletionOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
)
const usersLinerOverTimeConfig = {
type: 'line',
data: {
labels: userRegistrationOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
datasets: [
{
label: 'User Registrations',
backgroundColor: 'rgb(25, 255, 140)',
borderColor: 'rgb(25, 255, 140)',
data: userRegistrationOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Account Deletions',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: userDeletionOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
],
},
}
const quarters = [Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear]
const quarterlyUserRegistrations = []
const quarterlySubscriptionPurchases = []
const quarterlySubscriptionRenewals = []
for (const quarter of quarters) {
const registrations =
data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.Register && a.period === quarter,
)?.totalCount ?? 0
const purchases =
data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === quarter,
)?.totalCount ?? 0
const renewals =
data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === quarter,
)?.totalCount ?? 0
quarterlyUserRegistrations.push(registrations)
quarterlySubscriptionPurchases.push(purchases)
quarterlySubscriptionRenewals.push(renewals)
}
const quarterlyConfig = {
type: 'bar',
data: {
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
datasets: [
{
label: 'User Registrations',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
borderColor: 'rgb(255, 99, 132)',
borderWidth: 1,
data: quarterlyUserRegistrations,
},
{
label: 'Subscription Purchases',
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgb(54, 162, 235)',
borderWidth: 1,
data: quarterlySubscriptionPurchases,
},
{
label: 'Subscription Renewals',
backgroundColor: 'rgb(25, 255, 140, 0.5)',
borderColor: 'rgb(25, 255, 140)',
borderWidth: 1,
data: quarterlySubscriptionRenewals,
},
],
},
options: {
title: {
display: true,
text: 'Quarterly Performance',
},
plugins: {
datalabels: {
anchor: 'center',
align: 'center',
color: '#666',
font: {
weight: 'normal',
},
},
},
},
}
const monthlyChurnRates = data.churn.values.map((value: { rate: number }) => +value.rate.toFixed(2))
const churnConfig = {
type: 'bar',
data: {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
datasets: [
{
label: 'Churn Percent',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
borderColor: 'rgb(255, 99, 132)',
borderWidth: 1,
data: monthlyChurnRates,
},
],
},
options: {
title: {
display: true,
text: 'Monthly Churn Rate',
},
plugins: {
datalabels: {
anchor: 'center',
align: 'center',
color: '#666',
font: {
weight: 'normal',
},
},
},
},
}
const mrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
)
const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
)
const annualPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
)
const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
)
const proPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
)
const plusPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
)
const mrrOverTimeConfig = {
type: 'line',
data: {
labels: mrrOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
datasets: [
{
label: 'MRR',
backgroundColor: 'rgb(25, 255, 140)',
borderColor: 'rgb(25, 255, 140)',
data: mrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Monthly Plans',
backgroundColor: 'rgb(54, 162, 235)',
borderColor: 'rgb(54, 162, 235)',
data: monthlyPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Annual Plans',
backgroundColor: 'rgb(255, 221, 51)',
borderColor: 'rgb(255, 221, 51)',
data: annualPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Five Year Plans',
backgroundColor: 'rgb(255, 120, 120)',
borderColor: 'rgb(255, 120, 120)',
data: fiveYearPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - PRO Plans',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: proPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - PLUS Plans',
backgroundColor: 'rgb(221, 51, 255)',
borderColor: 'rgb(221, 51, 255)',
data: plusPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
],
},
}
const mrrMonthlyOverTime = data.statisticsOverTime
.find((a: { name: string; period: Period }) => a.name === 'mrr' && a.period === Period.ThisYear)
?.counts.map((count: { totalCount: number }) => +count.totalCount.toFixed(2))
const mrrMonthlyConfig = {
type: 'bar',
data: {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
datasets: [
{
label: 'MRR',
backgroundColor: 'rgba(25, 255, 140, 0.5)',
borderColor: 'rgb(25, 255, 140)',
borderWidth: 1,
data: mrrMonthlyOverTime,
},
],
},
options: {
title: {
display: true,
text: 'Monthly MRR',
},
plugins: {
datalabels: {
anchor: 'center',
align: 'center',
color: '#666',
font: {
weight: 'normal',
},
},
},
},
}
return {
subscriptions: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
JSON.stringify(subscriptionsLinerOverTimeConfig),
)}`,
users: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(usersLinerOverTimeConfig))}`,
quarterlyPerformance: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
JSON.stringify(quarterlyConfig),
)}`,
churn: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(churnConfig))}`,
mrr: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrOverTimeConfig))}`,
mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
}
}
export const html = (data: any, timer: TimerInterface) => {
const chartUrls = getChartUrls(data)
const successfullPaymentsActivity = data.activityStatistics.find(
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentSuccess && Period.Yesterday,
)
const failedPaymentsActivity = data.activityStatistics.find(
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentFailed && Period.Yesterday,
)
const limitedDiscountPurchasedActivity = data.activityStatistics.find(
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.LimitedDiscountOfferPurchased && Period.Yesterday,
)
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
)
const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
)
const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
)
const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
)
const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
)
const userRegistrationOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
)
const userDeletionOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
)
const incomeMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Income && a.period === Period.Yesterday,
)
const refundMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Refunds && a.period === Period.Yesterday,
)
const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0
const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0
const revenueYesterday = incomeYesterday - refundsYesterday
const subscriptionLengthMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.Yesterday,
)
const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0),
)
const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
)
const subscriptionRemainingTimePercentageYesterday = Math.floor(
subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0,
)
const registrationLengthMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.Yesterday,
)
const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationLengthMeasureYesterday?.average ?? 0),
)
const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
)
const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0),
)
const incomeMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Income && a.period === Period.ThisMonth,
)
const refundMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Refunds && a.period === Period.ThisMonth,
)
const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0
const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0
const revenueThisMonth = incomeThisMonth - refundsThisMonth
const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.ThisMonth,
)
const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0),
)
const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
)
const subscriptionRemainingTimePercentageThisMonth = Math.floor(
subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0,
)
const registrationLengthMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.ThisMonth,
)
const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationLengthMeasureThisMonth?.average ?? 0),
)
const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
)
const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0),
)
const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
)
const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
)
const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
)
const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
)
const mrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
)
const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
)
const annualPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
)
const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
)
const proPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
)
const plusPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
)
const today = new Date()
const thisMonthPeriodKey = `${today.getFullYear().toString()}-${(today.getMonth() + 1).toString()}`
const thisMonthChurn = data.churn.values.find(
(value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
)
return ` <div>
<p>Hello,</p>
<p>
<strong>Here are some statistics from yesterday:</strong>
</p>
<ul>
<li>
<b>Payments</b>
<ul>
<li>
Revenue: <b>$${revenueYesterday.toLocaleString('en-US')}</b> (Income: $
${incomeYesterday.toLocaleString('en-US')}, Refunds: $${refundsYesterday.toLocaleString('en-US')})
</li>
<li>
Successfull payments: <b>${successfullPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Failed payments: <b>${failedPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
<li>
<b>MRR Breakdown</b>
<ul>
<li>
<b>Total:</b> $${mrrOverTime?.counts[mrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
</li>
<li>
<b>By Subscription Type:</b>
<ul>
<li>
<b>PLUS:</b> $
${plusPlansMrrOverTime?.counts[plusPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
</li>
<li>
<b>PRO:</b> $
${proPlansMrrOverTime?.counts[proPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
</li>
</ul>
</li>
<li>
<b>By Billing Frequency:</b>
<ul>
<li>
<b>Monthly:</b> $
${monthlyPlansMrrOverTime?.counts[monthlyPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
'en-US',
)}
</li>
<li>
<b>Annual:</b> $
${annualPlansMrrOverTime?.counts[annualPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
'en-US',
)}
</li>
<li>
<b>5-year:</b> $
${fiveYearPlansMrrOverTime?.counts[fiveYearPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
'en-US',
)}
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Income Breakdown</b>
<ul>
<li>
<b>Plus Subscription:</b>
<ul>
<li>
<b>${plusSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
<b>$${plusSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${plusSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
<b>$${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${plusSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
<li>
<b>Pro Subscription:</b>
<ul>
<li>
<b>${proSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
<b>$${proSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${proSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
<b>$${proSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${proSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Users</b>
<ul>
<li>
Number of users registered:${' '}
<b>
${userRegistrationOverTime?.counts[userRegistrationOverTime?.counts.length - 1]?.totalCount.toLocaleString(
'en-US',
)}
</b>
</li>
<li>
Number of users unregistered:${' '}
<b>
${userDeletionOverTime?.counts[userDeletionOverTime?.counts.length - 1]?.totalCount.toLocaleString('en-US')}
</b>${' '}
(average account duration: ${registrationLengthDurationYesterday.days} days${' '}
${registrationLengthDurationYesterday.hours} hours ${registrationLengthDurationYesterday.minutes} minutes)
</li>
</ul>
</li>
<li>
<b>Subscriptions</b>
<ul>
<li>
Number of subscriptions purchased:${' '}
<b>
${subscriptionPurchasingOverTime?.counts[
subscriptionPurchasingOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>${' '}
(includes <b>${limitedDiscountPurchasedActivity?.totalCount.toLocaleString('en-US')}</b> limited time
offer purchases)
</li>
<li>
Number of subscriptions renewed:${' '}
<b>
${subscriptionRenewingOverTime?.counts[
subscriptionRenewingOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>
</li>
<li>
Number of subscriptions refunded:${' '}
<b>
${subscriptionRefundingOverTime?.counts[
subscriptionRefundingOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>
</li>
<li>
Number of subscriptions cancelled:${' '}
<b>
${subscriptionCancelledOverTime?.counts[
subscriptionCancelledOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>${' '}
(average subscription duration: ${subscriptionLengthDurationYesterday.days} days${' '}
${subscriptionLengthDurationYesterday.hours} hours ${subscriptionLengthDurationYesterday.minutes} minutes,
average remaining subscription percentage: ${subscriptionRemainingTimePercentageYesterday}%)
</li>
<li>
Number of subscriptions reactivated:${' '}
<b>
${subscriptionReactivatedOverTime?.counts[
subscriptionReactivatedOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>
</li>
<li>
Average time from registration to subscription purchase:${' '}
<b>
${registrationToSubscriptionDurationYesterday.days} days${' '}
${registrationToSubscriptionDurationYesterday.hours} hours${' '}
${registrationToSubscriptionDurationYesterday.minutes} minutes
</b>
</li>
</ul>
</li>
</ul>
<p>
<strong>Here are some statistics from last 30 days:</strong>
</p>
<ul>
<li>
<b>Payments (This Month)</b>
<ul>
<li>
Revenue: <b>$${revenueThisMonth.toLocaleString('en-US')}</b>
</li>
<li>
Income: <b>$${incomeThisMonth.toLocaleString('en-US')}</b>
</li>
<li>
Refunds: <b>$${refundsThisMonth.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
<li>
<b>Income Breakdown (This Month)</b>
<ul>
<li>
<b>Plus Subscription:</b>
<ul>
<li>
<b>${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
<b>$${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${plusSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
<b>$${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
<li>
<b>Pro Subscription:</b>
<ul>
<li>
<b>${proSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
<b>$${proSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${proSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
<b>$${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${proSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Users</b>
<ul>
<li>
Number of users registered: <b>${userRegistrationOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of users unregistered: <b>${userDeletionOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Average account duration this month:${' '}
<b>
${registrationLengthDurationThisMonth.days} days ${registrationLengthDurationThisMonth.hours} hours${' '}
${registrationLengthDurationThisMonth.minutes} minutes
</b>
</li>
</ul>
</li>
<li>
<b>Subscriptions</b>
<ul>
<li>
Number of subscriptions purchased:${' '}
<b>${subscriptionPurchasingOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of subscriptions renewed:${' '}
<b>${subscriptionRenewingOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of subscriptions refunded:${' '}
<b>${subscriptionRefundingOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of subscriptions cancelled:${' '}
<b>${subscriptionCancelledOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of subscriptions reactivated:${' '}
<b>${subscriptionReactivatedOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Average subscription duration this month:${' '}
<b>
${subscriptionLengthDurationThisMonth.days} days ${subscriptionLengthDurationThisMonth.hours} hours${' '}
${subscriptionLengthDurationThisMonth.minutes} minutes
</b>
</li>
<li>
Average subscription remaining percentage this month:${' '}
<b>${subscriptionRemainingTimePercentageThisMonth}%</b>
</li>
<li>
Average time from registration to subscription purchase this month:${' '}
<b>
${registrationToSubscriptionDurationThisMonth.days} days${' '}
${registrationToSubscriptionDurationThisMonth.hours} hours${' '}
${registrationToSubscriptionDurationThisMonth.minutes} minutes
</b>
</li>
</ul>
</li>
</ul>
<p>
<strong>Here is the MRR chart over 30 days:</strong>
</p>
<img src=${chartUrls.mrr}></img>
<p>
<strong>Here is the MRR Monthly chart this year:</strong>
</p>
<img src=${chartUrls.mrrMonthly}></img>
<p>
<strong>Here is the subscription chart over 30 days:</strong>
</p>
<img src=${chartUrls.subscriptions}></img>
<p>
<strong>Here is the users chart over 30 days:</strong>
</p>
<img src=${chartUrls.users}></img>
<p>
<strong>Here is the monthly churn rate percentage:</strong>
</p>
<p>✅ GREAT! Up to 7% 🔶 OKAY: 8-10% 🩸 BAD: 11 -15 % 🚨 TERRIBLE! 16-20%</p>
<p>Churn is calculated by the following formula:</p>
<p>
( Existing Customers Churn [${thisMonthChurn?.existingCustomersChurn}] + New Customers Churn [
${thisMonthChurn?.newCustomersChurn}] ) * 100 / Average Customers Count This Month [
${thisMonthChurn?.averageCustomersCount}]
</p>
<img src=${chartUrls.churn}></img>
<p>
<strong>Here is quarterly performance chart:</strong>
</p>
<img src=${chartUrls.quarterlyPerformance}></img>
<p>Thanks,SN</p>
</div>`
}
@@ -1,6 +1,6 @@
/* istanbul ignore file */ /* istanbul ignore file */
import { DomainEventService, DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events' import { DomainEventService, EmailRequestedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time' import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types' import TYPES from '../../Bootstrap/Types'
@@ -9,55 +9,20 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
@injectable() @injectable()
export class DomainEventFactory implements DomainEventFactoryInterface { export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {} constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createEmailRequestedEvent(dto: {
createDailyAnalyticsReportGeneratedEvent(dto: { userEmail: string
activityStatistics: Array<{ messageIdentifier: string
name: string level: string
retention: number body: string
totalCount: number subject: string
}> }): EmailRequestedEvent {
statisticMeasures: Array<{
name: string
totalValue: number
average: number
increments: number
period: number
}>
activityStatisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
totalCount: number
}>
statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}>
churn: {
periodKeys: Array<string>
values: Array<{
rate: number
periodKey: string
averageCustomersCount: number
existingCustomersChurn: number
newCustomersChurn: number
}>
}
}): DailyAnalyticsReportGeneratedEvent {
return { return {
type: 'DAILY_ANALYTICS_REPORT_GENERATED', type: 'EMAIL_REQUESTED',
createdAt: this.timer.getUTCDate(), createdAt: this.timer.getUTCDate(),
meta: { meta: {
correlation: { correlation: {
userIdentifier: '', userIdentifier: dto.userEmail,
userIdentifierType: 'uuid', userIdentifierType: 'email',
}, },
origin: DomainEventService.Analytics, origin: DomainEventService.Analytics,
}, },
@@ -1,45 +1,11 @@
import { DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events' import { EmailRequestedEvent } from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface { export interface DomainEventFactoryInterface {
createDailyAnalyticsReportGeneratedEvent(dto: { createEmailRequestedEvent(dto: {
activityStatistics: Array<{ userEmail: string
name: string messageIdentifier: string
retention: number level: string
totalCount: number body: string
}> subject: string
statisticMeasures: Array<{ }): EmailRequestedEvent
name: string
totalValue: number
average: number
increments: number
period: number
}>
activityStatisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
totalCount: number
}>
statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}>
churn: {
periodKeys: Array<string>
values: Array<{
rate: number
periodKey: string
averageCustomersCount: number
existingCustomersChurn: number
newCustomersChurn: number
}>
}
}): DailyAnalyticsReportGeneratedEvent
} }
+1
View File
@@ -11,6 +11,7 @@ WEB_SOCKET_SERVER_URL=http://websockets:3000
PAYMENTS_SERVER_URL=http://payments:3000 PAYMENTS_SERVER_URL=http://payments:3000
FILES_SERVER_URL=http://files:3000 FILES_SERVER_URL=http://files:3000
REVISIONS_SERVER_URL=http://revisions:3000 REVISIONS_SERVER_URL=http://revisions:3000
EMAIL_SERVER_URL=http://email:3000
HTTP_CALL_TIMEOUT=60000 HTTP_CALL_TIMEOUT=60000
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.1...@standardnotes/api-gateway@1.40.2) (2022-12-12)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.40.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.0...@standardnotes/api-gateway@1.40.1) (2022-12-12)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.40.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.24...@standardnotes/api-gateway@1.40.0) (2022-12-12)
### Features
* **api-gateway:** add unsubscribe from emails endpoint ([22d6a02](https://github.com/standardnotes/api-gateway/commit/22d6a02d049ba3bde890c7def91e19f013ba3e22))
## [1.39.24](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.23...@standardnotes/api-gateway@1.39.24) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.23](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.22...@standardnotes/api-gateway@1.39.23) (2022-12-09) ## [1.39.23](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.22...@standardnotes/api-gateway@1.39.23) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway **Note:** Version bump only for package @standardnotes/api-gateway
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/api-gateway", "name": "@standardnotes/api-gateway",
"version": "1.39.23", "version": "1.40.2",
"engines": { "engines": {
"node": ">=18.0.0 <19.0.0" "node": ">=18.0.0 <19.0.0"
}, },
@@ -55,6 +55,7 @@ export class ContainerConfigLoader {
container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL')) container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL'))
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL')) container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
container.bind(TYPES.REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL', true)) container.bind(TYPES.REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL', true))
container.bind(TYPES.EMAIL_SERVER_URL).toConstantValue(env.get('EMAIL_SERVER_URL', true))
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true)) container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true)) container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET')) container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
@@ -8,6 +8,7 @@ const TYPES = {
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'), PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'), FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
REVISIONS_SERVER_URL: Symbol.for('REVISIONS_SERVER_URL'), REVISIONS_SERVER_URL: Symbol.for('REVISIONS_SERVER_URL'),
EMAIL_SERVER_URL: Symbol.for('EMAIL_SERVER_URL'),
WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'), WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'),
WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'), WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'),
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'), AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
@@ -29,4 +29,14 @@ export class ActionsController extends BaseHttpController {
async methods(request: Request, response: Response): Promise<void> { async methods(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'auth/methods', request.body) await this.httpService.callAuthServer(request, response, 'auth/methods', request.body)
} }
@httpGet('/unsubscribe/:token')
async emailUnsubscribe(request: Request, response: Response): Promise<void> {
await this.httpService.callEmailServer(
request,
response,
`subscriptions/actions/unsubscribe/${request.params.token}`,
request.body,
)
}
} }
@@ -19,6 +19,7 @@ export class HttpService implements HttpServiceInterface {
@inject(TYPES.WORKSPACE_SERVER_URL) private workspaceServerUrl: string, @inject(TYPES.WORKSPACE_SERVER_URL) private workspaceServerUrl: string,
@inject(TYPES.WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string, @inject(TYPES.WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string,
@inject(TYPES.REVISIONS_SERVER_URL) private revisionsServerUrl: string, @inject(TYPES.REVISIONS_SERVER_URL) private revisionsServerUrl: string,
@inject(TYPES.EMAIL_SERVER_URL) private emailServerUrl: string,
@inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number, @inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface, @inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Logger) private logger: Logger, @inject(TYPES.Logger) private logger: Logger,
@@ -65,6 +66,21 @@ export class HttpService implements HttpServiceInterface {
await this.callServer(this.authServerUrl, request, response, endpoint, payload) await this.callServer(this.authServerUrl, request, response, endpoint, payload)
} }
async callEmailServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
if (!this.emailServerUrl) {
response.status(400).send({ message: 'Email Server not configured' })
return
}
await this.callServer(this.emailServerUrl, request, response, endpoint, payload)
}
async callWorkspaceServer( async callWorkspaceServer(
request: Request, request: Request,
response: Response, response: Response,
@@ -1,6 +1,12 @@
import { Request, Response } from 'express' import { Request, Response } from 'express'
export interface HttpServiceInterface { export interface HttpServiceInterface {
callEmailServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
callAuthServer( callAuthServer(
request: Request, request: Request,
response: Response, response: Response,
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.67.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.0...@standardnotes/auth-server@1.67.1) (2022-12-12)
### Bug Fixes
* user signed in email template ([c15e2e2](https://github.com/standardnotes/server/commit/c15e2e2c8f3a6c177e227d25440501fa38dd3d0e))
# [1.67.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.9...@standardnotes/auth-server@1.67.0) (2022-12-12)
### Features
* **auth:** add email subscription unsubscribed event handler ([10e2a26](https://github.com/standardnotes/server/commit/10e2a263522dfa33c06940f29cb77f783f66b20c))
## [1.66.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.8...@standardnotes/auth-server@1.66.9) (2022-12-12)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.66.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.7...@standardnotes/auth-server@1.66.8) (2022-12-12)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.66.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.6...@standardnotes/auth-server@1.66.7) (2022-12-09)
### Bug Fixes
* **auth:** linter issue ([104313c](https://github.com/standardnotes/server/commit/104313c15df79f6308d23e21f65111e5bd3d9c72))
## [1.66.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.5...@standardnotes/auth-server@1.66.6) (2022-12-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.66.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.4...@standardnotes/auth-server@1.66.5) (2022-12-09) ## [1.66.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.4...@standardnotes/auth-server@1.66.5) (2022-12-09)
**Note:** Version bump only for package @standardnotes/auth-server **Note:** Version bump only for package @standardnotes/auth-server
+21 -22
View File
@@ -3,20 +3,19 @@ import 'reflect-metadata'
import 'newrelic' import 'newrelic'
import { Stream } from 'stream' import { Stream } from 'stream'
import { Logger } from 'winston' import { Logger } from 'winston'
import * as dayjs from 'dayjs' import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc' import * as utc from 'dayjs/plugin/utc'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/domain-core'
import { PermissionName } from '@standardnotes/features'
import { ContainerConfigLoader } from '../src/Bootstrap/Container' import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types' import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env' import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface' import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface' import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
import { MuteFailedBackupsEmailsOption, MuteFailedCloudBackupsEmailsOption, SettingName } from '@standardnotes/settings'
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface' import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
import { PermissionName } from '@standardnotes/features'
import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface' import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface'
const inputArgs = process.argv.slice(2) const inputArgs = process.argv.slice(2)
@@ -30,38 +29,38 @@ const requestBackups = async (
domainEventFactory: DomainEventFactoryInterface, domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface, domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => { ): Promise<void> => {
let settingName: SettingName, let settingName: string,
permissionName: PermissionName, permissionName: PermissionName,
muteEmailsSettingName: SettingName, muteEmailsSettingName: string,
muteEmailsSettingValue: string, muteEmailsSettingValue: string,
providerTokenSettingName: SettingName providerTokenSettingName: string
switch (backupProvider) { switch (backupProvider) {
case 'email': case 'email':
settingName = SettingName.EmailBackupFrequency settingName = SettingName.NAMES.EmailBackupFrequency
permissionName = PermissionName.DailyEmailBackup permissionName = PermissionName.DailyEmailBackup
muteEmailsSettingName = SettingName.MuteFailedBackupsEmails muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted muteEmailsSettingValue = 'muted'
break break
case 'dropbox': case 'dropbox':
settingName = SettingName.DropboxBackupFrequency settingName = SettingName.NAMES.DropboxBackupFrequency
permissionName = PermissionName.DailyDropboxBackup permissionName = PermissionName.DailyDropboxBackup
muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted muteEmailsSettingValue = 'muted'
providerTokenSettingName = SettingName.DropboxBackupToken providerTokenSettingName = SettingName.NAMES.DropboxBackupToken
break break
case 'one_drive': case 'one_drive':
settingName = SettingName.OneDriveBackupFrequency settingName = SettingName.NAMES.OneDriveBackupFrequency
permissionName = PermissionName.DailyOneDriveBackup permissionName = PermissionName.DailyOneDriveBackup
muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted muteEmailsSettingValue = 'muted'
providerTokenSettingName = SettingName.OneDriveBackupToken providerTokenSettingName = SettingName.NAMES.OneDriveBackupToken
break break
case 'google_drive': case 'google_drive':
settingName = SettingName.GoogleDriveBackupFrequency settingName = SettingName.NAMES.GoogleDriveBackupFrequency
permissionName = PermissionName.DailyGDriveBackup permissionName = PermissionName.DailyGDriveBackup
muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted muteEmailsSettingValue = 'muted'
providerTokenSettingName = SettingName.GoogleDriveBackupToken providerTokenSettingName = SettingName.NAMES.GoogleDriveBackupToken
break break
default: default:
throw new Error(`Not handled backup provider: ${backupProvider}`) throw new Error(`Not handled backup provider: ${backupProvider}`)
+3 -3
View File
@@ -12,7 +12,7 @@ import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface' import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface' import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
import { MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface' import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
import { PermissionName } from '@standardnotes/features' import { PermissionName } from '@standardnotes/features'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface' import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
@@ -28,8 +28,8 @@ const requestBackups = async (
domainEventPublisher: DomainEventPublisherInterface, domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => { ): Promise<void> => {
const permissionName = PermissionName.DailyEmailBackup const permissionName = PermissionName.DailyEmailBackup
const muteEmailsSettingName = SettingName.MuteFailedBackupsEmails const muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
const muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted const muteEmailsSettingValue = 'muted'
if (!backupEmail) { if (!backupEmail) {
throw new Error('Could not trigger email backup for user - missing email parameter') throw new Error('Could not trigger email backup for user - missing email parameter')
@@ -1,5 +1,5 @@
import Redis, { Cluster } from 'ioredis' import Redis, { Cluster } from 'ioredis'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { MigrationInterface, QueryRunner } from 'typeorm' import { MigrationInterface, QueryRunner } from 'typeorm'
import { Setting } from '../src/Domain/Setting/Setting' import { Setting } from '../src/Domain/Setting/Setting'
@@ -34,7 +34,7 @@ export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterfa
const setting = new Setting() const setting = new Setting()
setting.uuid = item['uuid'] setting.uuid = item['uuid']
setting.name = SettingName.MfaSecret setting.name = SettingName.NAMES.MfaSecret
setting.value = item['content'] setting.value = item['content']
if (item['deleted']) { if (item['deleted']) {
setting.value = null setting.value = null
+1 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/auth-server", "name": "@standardnotes/auth-server",
"version": "1.66.5", "version": "1.67.1",
"engines": { "engines": {
"node": ">=18.0.0 <19.0.0" "node": ">=18.0.0 <19.0.0"
}, },
@@ -41,7 +41,6 @@
"@standardnotes/predicates": "workspace:*", "@standardnotes/predicates": "workspace:*",
"@standardnotes/responses": "^1.6.39", "@standardnotes/responses": "^1.6.39",
"@standardnotes/security": "workspace:*", "@standardnotes/security": "workspace:*",
"@standardnotes/settings": "workspace:*",
"@standardnotes/sncrypto-common": "^1.9.0", "@standardnotes/sncrypto-common": "^1.9.0",
"@standardnotes/sncrypto-node": "workspace:*", "@standardnotes/sncrypto-node": "workspace:*",
"@standardnotes/time": "workspace:*", "@standardnotes/time": "workspace:*",
+11
View File
@@ -193,6 +193,7 @@ import { SubscriptionInvitesController } from '../Controller/SubscriptionInvites
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken' import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest' import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest'
import { UserRequestsController } from '../Controller/UserRequestsController' import { UserRequestsController } from '../Controller/UserRequestsController'
import { EmailSubscriptionUnsubscribedEventHandler } from '../Domain/Handler/EmailSubscriptionUnsubscribedEventHandler'
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher') const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -560,6 +561,15 @@ export class ContainerConfigLoader {
) )
} }
container
.bind<EmailSubscriptionUnsubscribedEventHandler>(TYPES.EmailSubscriptionUnsubscribedEventHandler)
.toConstantValue(
new EmailSubscriptionUnsubscribedEventHandler(
container.get(TYPES.UserRepository),
container.get(TYPES.SettingService),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([ const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)], ['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)], ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
@@ -582,6 +592,7 @@ export class ContainerConfigLoader {
], ],
['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.SharedSubscriptionInvitationCreatedEventHandler)], ['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.SharedSubscriptionInvitationCreatedEventHandler)],
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.PredicateVerificationRequestedEventHandler)], ['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.PredicateVerificationRequestedEventHandler)],
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.EmailSubscriptionUnsubscribedEventHandler)],
]) ])
if (env.get('SQS_QUEUE_URL', true)) { if (env.get('SQS_QUEUE_URL', true)) {
+1
View File
@@ -138,6 +138,7 @@ const TYPES = {
UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'), UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'),
SharedSubscriptionInvitationCreatedEventHandler: Symbol.for('SharedSubscriptionInvitationCreatedEventHandler'), SharedSubscriptionInvitationCreatedEventHandler: Symbol.for('SharedSubscriptionInvitationCreatedEventHandler'),
PredicateVerificationRequestedEventHandler: Symbol.for('PredicateVerificationRequestedEventHandler'), PredicateVerificationRequestedEventHandler: Symbol.for('PredicateVerificationRequestedEventHandler'),
EmailSubscriptionUnsubscribedEventHandler: Symbol.for('EmailSubscriptionUnsubscribedEventHandler'),
// Services // Services
DeviceDetector: Symbol.for('DeviceDetector'), DeviceDetector: Symbol.for('DeviceDetector'),
SessionService: Symbol.for('SessionService'), SessionService: Symbol.for('SessionService'),
@@ -1,4 +1,4 @@
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { Request } from 'express' import { Request } from 'express'
import { inject } from 'inversify' import { inject } from 'inversify'
import { import {
@@ -69,7 +69,7 @@ export class AdminController extends BaseHttpController {
const result = await this.doDeleteSetting.execute({ const result = await this.doDeleteSetting.execute({
uuid, uuid,
userUuid, userUuid,
settingName: SettingName.MfaSecret, settingName: SettingName.NAMES.MfaSecret,
timestamp: updatedAt, timestamp: updatedAt,
softDelete: true, softDelete: true,
}) })
@@ -115,7 +115,7 @@ export class AdminController extends BaseHttpController {
const result = await this.doDeleteSetting.execute({ const result = await this.doDeleteSetting.execute({
userUuid, userUuid,
settingName: SettingName.EmailBackupFrequency, settingName: SettingName.NAMES.EmailBackupFrequency,
}) })
if (result.success) { if (result.success) {
@@ -1,4 +1,3 @@
import { SubscriptionSettingName } from '@standardnotes/settings'
import { Request, Response } from 'express' import { Request, Response } from 'express'
import { inject } from 'inversify' import { inject } from 'inversify'
import { import {
@@ -21,7 +20,7 @@ export class SubscriptionSettingsController extends BaseHttpController {
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> { async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.doGetSubscriptionSetting.execute({ const result = await this.doGetSubscriptionSetting.execute({
userUuid: response.locals.user.uuid, userUuid: response.locals.user.uuid,
subscriptionSettingName: request.params.subscriptionSettingName as SubscriptionSettingName, subscriptionSettingName: request.params.subscriptionSettingName,
}) })
if (result.success) { if (result.success) {
@@ -1,6 +1,6 @@
import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security' import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
import { ErrorTag, RoleName } from '@standardnotes/common' import { ErrorTag, RoleName } from '@standardnotes/common'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { Request, Response } from 'express' import { Request, Response } from 'express'
import { inject } from 'inversify' import { inject } from 'inversify'
import { import {
@@ -77,7 +77,7 @@ export class SubscriptionTokensController extends BaseHttpController {
const user = authenticateTokenResponse.user as User const user = authenticateTokenResponse.user as User
let extensionKey = undefined let extensionKey = undefined
const extensionKeySetting = await this.settingService.findSettingWithDecryptedValue({ const extensionKeySetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.ExtensionKey, settingName: SettingName.NAMES.ExtensionKey,
userUuid: user.uuid, userUuid: user.uuid,
}) })
if (extensionKeySetting !== null) { if (extensionKeySetting !== null) {
@@ -20,6 +20,5 @@ export const html = (email: string, device: string, browser: string, timeAndDate
<br /> <br />
SN SN
</p> </p>
<a href="https://app.standardnotes.com/?settings=account">Mute these emails</a>
</div> </div>
` `
@@ -0,0 +1,109 @@
import { EmailLevel } from '@standardnotes/domain-core'
import { EmailSubscriptionUnsubscribedEvent } from '@standardnotes/domain-events'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { EmailSubscriptionUnsubscribedEventHandler } from './EmailSubscriptionUnsubscribedEventHandler'
describe('EmailSubscriptionUnsubscribedEventHandler', () => {
let userRepository: UserRepositoryInterface
let settingsService: SettingServiceInterface
let event: EmailSubscriptionUnsubscribedEvent
const createHandler = () => new EmailSubscriptionUnsubscribedEventHandler(userRepository, settingsService)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByEmail = jest.fn().mockReturnValue({} as jest.Mocked<User>)
settingsService = {} as jest.Mocked<SettingServiceInterface>
settingsService.createOrReplace = jest.fn()
event = {
payload: {
userEmail: 'test@test.te',
level: EmailLevel.LEVELS.Marketing,
},
} as jest.Mocked<EmailSubscriptionUnsubscribedEvent>
})
it('should not do anything if user is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
expect(settingsService.createOrReplace).not.toHaveBeenCalled()
})
it('should update user marketing email settings', async () => {
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_MARKETING_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should update user sign in email settings', async () => {
event.payload.level = EmailLevel.LEVELS.SignIn
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_SIGN_IN_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should update user email backup email settings', async () => {
event.payload.level = EmailLevel.LEVELS.FailedEmailBackup
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_FAILED_BACKUPS_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should update user email backup email settings', async () => {
event.payload.level = EmailLevel.LEVELS.FailedCloudBackup
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_FAILED_CLOUD_BACKUPS_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should throw error for unrecognized level', async () => {
event.payload.level = 'foobar'
let caughtError = null
try {
await createHandler().handle(event)
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
})
@@ -0,0 +1,41 @@
import { EmailLevel } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, EmailSubscriptionUnsubscribedEvent } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/settings'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
export class EmailSubscriptionUnsubscribedEventHandler implements DomainEventHandlerInterface {
constructor(private userRepository: UserRepositoryInterface, private settingsService: SettingServiceInterface) {}
async handle(event: EmailSubscriptionUnsubscribedEvent): Promise<void> {
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
if (user === null) {
return
}
await this.settingsService.createOrReplace({
user,
props: {
name: this.getSettingNameFromLevel(event.payload.level),
unencryptedValue: 'muted',
sensitive: false,
},
})
}
private getSettingNameFromLevel(level: string): string {
switch (level) {
case EmailLevel.LEVELS.FailedCloudBackup:
return SettingName.MuteFailedCloudBackupsEmails
case EmailLevel.LEVELS.FailedEmailBackup:
return SettingName.MuteFailedBackupsEmails
case EmailLevel.LEVELS.Marketing:
return SettingName.MuteMarketingEmails
case EmailLevel.LEVELS.SignIn:
return SettingName.MuteSignInEmails
default:
throw new Error(`Unknown level: ${level}`)
}
}
}
@@ -1,5 +1,5 @@
import { DomainEventHandlerInterface, ExtensionKeyGrantedEvent } from '@standardnotes/domain-events' import { DomainEventHandlerInterface, ExtensionKeyGrantedEvent } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { OfflineFeaturesTokenData } from '@standardnotes/security' import { OfflineFeaturesTokenData } from '@standardnotes/security'
import { ContentDecoderInterface } from '@standardnotes/common' import { ContentDecoderInterface } from '@standardnotes/common'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
@@ -54,7 +54,7 @@ export class ExtensionKeyGrantedEventHandler implements DomainEventHandlerInterf
await this.settingService.createOrReplace({ await this.settingService.createOrReplace({
user, user,
props: { props: {
name: SettingName.ExtensionKey, name: SettingName.NAMES.ExtensionKey,
unencryptedValue: event.payload.extensionKey, unencryptedValue: event.payload.extensionKey,
serverEncryptionVersion: EncryptionVersion.Default, serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true, sensitive: true,
@@ -1,5 +1,5 @@
import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events' import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events'
import { SubscriptionSettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Logger } from 'winston' import { Logger } from 'winston'
@@ -38,7 +38,7 @@ export class FileRemovedEventHandler implements DomainEventHandlerInterface {
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: user.uuid, userUuid: user.uuid,
userSubscriptionUuid: subscription.uuid, userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed, subscriptionSettingName: SettingName.NAMES.FileUploadBytesUsed,
}) })
if (bytesUsedSetting === null) { if (bytesUsedSetting === null) {
this.logger.warn(`Could not find bytes used setting for user with uuid: ${user.uuid}`) this.logger.warn(`Could not find bytes used setting for user with uuid: ${user.uuid}`)
@@ -51,7 +51,7 @@ export class FileRemovedEventHandler implements DomainEventHandlerInterface {
await this.subscriptionSettingService.createOrReplace({ await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription, userSubscription: subscription,
props: { props: {
name: SubscriptionSettingName.FileUploadBytesUsed, name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesUsed - byteSize).toString(), unencryptedValue: (+bytesUsed - byteSize).toString(),
sensitive: false, sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
@@ -1,5 +1,5 @@
import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events' import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events'
import { SubscriptionSettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Logger } from 'winston' import { Logger } from 'winston'
@@ -47,7 +47,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface {
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid, userUuid: (await subscription.user).uuid,
userSubscriptionUuid: subscription.uuid, userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed, subscriptionSettingName: SettingName.NAMES.FileUploadBytesUsed,
}) })
if (bytesUsedSetting !== null) { if (bytesUsedSetting !== null) {
bytesUsed = bytesUsedSetting.value as string bytesUsed = bytesUsedSetting.value as string
@@ -56,7 +56,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface {
await this.subscriptionSettingService.createOrReplace({ await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription, userSubscription: subscription,
props: { props: {
name: SubscriptionSettingName.FileUploadBytesUsed, name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesUsed + byteSize).toString(), unencryptedValue: (+bytesUsed + byteSize).toString(),
sensitive: false, sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
@@ -1,5 +1,5 @@
import { DomainEventHandlerInterface, ListedAccountCreatedEvent } from '@standardnotes/domain-events' import { DomainEventHandlerInterface, ListedAccountCreatedEvent } from '@standardnotes/domain-events'
import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Logger } from 'winston' import { Logger } from 'winston'
@@ -25,14 +25,14 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter
const newSecret = { authorId: event.payload.userId, secret: event.payload.secret, hostUrl: event.payload.hostUrl } const newSecret = { authorId: event.payload.userId, secret: event.payload.secret, hostUrl: event.payload.hostUrl }
let authSecrets: ListedAuthorSecretsData = [newSecret] let authSecrets = [newSecret]
const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({ const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.ListedAuthorSecrets, settingName: SettingName.NAMES.ListedAuthorSecrets,
userUuid: user.uuid, userUuid: user.uuid,
}) })
if (listedAuthorSecretsSetting !== null) { if (listedAuthorSecretsSetting !== null) {
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.value as string) const existingSecrets = JSON.parse(listedAuthorSecretsSetting.value as string)
existingSecrets.push(newSecret) existingSecrets.push(newSecret)
authSecrets = existingSecrets authSecrets = existingSecrets
} }
@@ -40,7 +40,7 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter
await this.settingService.createOrReplace({ await this.settingService.createOrReplace({
user, user,
props: { props: {
name: SettingName.ListedAuthorSecrets, name: SettingName.NAMES.ListedAuthorSecrets,
unencryptedValue: JSON.stringify(authSecrets), unencryptedValue: JSON.stringify(authSecrets),
sensitive: false, sensitive: false,
}, },
@@ -1,5 +1,5 @@
import { DomainEventHandlerInterface, ListedAccountDeletedEvent } from '@standardnotes/domain-events' import { DomainEventHandlerInterface, ListedAccountDeletedEvent } from '@standardnotes/domain-events'
import { ListedAuthorSecretsData, SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Logger } from 'winston' import { Logger } from 'winston'
@@ -24,7 +24,7 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter
} }
const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({ const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.ListedAuthorSecrets, settingName: SettingName.NAMES.ListedAuthorSecrets,
userUuid: user.uuid, userUuid: user.uuid,
}) })
if (listedAuthorSecretsSetting === null) { if (listedAuthorSecretsSetting === null) {
@@ -33,9 +33,9 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter
return return
} }
const existingSecrets: ListedAuthorSecretsData = JSON.parse(listedAuthorSecretsSetting.value as string) const existingSecrets = JSON.parse(listedAuthorSecretsSetting.value as string)
const filteredSecrets = existingSecrets.filter( const filteredSecrets = existingSecrets.filter(
(secret) => (secret: Record<string, unknown>) =>
secret.authorId !== event.payload.userId || secret.authorId !== event.payload.userId ||
(secret.authorId === event.payload.userId && secret.hostUrl !== event.payload.hostUrl), (secret.authorId === event.payload.userId && secret.hostUrl !== event.payload.hostUrl),
) )
@@ -43,7 +43,7 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter
await this.settingService.createOrReplace({ await this.settingService.createOrReplace({
user, user,
props: { props: {
name: SettingName.ListedAuthorSecrets, name: SettingName.NAMES.ListedAuthorSecrets,
unencryptedValue: JSON.stringify(filteredSecrets), unencryptedValue: JSON.stringify(filteredSecrets),
sensitive: false, sensitive: false,
}, },
@@ -10,7 +10,7 @@ import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { UserSubscription } from '../Subscription/UserSubscription' import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
@@ -48,7 +48,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
await this.settingService.createOrReplace({ await this.settingService.createOrReplace({
user, user,
props: { props: {
name: SettingName.ExtensionKey, name: SettingName.NAMES.ExtensionKey,
unencryptedValue: event.payload.extensionKey, unencryptedValue: event.payload.extensionKey,
serverEncryptionVersion: EncryptionVersion.Default, serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true, sensitive: true,
@@ -15,7 +15,7 @@ import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface' import { OfflineSettingServiceInterface } from '../Setting/OfflineSettingServiceInterface'
import { ContentDecoderInterface } from '@standardnotes/common' import { ContentDecoderInterface } from '@standardnotes/common'
import { OfflineSettingName } from '../Setting/OfflineSettingName' import { OfflineSettingName } from '../Setting/OfflineSettingName'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface' import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
@@ -95,7 +95,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
await this.settingService.createOrReplace({ await this.settingService.createOrReplace({
user, user,
props: { props: {
name: SettingName.ExtensionKey, name: SettingName.NAMES.ExtensionKey,
unencryptedValue: event.payload.extensionKey, unencryptedValue: event.payload.extensionKey,
serverEncryptionVersion: EncryptionVersion.Default, serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true, sensitive: true,
@@ -12,7 +12,6 @@ import { EphemeralSession } from './EphemeralSession'
import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface' import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface'
import { RevokedSession } from './RevokedSession' import { RevokedSession } from './RevokedSession'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { LogSessionUserAgentOption } from '@standardnotes/settings'
import { Setting } from '../Setting/Setting' import { Setting } from '../Setting/Setting'
import { CryptoNode } from '@standardnotes/sncrypto-node' import { CryptoNode } from '@standardnotes/sncrypto-node'
@@ -171,7 +170,7 @@ describe('SessionService', () => {
user.uuid = '123' user.uuid = '123'
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({ settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: LogSessionUserAgentOption.Disabled, value: 'disabled',
} as jest.Mocked<Setting>) } as jest.Mocked<Setting>)
const sessionPayload = await createService().createNewSessionForUser({ const sessionPayload = await createService().createNewSessionForUser({
@@ -16,7 +16,7 @@ import { EphemeralSession } from './EphemeralSession'
import { RevokedSession } from './RevokedSession' import { RevokedSession } from './RevokedSession'
import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface' import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { LogSessionUserAgentOption, SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { SessionBody } from '@standardnotes/responses' import { SessionBody } from '@standardnotes/responses'
import { Uuid } from '@standardnotes/common' import { Uuid } from '@standardnotes/common'
import { CryptoNode } from '@standardnotes/sncrypto-node' import { CryptoNode } from '@standardnotes/sncrypto-node'
@@ -291,7 +291,7 @@ export class SessionService implements SessionServiceInterface {
private async isLoggingUserAgentEnabledOnSessions(user: User): Promise<boolean> { private async isLoggingUserAgentEnabledOnSessions(user: User): Promise<boolean> {
const loggingSetting = await this.settingService.findSettingWithDecryptedValue({ const loggingSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.LogSessionUserAgent, settingName: SettingName.NAMES.LogSessionUserAgent,
userUuid: user.uuid, userUuid: user.uuid,
}) })
@@ -299,6 +299,6 @@ export class SessionService implements SessionServiceInterface {
return true return true
} }
return loggingSetting.value === LogSessionUserAgentOption.Enabled return loggingSetting.value === 'enabled'
} }
} }
@@ -1,8 +1,7 @@
import { Uuid } from '@standardnotes/common' import { Uuid } from '@standardnotes/common'
import { SettingName } from '@standardnotes/settings'
export type FindSettingDTO = { export type FindSettingDTO = {
userUuid: string userUuid: string
settingName: SettingName settingName: string
settingUuid?: Uuid settingUuid?: Uuid
} }
@@ -1,9 +1,8 @@
import { Uuid } from '@standardnotes/common' import { Uuid } from '@standardnotes/common'
import { SubscriptionSettingName } from '@standardnotes/settings'
export type FindSubscriptionSettingDTO = { export type FindSubscriptionSettingDTO = {
userUuid: Uuid userUuid: Uuid
userSubscriptionUuid: Uuid userSubscriptionUuid: Uuid
subscriptionSettingName: SubscriptionSettingName subscriptionSettingName: string
settingUuid?: Uuid settingUuid?: Uuid
} }
@@ -5,13 +5,8 @@ import {
MuteEmailsSettingChangedEvent, MuteEmailsSettingChangedEvent,
UserDisabledSessionUserAgentLoggingEvent, UserDisabledSessionUserAgentLoggingEvent,
} from '@standardnotes/domain-events' } from '@standardnotes/domain-events'
import { import { MuteMarketingEmailsOption } from '@standardnotes/settings'
EmailBackupFrequency, import { SettingName } from '@standardnotes/domain-core'
LogSessionUserAgentOption,
MuteMarketingEmailsOption,
OneDriveBackupFrequency,
SettingName,
} from '@standardnotes/settings'
import 'reflect-metadata' import 'reflect-metadata'
import { Logger } from 'winston' import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
@@ -71,11 +66,11 @@ describe('SettingInterpreter', () => {
it('should trigger session cleanup if user is disabling session user agent logging', async () => { it('should trigger session cleanup if user is disabling session user agent logging', async () => {
const setting = { const setting = {
name: SettingName.LogSessionUserAgent, name: SettingName.NAMES.LogSessionUserAgent,
value: LogSessionUserAgentOption.Disabled, value: 'disabled',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
await createInterpreter().interpretSettingUpdated(setting, user, LogSessionUserAgentOption.Disabled) await createInterpreter().interpretSettingUpdated(setting, user, 'disabled')
expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent).toHaveBeenCalledWith({ expect(domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent).toHaveBeenCalledWith({
@@ -86,11 +81,11 @@ describe('SettingInterpreter', () => {
it('should trigger backup if email backup setting is created - emails not muted', async () => { it('should trigger backup if email backup setting is created - emails not muted', async () => {
const setting = { const setting = {
name: SettingName.EmailBackupFrequency, name: SettingName.NAMES.EmailBackupFrequency,
value: EmailBackupFrequency.Daily, value: 'daily',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
await createInterpreter().interpretSettingUpdated(setting, user, EmailBackupFrequency.Daily) await createInterpreter().interpretSettingUpdated(setting, user, 'daily')
expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '', false) expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '', false)
@@ -98,16 +93,16 @@ describe('SettingInterpreter', () => {
it('should trigger backup if email backup setting is created - emails muted', async () => { it('should trigger backup if email backup setting is created - emails muted', async () => {
const setting = { const setting = {
name: SettingName.EmailBackupFrequency, name: SettingName.NAMES.EmailBackupFrequency,
value: EmailBackupFrequency.Daily, value: 'daily',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({ settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({
name: SettingName.MuteFailedBackupsEmails, name: SettingName.NAMES.MuteFailedBackupsEmails,
uuid: '6-7-8', uuid: '6-7-8',
value: 'muted', value: 'muted',
} as jest.Mocked<Setting>) } as jest.Mocked<Setting>)
await createInterpreter().interpretSettingUpdated(setting, user, EmailBackupFrequency.Daily) await createInterpreter().interpretSettingUpdated(setting, user, 'daily')
expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '6-7-8', true) expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '6-7-8', true)
@@ -115,12 +110,12 @@ describe('SettingInterpreter', () => {
it('should not trigger backup if email backup setting is disabled', async () => { it('should not trigger backup if email backup setting is disabled', async () => {
const setting = { const setting = {
name: SettingName.EmailBackupFrequency, name: SettingName.NAMES.EmailBackupFrequency,
value: EmailBackupFrequency.Disabled, value: 'disabled',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(setting, user, EmailBackupFrequency.Disabled) await createInterpreter().interpretSettingUpdated(setting, user, 'disabled')
expect(domainEventPublisher.publish).not.toHaveBeenCalled() expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).not.toHaveBeenCalled() expect(domainEventFactory.createEmailBackupRequestedEvent).not.toHaveBeenCalled()
@@ -128,7 +123,7 @@ describe('SettingInterpreter', () => {
it('should trigger cloud backup if dropbox backup setting is created', async () => { it('should trigger cloud backup if dropbox backup setting is created', async () => {
const setting = { const setting = {
name: SettingName.DropboxBackupToken, name: SettingName.NAMES.DropboxBackupToken,
value: 'test-token', value: 'test-token',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
@@ -147,11 +142,11 @@ describe('SettingInterpreter', () => {
it('should trigger cloud backup if dropbox backup setting is created - muted emails', async () => { it('should trigger cloud backup if dropbox backup setting is created - muted emails', async () => {
const setting = { const setting = {
name: SettingName.DropboxBackupToken, name: SettingName.NAMES.DropboxBackupToken,
value: 'test-token', value: 'test-token',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({ settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({
name: SettingName.MuteFailedCloudBackupsEmails, name: SettingName.NAMES.MuteFailedCloudBackupsEmails,
uuid: '6-7-8', uuid: '6-7-8',
value: 'muted', value: 'muted',
} as jest.Mocked<Setting>) } as jest.Mocked<Setting>)
@@ -170,7 +165,7 @@ describe('SettingInterpreter', () => {
it('should trigger cloud backup if google drive backup setting is created', async () => { it('should trigger cloud backup if google drive backup setting is created', async () => {
const setting = { const setting = {
name: SettingName.GoogleDriveBackupToken, name: SettingName.NAMES.GoogleDriveBackupToken,
value: 'test-token', value: 'test-token',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
@@ -189,7 +184,7 @@ describe('SettingInterpreter', () => {
it('should trigger cloud backup if one drive backup setting is created', async () => { it('should trigger cloud backup if one drive backup setting is created', async () => {
const setting = { const setting = {
name: SettingName.OneDriveBackupToken, name: SettingName.NAMES.OneDriveBackupToken,
value: 'test-token', value: 'test-token',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null) settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
@@ -225,13 +220,13 @@ describe('SettingInterpreter', () => {
it('should trigger cloud backup if backup frequency setting is updated and a backup token setting is present', async () => { it('should trigger cloud backup if backup frequency setting is updated and a backup token setting is present', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({ settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({
name: SettingName.OneDriveBackupToken, name: SettingName.NAMES.OneDriveBackupToken,
serverEncryptionVersion: 1, serverEncryptionVersion: 1,
value: 'encrypted-backup-token', value: 'encrypted-backup-token',
sensitive: true, sensitive: true,
} as jest.Mocked<Setting>) } as jest.Mocked<Setting>)
const setting = { const setting = {
name: SettingName.OneDriveBackupFrequency, name: SettingName.NAMES.OneDriveBackupFrequency,
serverEncryptionVersion: 0, serverEncryptionVersion: 0,
value: 'daily', value: 'daily',
sensitive: false, sensitive: false,
@@ -251,19 +246,19 @@ describe('SettingInterpreter', () => {
it('should not trigger cloud backup if backup frequency setting is updated as disabled', async () => { it('should not trigger cloud backup if backup frequency setting is updated as disabled', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({ settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({
name: SettingName.OneDriveBackupToken, name: SettingName.NAMES.OneDriveBackupToken,
serverEncryptionVersion: 1, serverEncryptionVersion: 1,
value: 'encrypted-backup-token', value: 'encrypted-backup-token',
sensitive: true, sensitive: true,
} as jest.Mocked<Setting>) } as jest.Mocked<Setting>)
const setting = { const setting = {
name: SettingName.OneDriveBackupFrequency, name: SettingName.NAMES.OneDriveBackupFrequency,
serverEncryptionVersion: 0, serverEncryptionVersion: 0,
value: OneDriveBackupFrequency.Disabled, value: 'disabled',
sensitive: false, sensitive: false,
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
await createInterpreter().interpretSettingUpdated(setting, user, OneDriveBackupFrequency.Disabled) await createInterpreter().interpretSettingUpdated(setting, user, 'disabled')
expect(domainEventPublisher.publish).not.toHaveBeenCalled() expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).not.toHaveBeenCalled() expect(domainEventFactory.createCloudBackupRequestedEvent).not.toHaveBeenCalled()
@@ -272,7 +267,7 @@ describe('SettingInterpreter', () => {
it('should not trigger cloud backup if backup frequency setting is updated and a backup token setting is not present', async () => { it('should not trigger cloud backup if backup frequency setting is updated and a backup token setting is not present', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce(null) settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce(null)
const setting = { const setting = {
name: SettingName.OneDriveBackupFrequency, name: SettingName.NAMES.OneDriveBackupFrequency,
serverEncryptionVersion: 0, serverEncryptionVersion: 0,
value: 'daily', value: 'daily',
sensitive: false, sensitive: false,
@@ -1,15 +1,5 @@
import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core' import { EmailLevel, SettingName } from '@standardnotes/domain-core'
import {
DropboxBackupFrequency,
EmailBackupFrequency,
GoogleDriveBackupFrequency,
LogSessionUserAgentOption,
MuteFailedBackupsEmailsOption,
MuteFailedCloudBackupsEmailsOption,
OneDriveBackupFrequency,
SettingName,
} from '@standardnotes/settings'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Logger } from 'winston' import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types' import TYPES from '../../Bootstrap/Types'
@@ -23,22 +13,18 @@ import { SettingRepositoryInterface } from './SettingRepositoryInterface'
@injectable() @injectable()
export class SettingInterpreter implements SettingInterpreterInterface { export class SettingInterpreter implements SettingInterpreterInterface {
private readonly cloudBackupTokenSettings = [ private readonly cloudBackupTokenSettings = [
SettingName.DropboxBackupToken, SettingName.NAMES.DropboxBackupToken,
SettingName.GoogleDriveBackupToken, SettingName.NAMES.GoogleDriveBackupToken,
SettingName.OneDriveBackupToken, SettingName.NAMES.OneDriveBackupToken,
] ]
private readonly cloudBackupFrequencySettings = [ private readonly cloudBackupFrequencySettings = [
SettingName.DropboxBackupFrequency, SettingName.NAMES.DropboxBackupFrequency,
SettingName.GoogleDriveBackupFrequency, SettingName.NAMES.GoogleDriveBackupFrequency,
SettingName.OneDriveBackupFrequency, SettingName.NAMES.OneDriveBackupFrequency,
] ]
private readonly cloudBackupFrequencyDisabledValues = [ private readonly cloudBackupFrequencyDisabledValues = ['disabled']
DropboxBackupFrequency.Disabled,
GoogleDriveBackupFrequency.Disabled,
OneDriveBackupFrequency.Disabled,
]
private readonly emailSettingToSubscriptionRejectionLevelMap: Map<SettingName, string> = new Map([ private readonly emailSettingToSubscriptionRejectionLevelMap: Map<SettingName, string> = new Map([
[SettingName.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup], [SettingName.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
@@ -77,11 +63,11 @@ export class SettingInterpreter implements SettingInterpreterInterface {
let userHasEmailsMuted = false let userHasEmailsMuted = false
let muteEmailsSettingUuid = '' let muteEmailsSettingUuid = ''
const muteFailedEmailsBackupSetting = await this.settingRepository.findOneByNameAndUserUuid( const muteFailedEmailsBackupSetting = await this.settingRepository.findOneByNameAndUserUuid(
SettingName.MuteFailedBackupsEmails, SettingName.NAMES.MuteFailedBackupsEmails,
userUuid, userUuid,
) )
if (muteFailedEmailsBackupSetting !== null) { if (muteFailedEmailsBackupSetting !== null) {
userHasEmailsMuted = muteFailedEmailsBackupSetting.value === MuteFailedBackupsEmailsOption.Muted userHasEmailsMuted = muteFailedEmailsBackupSetting.value === 'muted'
muteEmailsSettingUuid = muteFailedEmailsBackupSetting.uuid muteEmailsSettingUuid = muteFailedEmailsBackupSetting.uuid
} }
@@ -100,21 +86,19 @@ export class SettingInterpreter implements SettingInterpreterInterface {
} }
private isEnablingEmailBackupSetting(setting: Setting): boolean { private isEnablingEmailBackupSetting(setting: Setting): boolean {
return setting.name === SettingName.EmailBackupFrequency && setting.value !== EmailBackupFrequency.Disabled return setting.name === SettingName.NAMES.EmailBackupFrequency && setting.value !== 'disabled'
} }
private isEnablingCloudBackupSetting(setting: Setting): boolean { private isEnablingCloudBackupSetting(setting: Setting): boolean {
return ( return (
(this.cloudBackupFrequencySettings.includes(setting.name as SettingName) || (this.cloudBackupFrequencySettings.includes(setting.name) ||
this.cloudBackupTokenSettings.includes(setting.name as SettingName)) && this.cloudBackupTokenSettings.includes(setting.name)) &&
!this.cloudBackupFrequencyDisabledValues.includes( !this.cloudBackupFrequencyDisabledValues.includes(setting.value as string)
setting.value as DropboxBackupFrequency | OneDriveBackupFrequency | GoogleDriveBackupFrequency,
)
) )
} }
private isDisablingSessionUserAgentLogging(setting: Setting): boolean { private isDisablingSessionUserAgentLogging(setting: Setting): boolean {
return SettingName.LogSessionUserAgent === setting.name && LogSessionUserAgentOption.Disabled === setting.value return SettingName.NAMES.LogSessionUserAgent === setting.name && 'disabled' === setting.value
} }
private async triggerEmailSubscriptionChange( private async triggerEmailSubscriptionChange(
@@ -144,29 +128,26 @@ export class SettingInterpreter implements SettingInterpreterInterface {
let cloudProvider let cloudProvider
let tokenSettingName let tokenSettingName
switch (setting.name) { switch (setting.name) {
case SettingName.DropboxBackupToken: case SettingName.NAMES.DropboxBackupToken:
case SettingName.DropboxBackupFrequency: case SettingName.NAMES.DropboxBackupFrequency:
cloudProvider = 'DROPBOX' cloudProvider = 'DROPBOX'
tokenSettingName = SettingName.DropboxBackupToken tokenSettingName = SettingName.NAMES.DropboxBackupToken
break break
case SettingName.GoogleDriveBackupToken: case SettingName.NAMES.GoogleDriveBackupToken:
case SettingName.GoogleDriveBackupFrequency: case SettingName.NAMES.GoogleDriveBackupFrequency:
cloudProvider = 'GOOGLE_DRIVE' cloudProvider = 'GOOGLE_DRIVE'
tokenSettingName = SettingName.GoogleDriveBackupToken tokenSettingName = SettingName.NAMES.GoogleDriveBackupToken
break break
case SettingName.OneDriveBackupToken: case SettingName.NAMES.OneDriveBackupToken:
case SettingName.OneDriveBackupFrequency: case SettingName.NAMES.OneDriveBackupFrequency:
cloudProvider = 'ONE_DRIVE' cloudProvider = 'ONE_DRIVE'
tokenSettingName = SettingName.OneDriveBackupToken tokenSettingName = SettingName.NAMES.OneDriveBackupToken
break break
} }
let backupToken = null let backupToken = null
if (this.cloudBackupFrequencySettings.includes(setting.name as SettingName)) { if (this.cloudBackupFrequencySettings.includes(setting.name)) {
const tokenSetting = await this.settingRepository.findLastByNameAndUserUuid( const tokenSetting = await this.settingRepository.findLastByNameAndUserUuid(tokenSettingName as string, userUuid)
tokenSettingName as SettingName,
userUuid,
)
if (tokenSetting !== null) { if (tokenSetting !== null) {
backupToken = await this.settingDecrypter.decryptSettingValue(tokenSetting, userUuid) backupToken = await this.settingDecrypter.decryptSettingValue(tokenSetting, userUuid)
} }
@@ -183,11 +164,11 @@ export class SettingInterpreter implements SettingInterpreterInterface {
let userHasEmailsMuted = false let userHasEmailsMuted = false
let muteEmailsSettingUuid = '' let muteEmailsSettingUuid = ''
const muteFailedCloudBackupSetting = await this.settingRepository.findOneByNameAndUserUuid( const muteFailedCloudBackupSetting = await this.settingRepository.findOneByNameAndUserUuid(
SettingName.MuteFailedCloudBackupsEmails, SettingName.NAMES.MuteFailedCloudBackupsEmails,
userUuid, userUuid,
) )
if (muteFailedCloudBackupSetting !== null) { if (muteFailedCloudBackupSetting !== null) {
userHasEmailsMuted = muteFailedCloudBackupSetting.value === MuteFailedCloudBackupsEmailsOption.Muted userHasEmailsMuted = muteFailedCloudBackupSetting.value === 'muted'
muteEmailsSettingUuid = muteFailedCloudBackupSetting.uuid muteEmailsSettingUuid = muteFailedCloudBackupSetting.uuid
} }
@@ -1,16 +1,15 @@
import { ReadStream } from 'fs' import { ReadStream } from 'fs'
import { SettingName } from '@standardnotes/settings'
import { DeleteSettingDto } from '../UseCase/DeleteSetting/DeleteSettingDto' import { DeleteSettingDto } from '../UseCase/DeleteSetting/DeleteSettingDto'
import { Setting } from './Setting' import { Setting } from './Setting'
export interface SettingRepositoryInterface { export interface SettingRepositoryInterface {
findOneByUuid(uuid: string): Promise<Setting | null> findOneByUuid(uuid: string): Promise<Setting | null>
findOneByUuidAndNames(uuid: string, names: SettingName[]): Promise<Setting | null> findOneByUuidAndNames(uuid: string, names: string[]): Promise<Setting | null>
findOneByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null> findOneByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null>
findLastByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null> findLastByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null>
findAllByUserUuid(userUuid: string): Promise<Setting[]> findAllByUserUuid(userUuid: string): Promise<Setting[]>
streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream> streamAllByNameAndValue(name: string, value: string): Promise<ReadStream>
deleteByUserUuid(dto: DeleteSettingDto): Promise<void> deleteByUserUuid(dto: DeleteSettingDto): Promise<void>
save(setting: Setting): Promise<Setting> save(setting: Setting): Promise<Setting>
} }
@@ -1,6 +1,6 @@
import 'reflect-metadata' import 'reflect-metadata'
import { LogSessionUserAgentOption, MuteSignInEmailsOption, SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { Logger } from 'winston' import { Logger } from 'winston'
import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { User } from '../User/User' import { User } from '../User/User'
@@ -54,9 +54,9 @@ describe('SettingService', () => {
settingsAssociationService.getDefaultSettingsAndValuesForNewUser = jest.fn().mockReturnValue( settingsAssociationService.getDefaultSettingsAndValuesForNewUser = jest.fn().mockReturnValue(
new Map([ new Map([
[ [
SettingName.MuteSignInEmails, SettingName.NAMES.MuteSignInEmails,
{ {
value: MuteSignInEmailsOption.NotMuted, value: 'not_muted',
sensitive: 0, sensitive: 0,
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
}, },
@@ -67,11 +67,11 @@ describe('SettingService', () => {
settingsAssociationService.getDefaultSettingsAndValuesForNewVaultAccount = jest.fn().mockReturnValue( settingsAssociationService.getDefaultSettingsAndValuesForNewVaultAccount = jest.fn().mockReturnValue(
new Map([ new Map([
[ [
SettingName.LogSessionUserAgent, SettingName.NAMES.LogSessionUserAgent,
{ {
sensitive: false, sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: LogSessionUserAgentOption.Disabled, value: 'disabled',
}, },
], ],
]), ]),
@@ -173,9 +173,7 @@ describe('SettingService', () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting) settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting)
expect( expect(await createService().findSettingWithDecryptedValue({ userUuid: '1-2-3', settingName: 'test' })).toEqual({
await createService().findSettingWithDecryptedValue({ userUuid: '1-2-3', settingName: 'test' as SettingName }),
).toEqual({
serverEncryptionVersion: 1, serverEncryptionVersion: 1,
value: 'decrypted', value: 'decrypted',
}) })
@@ -1,4 +1,3 @@
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Logger } from 'winston' import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types' import TYPES from '../../Bootstrap/Types'
@@ -74,7 +73,7 @@ export class SettingService implements SettingServiceInterface {
const existing = await this.findSettingWithDecryptedValue({ const existing = await this.findSettingWithDecryptedValue({
userUuid: user.uuid, userUuid: user.uuid,
settingName: props.name as SettingName, settingName: props.name,
settingUuid: props.uuid, settingUuid: props.uuid,
}) })
@@ -1,6 +1,6 @@
import 'reflect-metadata' import 'reflect-metadata'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { PermissionName } from '@standardnotes/features' import { PermissionName } from '@standardnotes/features'
import { SettingsAssociationService } from './SettingsAssociationService' import { SettingsAssociationService } from './SettingsAssociationService'
@@ -11,52 +11,54 @@ describe('SettingsAssociationService', () => {
const createService = () => new SettingsAssociationService() const createService = () => new SettingsAssociationService()
it('should tell if a setting is mutable by the client', () => { it('should tell if a setting is mutable by the client', () => {
expect(createService().isSettingMutableByClient(SettingName.DropboxBackupFrequency)).toBeTruthy() expect(createService().isSettingMutableByClient(SettingName.NAMES.DropboxBackupFrequency)).toBeTruthy()
}) })
it('should tell if a setting is immutable by the client', () => { it('should tell if a setting is immutable by the client', () => {
expect(createService().isSettingMutableByClient(SettingName.ListedAuthorSecrets)).toBeFalsy() expect(createService().isSettingMutableByClient(SettingName.NAMES.ListedAuthorSecrets)).toBeFalsy()
}) })
it('should return default encryption version for a setting which enecryption version is not strictly defined', () => { it('should return default encryption version for a setting which enecryption version is not strictly defined', () => {
expect(createService().getEncryptionVersionForSetting(SettingName.MfaSecret)).toEqual(EncryptionVersion.Default) expect(createService().getEncryptionVersionForSetting(SettingName.NAMES.MfaSecret)).toEqual(
EncryptionVersion.Default,
)
}) })
it('should return a defined encryption version for a setting which enecryption version is strictly defined', () => { it('should return a defined encryption version for a setting which enecryption version is strictly defined', () => {
expect(createService().getEncryptionVersionForSetting(SettingName.EmailBackupFrequency)).toEqual( expect(createService().getEncryptionVersionForSetting(SettingName.NAMES.EmailBackupFrequency)).toEqual(
EncryptionVersion.Unencrypted, EncryptionVersion.Unencrypted,
) )
}) })
it('should return default sensitivity for a setting which sensitivity is not strictly defined', () => { it('should return default sensitivity for a setting which sensitivity is not strictly defined', () => {
expect(createService().getSensitivityForSetting(SettingName.DropboxBackupToken)).toBeTruthy() expect(createService().getSensitivityForSetting(SettingName.NAMES.DropboxBackupToken)).toBeTruthy()
}) })
it('should return a defined sensitivity for a setting which sensitivity is strictly defined', () => { it('should return a defined sensitivity for a setting which sensitivity is strictly defined', () => {
expect(createService().getSensitivityForSetting(SettingName.DropboxBackupFrequency)).toBeFalsy() expect(createService().getSensitivityForSetting(SettingName.NAMES.DropboxBackupFrequency)).toBeFalsy()
}) })
it('should return the default set of settings for a newly registered user', () => { it('should return the default set of settings for a newly registered user', () => {
const settings = createService().getDefaultSettingsAndValuesForNewUser() const settings = createService().getDefaultSettingsAndValuesForNewUser()
const flatSettings = [...(settings as Map<SettingName, SettingDescription>).keys()] const flatSettings = [...(settings as Map<string, SettingDescription>).keys()]
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT']) expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
}) })
it('should return the default set of settings for a newly registered vault account', () => { it('should return the default set of settings for a newly registered vault account', () => {
const settings = createService().getDefaultSettingsAndValuesForNewVaultAccount() const settings = createService().getDefaultSettingsAndValuesForNewVaultAccount()
const flatSettings = [...(settings as Map<SettingName, SettingDescription>).keys()] const flatSettings = [...(settings as Map<string, SettingDescription>).keys()]
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT']) expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
expect(settings.get(SettingName.LogSessionUserAgent)?.value).toEqual('disabled') expect(settings.get(SettingName.NAMES.LogSessionUserAgent)?.value).toEqual('disabled')
}) })
it('should return a permission name associated to a given setting', () => { it('should return a permission name associated to a given setting', () => {
expect(createService().getPermissionAssociatedWithSetting(SettingName.EmailBackupFrequency)).toEqual( expect(createService().getPermissionAssociatedWithSetting(SettingName.NAMES.EmailBackupFrequency)).toEqual(
PermissionName.DailyEmailBackup, PermissionName.DailyEmailBackup,
) )
}) })
it('should not return a permission name if not associated to a given setting', () => { it('should not return a permission name if not associated to a given setting', () => {
expect(createService().getPermissionAssociatedWithSetting(SettingName.ExtensionKey)).toBeUndefined() expect(createService().getPermissionAssociatedWithSetting(SettingName.NAMES.ExtensionKey)).toBeUndefined()
}) })
}) })
@@ -1,10 +1,5 @@
import { PermissionName } from '@standardnotes/features' import { PermissionName } from '@standardnotes/features'
import { import { SettingName } from '@standardnotes/domain-core'
LogSessionUserAgentOption,
MuteMarketingEmailsOption,
MuteSignInEmailsOption,
SettingName,
} from '@standardnotes/settings'
import { injectable } from 'inversify' import { injectable } from 'inversify'
import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { EncryptionVersion } from '../Encryption/EncryptionVersion'
@@ -15,79 +10,79 @@ import { SettingsAssociationServiceInterface } from './SettingsAssociationServic
@injectable() @injectable()
export class SettingsAssociationService implements SettingsAssociationServiceInterface { export class SettingsAssociationService implements SettingsAssociationServiceInterface {
private readonly UNENCRYPTED_SETTINGS = [ private readonly UNENCRYPTED_SETTINGS = [
SettingName.EmailBackupFrequency, SettingName.NAMES.EmailBackupFrequency,
SettingName.MuteFailedBackupsEmails, SettingName.NAMES.MuteFailedBackupsEmails,
SettingName.MuteFailedCloudBackupsEmails, SettingName.NAMES.MuteFailedCloudBackupsEmails,
SettingName.MuteSignInEmails, SettingName.NAMES.MuteSignInEmails,
SettingName.MuteMarketingEmails, SettingName.NAMES.MuteMarketingEmails,
SettingName.DropboxBackupFrequency, SettingName.NAMES.DropboxBackupFrequency,
SettingName.GoogleDriveBackupFrequency, SettingName.NAMES.GoogleDriveBackupFrequency,
SettingName.OneDriveBackupFrequency, SettingName.NAMES.OneDriveBackupFrequency,
SettingName.LogSessionUserAgent, SettingName.NAMES.LogSessionUserAgent,
] ]
private readonly UNSENSITIVE_SETTINGS = [ private readonly UNSENSITIVE_SETTINGS = [
SettingName.DropboxBackupFrequency, SettingName.NAMES.DropboxBackupFrequency,
SettingName.GoogleDriveBackupFrequency, SettingName.NAMES.GoogleDriveBackupFrequency,
SettingName.OneDriveBackupFrequency, SettingName.NAMES.OneDriveBackupFrequency,
SettingName.EmailBackupFrequency, SettingName.NAMES.EmailBackupFrequency,
SettingName.MuteFailedBackupsEmails, SettingName.NAMES.MuteFailedBackupsEmails,
SettingName.MuteFailedCloudBackupsEmails, SettingName.NAMES.MuteFailedCloudBackupsEmails,
SettingName.MuteSignInEmails, SettingName.NAMES.MuteSignInEmails,
SettingName.MuteMarketingEmails, SettingName.NAMES.MuteMarketingEmails,
SettingName.ListedAuthorSecrets, SettingName.NAMES.ListedAuthorSecrets,
SettingName.LogSessionUserAgent, SettingName.NAMES.LogSessionUserAgent,
] ]
private readonly CLIENT_IMMUTABLE_SETTINGS = [SettingName.ListedAuthorSecrets] private readonly CLIENT_IMMUTABLE_SETTINGS = [SettingName.NAMES.ListedAuthorSecrets]
private readonly permissionsAssociatedWithSettings = new Map<SettingName, PermissionName>([ private readonly permissionsAssociatedWithSettings = new Map<string, PermissionName>([
[SettingName.EmailBackupFrequency, PermissionName.DailyEmailBackup], [SettingName.NAMES.EmailBackupFrequency, PermissionName.DailyEmailBackup],
]) ])
private readonly defaultSettings = new Map<SettingName, SettingDescription>([ private readonly defaultSettings = new Map<string, SettingDescription>([
[ [
SettingName.MuteSignInEmails, SettingName.NAMES.MuteSignInEmails,
{ {
sensitive: false, sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: MuteSignInEmailsOption.NotMuted, value: 'not_muted',
replaceable: false, replaceable: false,
}, },
], ],
[ [
SettingName.MuteMarketingEmails, SettingName.NAMES.MuteMarketingEmails,
{ {
sensitive: false, sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: MuteMarketingEmailsOption.NotMuted, value: 'not_muted',
replaceable: false, replaceable: false,
}, },
], ],
[ [
SettingName.LogSessionUserAgent, SettingName.NAMES.LogSessionUserAgent,
{ {
sensitive: false, sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: LogSessionUserAgentOption.Enabled, value: 'enabled',
replaceable: false, replaceable: false,
}, },
], ],
]) ])
private readonly vaultAccountDefaultSettingsOverwrites = new Map<SettingName, SettingDescription>([ private readonly vaultAccountDefaultSettingsOverwrites = new Map<string, SettingDescription>([
[ [
SettingName.LogSessionUserAgent, SettingName.NAMES.LogSessionUserAgent,
{ {
sensitive: false, sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: LogSessionUserAgentOption.Disabled, value: 'disabled',
replaceable: false, replaceable: false,
}, },
], ],
]) ])
isSettingMutableByClient(settingName: SettingName): boolean { isSettingMutableByClient(settingName: string): boolean {
if (this.CLIENT_IMMUTABLE_SETTINGS.includes(settingName)) { if (this.CLIENT_IMMUTABLE_SETTINGS.includes(settingName)) {
return false return false
} }
@@ -95,7 +90,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
return true return true
} }
getSensitivityForSetting(settingName: SettingName): boolean { getSensitivityForSetting(settingName: string): boolean {
if (this.UNSENSITIVE_SETTINGS.includes(settingName)) { if (this.UNSENSITIVE_SETTINGS.includes(settingName)) {
return false return false
} }
@@ -103,7 +98,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
return true return true
} }
getEncryptionVersionForSetting(settingName: SettingName): EncryptionVersion { getEncryptionVersionForSetting(settingName: string): EncryptionVersion {
if (this.UNENCRYPTED_SETTINGS.includes(settingName)) { if (this.UNENCRYPTED_SETTINGS.includes(settingName)) {
return EncryptionVersion.Unencrypted return EncryptionVersion.Unencrypted
} }
@@ -111,7 +106,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
return EncryptionVersion.Default return EncryptionVersion.Default
} }
getPermissionAssociatedWithSetting(settingName: SettingName): PermissionName | undefined { getPermissionAssociatedWithSetting(settingName: string): PermissionName | undefined {
if (!this.permissionsAssociatedWithSettings.has(settingName)) { if (!this.permissionsAssociatedWithSettings.has(settingName)) {
return undefined return undefined
} }
@@ -119,11 +114,11 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
return this.permissionsAssociatedWithSettings.get(settingName) return this.permissionsAssociatedWithSettings.get(settingName)
} }
getDefaultSettingsAndValuesForNewUser(): Map<SettingName, SettingDescription> { getDefaultSettingsAndValuesForNewUser(): Map<string, SettingDescription> {
return this.defaultSettings return this.defaultSettings
} }
getDefaultSettingsAndValuesForNewVaultAccount(): Map<SettingName, SettingDescription> { getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription> {
const defaultVaultSettings = new Map(this.defaultSettings) const defaultVaultSettings = new Map(this.defaultSettings)
for (const vaultAccountDefaultSettingOverwriteKey of this.vaultAccountDefaultSettingsOverwrites.keys()) { for (const vaultAccountDefaultSettingOverwriteKey of this.vaultAccountDefaultSettingsOverwrites.keys()) {
@@ -1,13 +1,12 @@
import { PermissionName } from '@standardnotes/features' import { PermissionName } from '@standardnotes/features'
import { SettingName, SubscriptionSettingName } from '@standardnotes/settings'
import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { SettingDescription } from './SettingDescription' import { SettingDescription } from './SettingDescription'
export interface SettingsAssociationServiceInterface { export interface SettingsAssociationServiceInterface {
getDefaultSettingsAndValuesForNewUser(): Map<SettingName, SettingDescription> getDefaultSettingsAndValuesForNewUser(): Map<string, SettingDescription>
getDefaultSettingsAndValuesForNewVaultAccount(): Map<SettingName, SettingDescription> getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription>
getPermissionAssociatedWithSetting(settingName: SettingName): PermissionName | undefined getPermissionAssociatedWithSetting(settingName: string): PermissionName | undefined
getEncryptionVersionForSetting(settingName: SettingName): EncryptionVersion getEncryptionVersionForSetting(settingName: string): EncryptionVersion
getSensitivityForSetting(settingName: SettingName): boolean getSensitivityForSetting(settingName: string): boolean
isSettingMutableByClient(settingName: SettingName | SubscriptionSettingName): boolean isSettingMutableByClient(settingName: string): boolean
} }
@@ -1,6 +1,6 @@
import 'reflect-metadata' import 'reflect-metadata'
import { SubscriptionSettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { Logger } from 'winston' import { Logger } from 'winston'
import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { EncryptionVersion } from '../Encryption/EncryptionVersion'
@@ -68,7 +68,7 @@ describe('SubscriptionSettingService', () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([ new Map([
[ [
SubscriptionSettingName.FileUploadBytesUsed, SettingName.NAMES.FileUploadBytesUsed,
{ {
value: '0', value: '0',
sensitive: 0, sensitive: 0,
@@ -102,7 +102,7 @@ describe('SubscriptionSettingService', () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([ new Map([
[ [
SubscriptionSettingName.FileUploadBytesUsed, SettingName.NAMES.FileUploadBytesUsed,
{ {
value: '0', value: '0',
sensitive: 0, sensitive: 0,
@@ -127,7 +127,7 @@ describe('SubscriptionSettingService', () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([ new Map([
[ [
SubscriptionSettingName.FileUploadBytesUsed, SettingName.NAMES.FileUploadBytesUsed,
{ {
value: '0', value: '0',
sensitive: 0, sensitive: 0,
@@ -152,7 +152,7 @@ describe('SubscriptionSettingService', () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([ new Map([
[ [
SubscriptionSettingName.FileUploadBytesUsed, SettingName.NAMES.FileUploadBytesUsed,
{ {
value: '0', value: '0',
sensitive: 0, sensitive: 0,
@@ -266,7 +266,7 @@ describe('SubscriptionSettingService', () => {
await createService().findSubscriptionSettingWithDecryptedValue({ await createService().findSubscriptionSettingWithDecryptedValue({
userSubscriptionUuid: '2-3-4', userSubscriptionUuid: '2-3-4',
userUuid: '1-2-3', userUuid: '1-2-3',
subscriptionSettingName: 'test' as SubscriptionSettingName, subscriptionSettingName: 'test',
}), }),
).toEqual({ ).toEqual({
serverEncryptionVersion: 1, serverEncryptionVersion: 1,
@@ -1,5 +1,4 @@
import { SubscriptionName, Uuid } from '@standardnotes/common' import { SubscriptionName, Uuid } from '@standardnotes/common'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Logger } from 'winston' import { Logger } from 'winston'
@@ -98,7 +97,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
const existing = await this.findSubscriptionSettingWithDecryptedValue({ const existing = await this.findSubscriptionSettingWithDecryptedValue({
userUuid: (await userSubscription.user).uuid, userUuid: (await userSubscription.user).uuid,
userSubscriptionUuid: userSubscription.uuid, userSubscriptionUuid: userSubscription.uuid,
subscriptionSettingName: props.name as SubscriptionSettingName, subscriptionSettingName: props.name,
settingUuid: props.uuid, settingUuid: props.uuid,
}) })
@@ -128,7 +127,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
} }
private async findPreviousSubscriptionSetting( private async findPreviousSubscriptionSetting(
settingName: SubscriptionSettingName, settingName: string,
currentUserSubscriptionUuid: Uuid, currentUserSubscriptionUuid: Uuid,
userUuid: Uuid, userUuid: Uuid,
): Promise<SubscriptionSetting | null> { ): Promise<SubscriptionSetting | null> {
@@ -1,7 +1,7 @@
import 'reflect-metadata' import 'reflect-metadata'
import { RoleName, SubscriptionName } from '@standardnotes/common' import { RoleName, SubscriptionName } from '@standardnotes/common'
import { SubscriptionSettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { PermissionName } from '@standardnotes/features' import { PermissionName } from '@standardnotes/features'
import { EncryptionVersion } from '../Encryption/EncryptionVersion' import { EncryptionVersion } from '../Encryption/EncryptionVersion'
@@ -50,14 +50,11 @@ describe('SubscriptionSettingsAssociationService', () => {
const flatSettings = [ const flatSettings = [
...( ...(
settings as Map< settings as Map<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
SubscriptionSettingName,
{ value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }
>
).keys(), ).keys(),
] ]
expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'FILE_UPLOAD_BYTES_LIMIT']) expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'FILE_UPLOAD_BYTES_LIMIT'])
expect(settings?.get(SubscriptionSettingName.FileUploadBytesLimit)).toEqual({ expect(settings?.get(SettingName.NAMES.FileUploadBytesLimit)).toEqual({
sensitive: false, sensitive: false,
serverEncryptionVersion: 0, serverEncryptionVersion: 0,
value: '107374182400', value: '107374182400',
@@ -78,14 +75,11 @@ describe('SubscriptionSettingsAssociationService', () => {
const flatSettings = [ const flatSettings = [
...( ...(
settings as Map< settings as Map<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
SubscriptionSettingName,
{ value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }
>
).keys(), ).keys(),
] ]
expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'FILE_UPLOAD_BYTES_LIMIT']) expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'FILE_UPLOAD_BYTES_LIMIT'])
expect(settings?.get(SubscriptionSettingName.FileUploadBytesLimit)).toEqual({ expect(settings?.get(SettingName.NAMES.FileUploadBytesLimit)).toEqual({
sensitive: false, sensitive: false,
serverEncryptionVersion: 0, serverEncryptionVersion: 0,
value: '104857600', value: '104857600',
@@ -1,6 +1,6 @@
import { RoleName, SubscriptionName } from '@standardnotes/common' import { RoleName, SubscriptionName } from '@standardnotes/common'
import { PermissionName } from '@standardnotes/features' import { PermissionName } from '@standardnotes/features'
import { SubscriptionSettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types' import TYPES from '../../Bootstrap/Types'
@@ -19,15 +19,12 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
@inject(TYPES.RoleRepository) private roleRepository: RoleRepositoryInterface, @inject(TYPES.RoleRepository) private roleRepository: RoleRepositoryInterface,
) {} ) {}
private readonly settingsToSubscriptionNameMap = new Map< private readonly settingsToSubscriptionNameMap = new Map<SubscriptionName, Map<string, SettingDescription>>([
SubscriptionName,
Map<SubscriptionSettingName, SettingDescription>
>([
[ [
SubscriptionName.PlusPlan, SubscriptionName.PlusPlan,
new Map([ new Map([
[ [
SubscriptionSettingName.FileUploadBytesUsed, SettingName.NAMES.FileUploadBytesUsed,
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false }, { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false },
], ],
]), ]),
@@ -36,7 +33,7 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
SubscriptionName.ProPlan, SubscriptionName.ProPlan,
new Map([ new Map([
[ [
SubscriptionSettingName.FileUploadBytesUsed, SettingName.NAMES.FileUploadBytesUsed,
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false }, { sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false },
], ],
]), ]),
@@ -45,14 +42,14 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
async getDefaultSettingsAndValuesForSubscriptionName( async getDefaultSettingsAndValuesForSubscriptionName(
subscriptionName: SubscriptionName, subscriptionName: SubscriptionName,
): Promise<Map<SubscriptionSettingName, SettingDescription> | undefined> { ): Promise<Map<string, SettingDescription> | undefined> {
const defaultSettings = this.settingsToSubscriptionNameMap.get(subscriptionName) const defaultSettings = this.settingsToSubscriptionNameMap.get(subscriptionName)
if (defaultSettings === undefined) { if (defaultSettings === undefined) {
return undefined return undefined
} }
defaultSettings.set(SubscriptionSettingName.FileUploadBytesLimit, { defaultSettings.set(SettingName.NAMES.FileUploadBytesLimit, {
sensitive: false, sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: (await this.getFileUploadLimit(subscriptionName)).toString(), value: (await this.getFileUploadLimit(subscriptionName)).toString(),
@@ -1,11 +1,10 @@
import { SubscriptionName } from '@standardnotes/common' import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { SettingDescription } from './SettingDescription' import { SettingDescription } from './SettingDescription'
export interface SubscriptionSettingsAssociationServiceInterface { export interface SubscriptionSettingsAssociationServiceInterface {
getDefaultSettingsAndValuesForSubscriptionName( getDefaultSettingsAndValuesForSubscriptionName(
subscriptionName: SubscriptionName, subscriptionName: SubscriptionName,
): Promise<Map<SubscriptionSettingName, SettingDescription> | undefined> ): Promise<Map<string, SettingDescription> | undefined>
getFileUploadLimit(subscriptionName: SubscriptionName): Promise<number> getFileUploadLimit(subscriptionName: SubscriptionName): Promise<number>
} }
@@ -47,9 +47,7 @@ describe('CreateOfflineSubscriptionToken', () => {
domainEventPublisher.publish = jest.fn() domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface> domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createEmailRequestedEvent = jest domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
.fn()
.mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
timer = {} as jest.Mocked<TimerInterface> timer = {} as jest.Mocked<TimerInterface>
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(1) timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(1)
@@ -3,7 +3,7 @@ import { SubscriptionName } from '@standardnotes/common'
import { TimerInterface } from '@standardnotes/time' import { TimerInterface } from '@standardnotes/time'
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security' import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
import { CreateValetTokenPayload, CreateValetTokenResponseData } from '@standardnotes/responses' import { CreateValetTokenPayload, CreateValetTokenResponseData } from '@standardnotes/responses'
import { SubscriptionSettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types' import TYPES from '../../../Bootstrap/Types'
import { UseCaseInterface } from '../UseCaseInterface' import { UseCaseInterface } from '../UseCaseInterface'
@@ -56,7 +56,7 @@ export class CreateValetToken implements UseCaseInterface {
const uploadBytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ const uploadBytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: regularSubscriptionUserUuid, userUuid: regularSubscriptionUserUuid,
userSubscriptionUuid: regularSubscription.uuid, userSubscriptionUuid: regularSubscription.uuid,
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed, subscriptionSettingName: SettingName.NAMES.FileUploadBytesUsed,
}) })
if (uploadBytesUsedSetting !== null) { if (uploadBytesUsedSetting !== null) {
uploadBytesUsed = +(uploadBytesUsedSetting.value as string) uploadBytesUsed = +(uploadBytesUsedSetting.value as string)
@@ -70,7 +70,7 @@ export class CreateValetToken implements UseCaseInterface {
await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: regularSubscriptionUserUuid, userUuid: regularSubscriptionUserUuid,
userSubscriptionUuid: regularSubscription.uuid, userSubscriptionUuid: regularSubscription.uuid,
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, subscriptionSettingName: SettingName.NAMES.FileUploadBytesLimit,
}) })
if (overwriteWithUserUploadBytesLimitSetting !== null) { if (overwriteWithUserUploadBytesLimitSetting !== null) {
uploadBytesLimit = +(overwriteWithUserUploadBytesLimitSetting.value as string) uploadBytesLimit = +(overwriteWithUserUploadBytesLimitSetting.value as string)
@@ -1,4 +1,4 @@
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import 'reflect-metadata' import 'reflect-metadata'
import { SettingProjector } from '../../../Projection/SettingProjector' import { SettingProjector } from '../../../Projection/SettingProjector'
import { Setting } from '../../Setting/Setting' import { Setting } from '../../Setting/Setting'
@@ -45,12 +45,12 @@ describe('GetSetting', () => {
it('should not retrieve a sensitive setting for user', async () => { it('should not retrieve a sensitive setting for user', async () => {
setting = { setting = {
sensitive: true, sensitive: true,
name: SettingName.MfaSecret, name: SettingName.NAMES.MfaSecret,
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.MfaSecret })).toEqual({ expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
success: true, success: true,
sensitive: true, sensitive: true,
}) })
@@ -59,7 +59,7 @@ describe('GetSetting', () => {
it('should retrieve a sensitive setting for user if explicitly told to', async () => { it('should retrieve a sensitive setting for user if explicitly told to', async () => {
setting = { setting = {
sensitive: true, sensitive: true,
name: SettingName.MfaSecret, name: SettingName.NAMES.MfaSecret,
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
@@ -1,4 +1,3 @@
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { GetSettingDto } from './GetSettingDto' import { GetSettingDto } from './GetSettingDto'
import { GetSettingResponse } from './GetSettingResponse' import { GetSettingResponse } from './GetSettingResponse'
@@ -19,7 +18,7 @@ export class GetSetting implements UseCaseInterface {
const setting = await this.settingService.findSettingWithDecryptedValue({ const setting = await this.settingService.findSettingWithDecryptedValue({
userUuid, userUuid,
settingName: settingName as SettingName, settingName: settingName,
}) })
if (setting === null) { if (setting === null) {
@@ -1,6 +1,6 @@
import 'reflect-metadata' import 'reflect-metadata'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { SettingProjector } from '../../../Projection/SettingProjector' import { SettingProjector } from '../../../Projection/SettingProjector'
import { Setting } from '../../Setting/Setting' import { Setting } from '../../Setting/Setting'
@@ -31,7 +31,7 @@ describe('GetSettings', () => {
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
mfaSetting = { mfaSetting = {
name: SettingName.MfaSecret, name: SettingName.NAMES.MfaSecret,
updatedAt: 122, updatedAt: 122,
sensitive: true, sensitive: true,
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
@@ -1,6 +1,6 @@
import 'reflect-metadata' import 'reflect-metadata'
import { SubscriptionSettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector' import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting' import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
@@ -54,7 +54,7 @@ describe('GetSubscriptionSetting', () => {
expect( expect(
await createUseCase().execute({ await createUseCase().execute({
userUuid: '1-2-3', userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed, subscriptionSettingName: SettingName.NAMES.FileUploadBytesUsed,
}), }),
).toEqual({ ).toEqual({
success: true, success: true,
@@ -70,7 +70,7 @@ describe('GetSubscriptionSetting', () => {
expect( expect(
await createUseCase().execute({ await createUseCase().execute({
userUuid: '1-2-3', userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, subscriptionSettingName: SettingName.NAMES.FileUploadBytesLimit,
}), }),
).toEqual({ ).toEqual({
success: false, success: false,
@@ -86,7 +86,7 @@ describe('GetSubscriptionSetting', () => {
expect( expect(
await createUseCase().execute({ await createUseCase().execute({
userUuid: '1-2-3', userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, subscriptionSettingName: SettingName.NAMES.FileUploadBytesLimit,
}), }),
).toEqual({ ).toEqual({
success: false, success: false,
@@ -99,7 +99,7 @@ describe('GetSubscriptionSetting', () => {
it('should not retrieve a sensitive setting for user', async () => { it('should not retrieve a sensitive setting for user', async () => {
subscriptionSetting = { subscriptionSetting = {
sensitive: true, sensitive: true,
name: SubscriptionSettingName.FileUploadBytesLimit, name: SettingName.NAMES.FileUploadBytesLimit,
} as jest.Mocked<SubscriptionSetting> } as jest.Mocked<SubscriptionSetting>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
@@ -109,7 +109,7 @@ describe('GetSubscriptionSetting', () => {
expect( expect(
await createUseCase().execute({ await createUseCase().execute({
userUuid: '1-2-3', userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, subscriptionSettingName: SettingName.NAMES.FileUploadBytesLimit,
}), }),
).toEqual({ ).toEqual({
success: true, success: true,
@@ -120,7 +120,7 @@ describe('GetSubscriptionSetting', () => {
it('should retrieve a sensitive setting for user if explicitly told to', async () => { it('should retrieve a sensitive setting for user if explicitly told to', async () => {
subscriptionSetting = { subscriptionSetting = {
sensitive: true, sensitive: true,
name: SubscriptionSettingName.FileUploadBytesLimit, name: SettingName.NAMES.FileUploadBytesLimit,
} as jest.Mocked<SubscriptionSetting> } as jest.Mocked<SubscriptionSetting>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
@@ -130,7 +130,7 @@ describe('GetSubscriptionSetting', () => {
expect( expect(
await createUseCase().execute({ await createUseCase().execute({
userUuid: '1-2-3', userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit, subscriptionSettingName: SettingName.NAMES.FileUploadBytesLimit,
allowSensitiveRetrieval: true, allowSensitiveRetrieval: true,
}), }),
).toEqual({ ).toEqual({
@@ -1,8 +1,7 @@
import { Uuid } from '@standardnotes/common' import { Uuid } from '@standardnotes/common'
import { SubscriptionSettingName } from '@standardnotes/settings'
export type GetSubscriptionSettingDTO = { export type GetSubscriptionSettingDTO = {
userUuid: Uuid userUuid: Uuid
subscriptionSettingName: SubscriptionSettingName subscriptionSettingName: string
allowSensitiveRetrieval?: boolean allowSensitiveRetrieval?: boolean
} }
@@ -13,7 +13,7 @@ import { SimpleSetting } from '../../Setting/SimpleSetting'
import { User } from '../../User/User' import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UpdateSetting } from './UpdateSetting' import { UpdateSetting } from './UpdateSetting'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
describe('UpdateSetting', () => { describe('UpdateSetting', () => {
let settingService: SettingServiceInterface let settingService: SettingServiceInterface
@@ -59,7 +59,7 @@ describe('UpdateSetting', () => {
it('should create a setting', async () => { it('should create a setting', async () => {
const props = { const props = {
name: SettingName.ExtensionKey, name: SettingName.NAMES.ExtensionKey,
unencryptedValue: 'test-setting-value', unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Default, serverEncryptionVersion: EncryptionVersion.Default,
sensitive: false, sensitive: false,
@@ -88,7 +88,7 @@ describe('UpdateSetting', () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null) userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const props = { const props = {
name: SettingName.ExtensionKey, name: SettingName.NAMES.ExtensionKey,
unencryptedValue: 'test-setting-value', unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false, sensitive: false,
@@ -136,7 +136,7 @@ describe('UpdateSetting', () => {
roleService.userHasPermission = jest.fn().mockReturnValue(false) roleService.userHasPermission = jest.fn().mockReturnValue(false)
const props = { const props = {
name: SettingName.ExtensionKey, name: SettingName.NAMES.ExtensionKey,
unencryptedValue: 'test-setting-value', unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false, sensitive: false,
@@ -159,7 +159,7 @@ describe('UpdateSetting', () => {
settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(false) settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(false)
const props = { const props = {
name: SettingName.ExtensionKey, name: SettingName.NAMES.ExtensionKey,
unencryptedValue: 'test-setting-value', unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted, serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false, sensitive: false,
@@ -9,7 +9,7 @@ import { SettingProjector } from '../../../Projection/SettingProjector'
import { Logger } from 'winston' import { Logger } from 'winston'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { User } from '../../User/User' import { User } from '../../User/User'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface' import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface' import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface'
@@ -25,7 +25,7 @@ export class UpdateSetting implements UseCaseInterface {
) {} ) {}
async execute(dto: UpdateSettingDto): Promise<UpdateSettingResponse> { async execute(dto: UpdateSettingDto): Promise<UpdateSettingResponse> {
if (!Object.values(SettingName).includes(dto.props.name as SettingName)) { if (!Object.values(SettingName.NAMES).includes(dto.props.name)) {
return { return {
success: false, success: false,
error: { error: {
@@ -51,7 +51,7 @@ export class UpdateSetting implements UseCaseInterface {
} }
} }
if (!(await this.userHasPermissionToUpdateSetting(user, props.name as SettingName))) { if (!(await this.userHasPermissionToUpdateSetting(user, props.name))) {
return { return {
success: false, success: false,
error: { error: {
@@ -61,10 +61,8 @@ export class UpdateSetting implements UseCaseInterface {
} }
} }
props.serverEncryptionVersion = this.settingsAssociationService.getEncryptionVersionForSetting( props.serverEncryptionVersion = this.settingsAssociationService.getEncryptionVersionForSetting(props.name)
props.name as SettingName, props.sensitive = this.settingsAssociationService.getSensitivityForSetting(props.name)
)
props.sensitive = this.settingsAssociationService.getSensitivityForSetting(props.name as SettingName)
const response = await this.settingService.createOrReplace({ const response = await this.settingService.createOrReplace({
user, user,
@@ -91,8 +89,8 @@ export class UpdateSetting implements UseCaseInterface {
throw new Error(`Unrecognized status: ${exhaustiveCheck}!`) throw new Error(`Unrecognized status: ${exhaustiveCheck}!`)
} }
private async userHasPermissionToUpdateSetting(user: User, settingName: SettingName): Promise<boolean> { private async userHasPermissionToUpdateSetting(user: User, settingName: string): Promise<boolean> {
const settingIsMutableByClient = await this.settingsAssociationService.isSettingMutableByClient(settingName) const settingIsMutableByClient = this.settingsAssociationService.isSettingMutableByClient(settingName)
if (!settingIsMutableByClient) { if (!settingIsMutableByClient) {
return false return false
} }
@@ -6,7 +6,7 @@ import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { VerifyMFA } from './VerifyMFA' import { VerifyMFA } from './VerifyMFA'
import { Setting } from '../Setting/Setting' import { Setting } from '../Setting/Setting'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { SelectorInterface } from '@standardnotes/security' import { SelectorInterface } from '@standardnotes/security'
import { LockRepositoryInterface } from '../User/LockRepositoryInterface' import { LockRepositoryInterface } from '../User/LockRepositoryInterface'
@@ -36,7 +36,7 @@ describe('VerifyMFA', () => {
lockRepository.lockSuccessfullOTP = jest.fn() lockRepository.lockSuccessfullOTP = jest.fn()
setting = { setting = {
name: SettingName.MfaSecret, name: SettingName.NAMES.MfaSecret,
value: 'shhhh', value: 'shhhh',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
@@ -58,7 +58,7 @@ describe('VerifyMFA', () => {
it('should pass MFA verification if user has MFA deleted', async () => { it('should pass MFA verification if user has MFA deleted', async () => {
setting = { setting = {
name: SettingName.MfaSecret, name: SettingName.NAMES.MfaSecret,
value: null, value: null,
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
@@ -148,7 +148,7 @@ describe('VerifyMFA', () => {
it('should not pass MFA verification if mfa is not correct', async () => { it('should not pass MFA verification if mfa is not correct', async () => {
setting = { setting = {
name: SettingName.MfaSecret, name: SettingName.NAMES.MfaSecret,
value: 'shhhh2', value: 'shhhh2',
} as jest.Mocked<Setting> } as jest.Mocked<Setting>
@@ -1,6 +1,6 @@
import * as crypto from 'crypto' import * as crypto from 'crypto'
import { ErrorTag } from '@standardnotes/common' import { ErrorTag } from '@standardnotes/common'
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { authenticator } from 'otplib' import { authenticator } from 'otplib'
@@ -50,7 +50,7 @@ export class VerifyMFA implements UseCaseInterface {
const mfaSecret = await this.settingService.findSettingWithDecryptedValue({ const mfaSecret = await this.settingService.findSettingWithDecryptedValue({
userUuid: user.uuid, userUuid: user.uuid,
settingName: SettingName.MfaSecret, settingName: SettingName.NAMES.MfaSecret,
}) })
if (mfaSecret === null || mfaSecret.value === null) { if (mfaSecret === null || mfaSecret.value === null) {
return { return {
@@ -8,7 +8,6 @@ import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface' import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
import { VerifyPredicate } from './VerifyPredicate' import { VerifyPredicate } from './VerifyPredicate'
import { EmailBackupFrequency } from '@standardnotes/settings'
describe('VerifyPredicate', () => { describe('VerifyPredicate', () => {
let settingRepository: SettingRepositoryInterface let settingRepository: SettingRepositoryInterface
@@ -30,7 +29,7 @@ describe('VerifyPredicate', () => {
}) })
it('should tell that a user has enabled email backups', async () => { it('should tell that a user has enabled email backups', async () => {
setting = { value: EmailBackupFrequency.Weekly } as jest.Mocked<Setting> setting = { value: 'weekly' } as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting) settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting)
expect( expect(
@@ -44,7 +43,7 @@ describe('VerifyPredicate', () => {
}) })
it('should tell that a user has disabled email backups', async () => { it('should tell that a user has disabled email backups', async () => {
setting = { value: EmailBackupFrequency.Disabled } as jest.Mocked<Setting> setting = { value: 'disabled' } as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting) settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(setting)
expect( expect(
@@ -1,6 +1,6 @@
import { Uuid } from '@standardnotes/common' import { Uuid } from '@standardnotes/common'
import { PredicateName, PredicateVerificationResult } from '@standardnotes/predicates' import { PredicateName, PredicateVerificationResult } from '@standardnotes/predicates'
import { EmailBackupFrequency, SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/domain-core'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types' import TYPES from '../../../Bootstrap/Types'
@@ -40,9 +40,12 @@ export class VerifyPredicate implements UseCaseInterface {
} }
private async hasUserEnabledEmailBackups(userUuid: Uuid): Promise<boolean> { private async hasUserEnabledEmailBackups(userUuid: Uuid): Promise<boolean> {
const setting = await this.settingRepository.findOneByNameAndUserUuid(SettingName.EmailBackupFrequency, userUuid) const setting = await this.settingRepository.findOneByNameAndUserUuid(
SettingName.NAMES.EmailBackupFrequency,
userUuid,
)
if (setting === null || setting.value === EmailBackupFrequency.Disabled) { if (setting === null || setting.value === 'disabled') {
return false return false
} }
@@ -1,4 +1,3 @@
import { SettingName } from '@standardnotes/settings'
import { ReadStream } from 'fs' import { ReadStream } from 'fs'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Repository } from 'typeorm' import { Repository } from 'typeorm'
@@ -18,7 +17,7 @@ export class MySQLSettingRepository implements SettingRepositoryInterface {
return this.ormRepository.save(setting) return this.ormRepository.save(setting)
} }
async findOneByUuidAndNames(uuid: string, names: SettingName[]): Promise<Setting | null> { async findOneByUuidAndNames(uuid: string, names: string[]): Promise<Setting | null> {
return this.ormRepository return this.ormRepository
.createQueryBuilder('setting') .createQueryBuilder('setting')
.where('setting.uuid = :uuid AND setting.name IN (:...names)', { .where('setting.uuid = :uuid AND setting.name IN (:...names)', {
@@ -28,7 +27,7 @@ export class MySQLSettingRepository implements SettingRepositoryInterface {
.getOne() .getOne()
} }
async streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream> { async streamAllByNameAndValue(name: string, value: string): Promise<ReadStream> {
return this.ormRepository return this.ormRepository
.createQueryBuilder('setting') .createQueryBuilder('setting')
.where('setting.name = :name AND setting.value = :value', { .where('setting.name = :name AND setting.value = :value', {
@@ -35,6 +35,19 @@ describe('RoleNameCollection', () => {
expect(valueOrError.getValue().equals(roles2)).toBeFalsy() expect(valueOrError.getValue().equals(roles2)).toBeFalsy()
}) })
it('should tell if collections are not equal', () => {
const roles1 = [RoleName.create('PRO_USER').getValue(), RoleName.create('PLUS_USER').getValue()]
const roles2 = RoleNameCollection.create([
RoleName.create('PRO_USER').getValue(),
RoleName.create('PLUS_USER').getValue(),
RoleName.create('CORE_USER').getValue(),
]).getValue()
const valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().equals(roles2)).toBeFalsy()
})
it('should tell if collections are equal', () => { it('should tell if collections are equal', () => {
const roles1 = [ const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(), RoleName.create(RoleName.NAMES.ProUser).getValue(),
@@ -0,0 +1,21 @@
import { Timestamps } from './Timestamps'
describe('Timestamps', () => {
it('should create a value object', () => {
const valueOrError = Timestamps.create(1, 2)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().createdAt).toEqual(1)
expect(valueOrError.getValue().updatedAt).toEqual(2)
})
it('should not create an invalid value object', () => {
let valueOrError = Timestamps.create(null as unknown as number, 'b' as unknown as number)
expect(valueOrError.isFailed()).toBeTruthy()
valueOrError = Timestamps.create(2, 'a' as unknown as number)
expect(valueOrError.isFailed()).toBeTruthy()
})
})
@@ -0,0 +1,30 @@
import { Result } from '../Core/Result'
import { ValueObject } from '../Core/ValueObject'
import { TimestampsProps } from './TimestampsProps'
export class Timestamps extends ValueObject<TimestampsProps> {
get createdAt(): number {
return this.props.createdAt
}
get updatedAt(): number {
return this.props.updatedAt
}
private constructor(props: TimestampsProps) {
super(props)
}
static create(createdAt: number, updatedAt: number): Result<Timestamps> {
if (isNaN(createdAt) || createdAt === null || createdAt === undefined) {
return Result.fail<Timestamps>(
`Could not create Timestamps. Creation date should be a number, given: ${createdAt}`,
)
}
if (isNaN(updatedAt) || updatedAt === null || updatedAt === undefined) {
return Result.fail<Timestamps>(`Could not create Timestamps. Update date should be a number, given: ${createdAt}`)
}
return Result.ok<Timestamps>(new Timestamps({ createdAt, updatedAt }))
}
}
@@ -0,0 +1,4 @@
export interface TimestampsProps {
createdAt: number
updatedAt: number
}
@@ -0,0 +1,28 @@
import { EncryptionVersion } from './EncryptionVersion'
describe('EncryptionVersion', () => {
it('should create a value object', () => {
const valueOrError = EncryptionVersion.create(1)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual(1)
})
it('should not create an invalid value object', () => {
let valueOrError = EncryptionVersion.create('asd' as unknown as number)
expect(valueOrError.isFailed()).toBeTruthy()
valueOrError = EncryptionVersion.create(null as unknown as number)
expect(valueOrError.isFailed()).toBeTruthy()
valueOrError = EncryptionVersion.create(undefined as unknown as number)
expect(valueOrError.isFailed()).toBeTruthy()
valueOrError = EncryptionVersion.create(754)
expect(valueOrError.isFailed()).toBeTruthy()
})
})
@@ -0,0 +1,33 @@
import { Result } from '../Core/Result'
import { ValueObject } from '../Core/ValueObject'
import { EncryptionVersionProps } from './EncryptionVersionProps'
export class EncryptionVersion extends ValueObject<EncryptionVersionProps> {
static readonly VERSIONS = {
Unencrypted: 0,
Default: 1,
}
get value(): number {
return this.props.value
}
private constructor(props: EncryptionVersionProps) {
super(props)
}
static create(version: number): Result<EncryptionVersion> {
if (
isNaN(version) ||
version === null ||
version === undefined ||
!Object.values(this.VERSIONS).includes(version)
) {
return Result.fail<EncryptionVersion>(
`Could not create EncryptionVersion. Version should be a number, given: ${version}`,
)
}
return Result.ok<EncryptionVersion>(new EncryptionVersion({ value: version }))
}
}
@@ -0,0 +1,3 @@
export interface EncryptionVersionProps {
value: number
}
@@ -0,0 +1,48 @@
import { Result } from '../Core/Result'
import { ValueObject } from '../Core/ValueObject'
import { SettingNameProps } from './SettingNameProps'
export class SettingName extends ValueObject<SettingNameProps> {
static readonly NAMES = {
MfaSecret: 'MFA_SECRET',
ExtensionKey: 'EXTENSION_KEY',
EmailBackupFrequency: 'EMAIL_BACKUP_FREQUENCY',
DropboxBackupFrequency: 'DROPBOX_BACKUP_FREQUENCY',
DropboxBackupToken: 'DROPBOX_BACKUP_TOKEN',
OneDriveBackupFrequency: 'ONE_DRIVE_BACKUP_FREQUENCY',
OneDriveBackupToken: 'ONE_DRIVE_BACKUP_TOKEN',
GoogleDriveBackupFrequency: 'GOOGLE_DRIVE_BACKUP_FREQUENCY',
GoogleDriveBackupToken: 'GOOGLE_DRIVE_BACKUP_TOKEN',
MuteFailedBackupsEmails: 'MUTE_FAILED_BACKUPS_EMAILS',
MuteFailedCloudBackupsEmails: 'MUTE_FAILED_CLOUD_BACKUPS_EMAILS',
MuteSignInEmails: 'MUTE_SIGN_IN_EMAILS',
MuteMarketingEmails: 'MUTE_MARKETING_EMAILS',
ListedAuthorSecrets: 'LISTED_AUTHOR_SECRETS',
LogSessionUserAgent: 'LOG_SESSION_USER_AGENT',
FileUploadBytesLimit: 'FILE_UPLOAD_BYTES_LIMIT',
FileUploadBytesUsed: 'FILE_UPLOAD_BYTES_USED',
EmailUnsubscribeToken: 'EMAIL_UNSUBSCRIBE_TOKEN',
}
get value(): string {
return this.props.value
}
isSensitive(): boolean {
return [SettingName.NAMES.MfaSecret, SettingName.NAMES.ExtensionKey].includes(this.value)
}
private constructor(props: SettingNameProps) {
super(props)
}
static create(name: string): Result<SettingName> {
const isValidName = Object.values(this.NAMES).includes(name)
if (!isValidName) {
return Result.fail<SettingName>(`Invalid setting name: ${name}`)
} else {
return Result.ok<SettingName>(new SettingName({ value: name }))
}
}
}
@@ -0,0 +1,3 @@
export interface SettingNameProps {
value: string
}
+8
View File
@@ -8,6 +8,8 @@ export * from './Common/RoleNameCollection'
export * from './Common/RoleNameCollectionProps' export * from './Common/RoleNameCollectionProps'
export * from './Common/Username' export * from './Common/Username'
export * from './Common/UsernameProps' export * from './Common/UsernameProps'
export * from './Common/Timestamps'
export * from './Common/TimestampsProps'
export * from './Common/Uuid' export * from './Common/Uuid'
export * from './Common/UuidProps' export * from './Common/UuidProps'
@@ -23,8 +25,14 @@ export * from './Core/ValueObjectProps'
export * from './Email/EmailLevel' export * from './Email/EmailLevel'
export * from './Email/EmailLevelProps' export * from './Email/EmailLevelProps'
export * from './Encryption/EncryptionVersion'
export * from './Encryption/EncryptionVersionProps'
export * from './Mapping/MapperInterface' export * from './Mapping/MapperInterface'
export * from './Setting/SettingName'
export * from './Setting/SettingNameProps'
export * from './Subscription/SubscriptionPlanName' export * from './Subscription/SubscriptionPlanName'
export * from './Subscription/SubscriptionPlanNameProps' export * from './Subscription/SubscriptionPlanNameProps'
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.56](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.55...@standardnotes/domain-events-infra@1.9.56) (2022-12-12)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.55](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.54...@standardnotes/domain-events-infra@1.9.55) (2022-12-12)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.54](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.53...@standardnotes/domain-events-infra@1.9.54) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.53](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.52...@standardnotes/domain-events-infra@1.9.53) (2022-12-09) ## [1.9.53](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.52...@standardnotes/domain-events-infra@1.9.53) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra **Note:** Version bump only for package @standardnotes/domain-events-infra
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/domain-events-infra", "name": "@standardnotes/domain-events-infra",
"version": "1.9.53", "version": "1.9.56",
"engines": { "engines": {
"node": ">=18.0.0 <19.0.0" "node": ">=18.0.0 <19.0.0"
}, },
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.104.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.104.0...@standardnotes/domain-events@2.104.1) (2022-12-12)
### Bug Fixes
* **domain-events:** add additional domain event services ([2980c42](https://github.com/standardnotes/server/commit/2980c42e88b6be5f065c91c86bf85a706975f801))
# [2.104.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.103.2...@standardnotes/domain-events@2.104.0) (2022-12-12)
### Features
* **domain-events:** add event for email subscription unsubscribed ([7f18fcf](https://github.com/standardnotes/server/commit/7f18fcfc139911620f2ea72729357aefd0613315))
## [2.103.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.103.1...@standardnotes/domain-events@2.103.2) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events
## [2.103.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.103.0...@standardnotes/domain-events@2.103.1) (2022-12-09) ## [2.103.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.103.0...@standardnotes/domain-events@2.103.1) (2022-12-09)
### Bug Fixes ### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/domain-events", "name": "@standardnotes/domain-events",
"version": "2.103.1", "version": "2.104.1",
"engines": { "engines": {
"node": ">=18.0.0 <19.0.0" "node": ">=18.0.0 <19.0.0"
}, },
@@ -1,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { DailyAnalyticsReportGeneratedEventPayload } from './DailyAnalyticsReportGeneratedEventPayload'
export interface DailyAnalyticsReportGeneratedEvent extends DomainEventInterface {
type: 'DAILY_ANALYTICS_REPORT_GENERATED'
payload: DailyAnalyticsReportGeneratedEventPayload
}
@@ -1,41 +0,0 @@
export interface DailyAnalyticsReportGeneratedEventPayload {
activityStatistics: Array<{
name: string
retention: number
totalCount: number
}>
statisticMeasures: Array<{
name: string
totalValue: number
average: number
increments: number
period: number
}>
activityStatisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
totalCount: number
}>
statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}>
churn: {
periodKeys: Array<string>
values: Array<{
rate: number
averageCustomersCount: number
existingCustomersChurn: number
newCustomersChurn: number
periodKey: string
}>
}
}
@@ -10,4 +10,7 @@ export enum DomainEventService {
Scheduler = 'scheduler', Scheduler = 'scheduler',
Workspace = 'workspace', Workspace = 'workspace',
Analytics = 'analytics', Analytics = 'analytics',
Revisions = 'revisions',
Email = 'email',
Settings = 'settings',
} }
@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { EmailSubscriptionUnsubscribedEventPayload } from './EmailSubscriptionUnsubscribedEventPayload'
export interface EmailSubscriptionUnsubscribedEvent extends DomainEventInterface {
type: 'EMAIL_SUBSCRIPTION_UNSUBSCRIBED'
payload: EmailSubscriptionUnsubscribedEventPayload
}
@@ -0,0 +1,4 @@
export interface EmailSubscriptionUnsubscribedEventPayload {
userEmail: string
level: string
}
+2 -2
View File
@@ -2,8 +2,6 @@ export * from './Event/AccountDeletionRequestedEvent'
export * from './Event/AccountDeletionRequestedEventPayload' export * from './Event/AccountDeletionRequestedEventPayload'
export * from './Event/CloudBackupRequestedEvent' export * from './Event/CloudBackupRequestedEvent'
export * from './Event/CloudBackupRequestedEventPayload' export * from './Event/CloudBackupRequestedEventPayload'
export * from './Event/DailyAnalyticsReportGeneratedEvent'
export * from './Event/DailyAnalyticsReportGeneratedEventPayload'
export * from './Event/DiscountApplyRequestedEvent' export * from './Event/DiscountApplyRequestedEvent'
export * from './Event/DiscountApplyRequestedEventPayload' export * from './Event/DiscountApplyRequestedEventPayload'
export * from './Event/DiscountWithdrawRequestedEvent' export * from './Event/DiscountWithdrawRequestedEvent'
@@ -18,6 +16,8 @@ export * from './Event/EmailBackupRequestedEvent'
export * from './Event/EmailBackupRequestedEventPayload' export * from './Event/EmailBackupRequestedEventPayload'
export * from './Event/EmailRequestedEvent' export * from './Event/EmailRequestedEvent'
export * from './Event/EmailRequestedEventPayload' export * from './Event/EmailRequestedEventPayload'
export * from './Event/EmailSubscriptionUnsubscribedEvent'
export * from './Event/EmailSubscriptionUnsubscribedEventPayload'
export * from './Event/ExitDiscountAppliedEvent' export * from './Event/ExitDiscountAppliedEvent'
export * from './Event/ExitDiscountAppliedEventPayload' export * from './Event/ExitDiscountAppliedEventPayload'
export * from './Event/ExitDiscountApplyRequestedEvent' export * from './Event/ExitDiscountApplyRequestedEvent'

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