mirror of
https://github.com/standardnotes/server
synced 2026-04-25 00:01:47 -04:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55f8f65c3f | |||
| 3953dbc6b4 | |||
| 0b205287d1 | |||
| 4f0bc57b1a | |||
| 7d43316597 | |||
| 65d31f011b | |||
| 80dd6efae3 | |||
| a96f2c9153 | |||
| 225e0aaf88 | |||
| f0c85910bc | |||
| 124c443528 | |||
| 37c7f8d39f | |||
| c419f1ce22 | |||
| 4949cdfe2f | |||
| cd101b96ea | |||
| 40d0e4631f | |||
| a55a995660 | |||
| 1d576d48ad | |||
| 4ff8030f87 | |||
| c15e2e2c8f | |||
| 41d31a8d75 | |||
| 10e2a26352 | |||
| 6e547f77d0 | |||
| 530a426601 | |||
| 642d6bab77 | |||
| 7980af3d82 | |||
| 2980c42e88 | |||
| b03994f9db | |||
| 41906ec2f9 | |||
| 4d1e7ff2a5 | |||
| 7f18fcfc13 | |||
| ff02ce0747 | |||
| a6056600eb | |||
| 24c94326d5 | |||
| 48c0cb5e62 | |||
| 9968efe1b2 | |||
| 6368342149 | |||
| b5f73db210 | |||
| 22d6a02d04 | |||
| 4e0bcfcccf | |||
| 104313c15d | |||
| 814289af46 | |||
| 3096cd98d5 |
@@ -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
|
||||||
|
|||||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ module.exports = {
|
|||||||
transform: {
|
transform: {
|
||||||
...tsjPreset.transform,
|
...tsjPreset.transform,
|
||||||
},
|
},
|
||||||
coveragePathIgnorePatterns: ['/Infra/'],
|
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/'],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,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,
|
||||||
|
|||||||
@@ -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
@@ -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}`)
|
||||||
|
|||||||
@@ -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,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:*",
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -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>
|
||||||
|
|||||||
+8
-8
@@ -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
-2
@@ -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,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'
|
||||||
|
|
||||||
|
|||||||
@@ -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,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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,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,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
Reference in New Issue
Block a user