mirror of
https://github.com/standardnotes/server
synced 2026-01-17 23:04:34 -05:00
Compare commits
76 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02705ea3ad | ||
|
|
df6e3f06a6 | ||
|
|
1cb5ee9fd6 | ||
|
|
893d6176c3 | ||
|
|
2c1b512e40 | ||
|
|
de50d76800 | ||
|
|
401b78e477 | ||
|
|
01837eaea9 | ||
|
|
7df699353c | ||
|
|
5455972be2 | ||
|
|
57488bcd16 | ||
|
|
b6fda901ef | ||
|
|
14669df890 | ||
|
|
64525a65f2 | ||
|
|
61fc7efecb | ||
|
|
8c7c1e4745 | ||
|
|
f64d30ec88 | ||
|
|
384dfc8da4 | ||
|
|
841784ae8c | ||
|
|
f5683cfd94 | ||
|
|
0a420ce30e | ||
|
|
a5e7132d3c | ||
|
|
6dfb2be4a2 | ||
|
|
d81cbad550 | ||
|
|
51ad06b303 | ||
|
|
27048ad95c | ||
|
|
fa9bf0b448 | ||
|
|
305190b64e | ||
|
|
98e3d18335 | ||
|
|
72e398956b | ||
|
|
1e69a13a97 | ||
|
|
7f9e6e2f44 | ||
|
|
d3c6c0d48e | ||
|
|
6c83476fd2 | ||
|
|
9cdf7e2c51 | ||
|
|
599119e14e | ||
|
|
a2c484e0f3 | ||
|
|
97ff4d5ac2 | ||
|
|
5255cfbb25 | ||
|
|
780358368b | ||
|
|
cf0b918913 | ||
|
|
4ea690204e | ||
|
|
14eb775749 | ||
|
|
bf4a3be6d9 | ||
|
|
b9e1e47871 | ||
|
|
ff532ecb22 | ||
|
|
eb21872db1 | ||
|
|
8e3df184dc | ||
|
|
b34bbcac8b | ||
|
|
226965a1d7 | ||
|
|
17b2ea126c | ||
|
|
59fc4a089c | ||
|
|
ef26dc8cbb | ||
|
|
8a0fbb28b0 | ||
|
|
618d8d5b1a | ||
|
|
3a936dc9c1 | ||
|
|
031fcd75ee | ||
|
|
c8cd23cb32 | ||
|
|
a3049938a3 | ||
|
|
b23488e862 | ||
|
|
c8203cf04c | ||
|
|
4f2616ef0a | ||
|
|
04ffc69e00 | ||
|
|
5b4bb6e7a7 | ||
|
|
2e953ba998 | ||
|
|
ed5a4eb960 | ||
|
|
31b2c05084 | ||
|
|
6e1662038c | ||
|
|
df78d88f79 | ||
|
|
addedb3091 | ||
|
|
2ea17b2dea | ||
|
|
85d2f42f47 | ||
|
|
cdb655c1bd | ||
|
|
3064d03aa9 | ||
|
|
6af6417ca2 | ||
|
|
a35271fbb3 |
@@ -190,9 +190,9 @@ jobs:
|
||||
uses: convictional/trigger-workflow-and-wait@master
|
||||
with:
|
||||
owner: standardnotes
|
||||
repo: e2e
|
||||
repo: self-hosted
|
||||
github_token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
workflow_file_name: testing-with-stable-client.yml
|
||||
workflow_file_name: testing-with-updating-client-and-server.yml
|
||||
wait_interval: 30
|
||||
client_payload: '{"${{ inputs.e2e_tag_parameter_name }}": "${{ github.sha }}"}'
|
||||
propagate_failure: true
|
||||
|
||||
376
.pnp.cjs
generated
376
.pnp.cjs
generated
@@ -126,7 +126,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@lerna-lite/cli", "npm:1.6.0"],\
|
||||
["@lerna-lite/list", "npm:1.6.0"],\
|
||||
["@lerna-lite/run", "npm:1.6.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@types/jest", "npm:29.1.1"],\
|
||||
["@types/newrelic", "npm:7.0.4"],\
|
||||
["@types/node", "npm:18.11.9"],\
|
||||
@@ -1968,6 +1968,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@noble/ed25519", [\
|
||||
["npm:1.7.1", {\
|
||||
"packageLocation": "./.yarn/cache/@noble-ed25519-npm-1.7.1-177d9beb01-b1aa4b9264.zip/node_modules/@noble/ed25519/",\
|
||||
"packageDependencies": [\
|
||||
["@noble/ed25519", "npm:1.7.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@nodelib/fs.scandir", [\
|
||||
["npm:2.1.5", {\
|
||||
"packageLocation": "./.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-5f309a3b37.zip/node_modules/@nodelib/fs.scandir/",\
|
||||
@@ -2324,6 +2333,44 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@peculiar/asn1-android", [\
|
||||
["npm:2.3.3", {\
|
||||
"packageLocation": "./.yarn/cache/@peculiar-asn1-android-npm-2.3.3-28df67d7a3-0c7cad544e.zip/node_modules/@peculiar/asn1-android/",\
|
||||
"packageDependencies": [\
|
||||
["@peculiar/asn1-android", "npm:2.3.3"],\
|
||||
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||
["asn1js", "npm:3.0.5"],\
|
||||
["tslib", "npm:2.4.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@peculiar/asn1-schema", [\
|
||||
["npm:2.3.3", {\
|
||||
"packageLocation": "./.yarn/cache/@peculiar-asn1-schema-npm-2.3.3-7c2b9469c4-f584f79d5a.zip/node_modules/@peculiar/asn1-schema/",\
|
||||
"packageDependencies": [\
|
||||
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||
["asn1js", "npm:3.0.5"],\
|
||||
["pvtsutils", "npm:1.3.2"],\
|
||||
["tslib", "npm:2.4.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@peculiar/asn1-x509", [\
|
||||
["npm:2.3.4", {\
|
||||
"packageLocation": "./.yarn/cache/@peculiar-asn1-x509-npm-2.3.4-a579005836-10a8659980.zip/node_modules/@peculiar/asn1-x509/",\
|
||||
"packageDependencies": [\
|
||||
["@peculiar/asn1-x509", "npm:2.3.4"],\
|
||||
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||
["asn1js", "npm:3.0.5"],\
|
||||
["ipaddr.js", "npm:2.0.1"],\
|
||||
["pvtsutils", "npm:1.3.2"],\
|
||||
["tslib", "npm:2.4.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@pnpm/network.ca-file", [\
|
||||
["npm:1.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/@pnpm-network.ca-file-npm-1.0.1-42bfe40bec-ed952a5574.zip/node_modules/@pnpm/network.ca-file/",\
|
||||
@@ -2447,6 +2494,16 @@ const RAW_RUNTIME_STATE =
|
||||
["tslib", "npm:1.14.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:7.28.1", {\
|
||||
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.28.1-a468033ea8-f29d747d3e.zip/node_modules/@sentry/core/",\
|
||||
"packageDependencies": [\
|
||||
["@sentry/core", "npm:7.28.1"],\
|
||||
["@sentry/types", "npm:7.28.1"],\
|
||||
["@sentry/utils", "npm:7.28.1"],\
|
||||
["tslib", "npm:1.14.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@sentry/hub", [\
|
||||
@@ -2476,6 +2533,20 @@ const RAW_RUNTIME_STATE =
|
||||
["tslib", "npm:1.14.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:7.28.1", {\
|
||||
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.28.1-b0e124fdfc-b4922d1f0a.zip/node_modules/@sentry/node/",\
|
||||
"packageDependencies": [\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@sentry/core", "npm:7.28.1"],\
|
||||
["@sentry/types", "npm:7.28.1"],\
|
||||
["@sentry/utils", "npm:7.28.1"],\
|
||||
["cookie", "npm:0.4.2"],\
|
||||
["https-proxy-agent", "npm:5.0.1"],\
|
||||
["lru_map", "npm:0.3.3"],\
|
||||
["tslib", "npm:1.14.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@sentry/profiling-node", [\
|
||||
@@ -2506,6 +2577,17 @@ const RAW_RUNTIME_STATE =
|
||||
["tslib", "npm:1.14.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:7.28.1", {\
|
||||
"packageLocation": "./.yarn/cache/@sentry-tracing-npm-7.28.1-e15d453d8e-be501ca9d7.zip/node_modules/@sentry/tracing/",\
|
||||
"packageDependencies": [\
|
||||
["@sentry/tracing", "npm:7.28.1"],\
|
||||
["@sentry/core", "npm:7.28.1"],\
|
||||
["@sentry/types", "npm:7.28.1"],\
|
||||
["@sentry/utils", "npm:7.28.1"],\
|
||||
["tslib", "npm:1.14.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@sentry/types", [\
|
||||
@@ -2515,6 +2597,13 @@ const RAW_RUNTIME_STATE =
|
||||
["@sentry/types", "npm:7.27.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:7.28.1", {\
|
||||
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.28.1-42d9a8574c-7dc6639cb7.zip/node_modules/@sentry/types/",\
|
||||
"packageDependencies": [\
|
||||
["@sentry/types", "npm:7.28.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@sentry/utils", [\
|
||||
@@ -2526,6 +2615,43 @@ const RAW_RUNTIME_STATE =
|
||||
["tslib", "npm:1.14.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:7.28.1", {\
|
||||
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.28.1-71eaeb767f-a4b5f73db0.zip/node_modules/@sentry/utils/",\
|
||||
"packageDependencies": [\
|
||||
["@sentry/utils", "npm:7.28.1"],\
|
||||
["@sentry/types", "npm:7.28.1"],\
|
||||
["tslib", "npm:1.14.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@simplewebauthn/server", [\
|
||||
["npm:6.2.2", {\
|
||||
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-6.2.2-ca870b05c2-5ffb9b1c15.zip/node_modules/@simplewebauthn/server/",\
|
||||
"packageDependencies": [\
|
||||
["@simplewebauthn/server", "npm:6.2.2"],\
|
||||
["@noble/ed25519", "npm:1.7.1"],\
|
||||
["@peculiar/asn1-android", "npm:2.3.3"],\
|
||||
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||
["@peculiar/asn1-x509", "npm:2.3.4"],\
|
||||
["base64url", "npm:3.0.1"],\
|
||||
["cbor", "npm:5.2.0"],\
|
||||
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
|
||||
["jsrsasign", "npm:10.6.1"],\
|
||||
["jwk-to-pem", "npm:2.0.5"],\
|
||||
["node-fetch", "virtual:25a5f5382d53dbf298bf7a1191760bc2e0a523a619eeb0e667b99a8649e8ad183f9e2e0b45f6fb831b92f4078b61622aa567cf79565f6aa5af9597e3c84864f6#npm:2.6.7"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@simplewebauthn/typescript-types", [\
|
||||
["npm:6.3.0-alpha.1", {\
|
||||
"packageLocation": "./.yarn/cache/@simplewebauthn-typescript-types-npm-6.3.0-alpha.1-629da05c10-5667c214e9.zip/node_modules/@simplewebauthn/typescript-types/",\
|
||||
"packageDependencies": [\
|
||||
["@simplewebauthn/typescript-types", "npm:6.3.0-alpha.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@sinclair/typebox", [\
|
||||
@@ -2581,7 +2707,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/analytics", "workspace:packages/analytics"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
@@ -2589,6 +2715,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
["@types/ioredis", "npm:5.0.0"],\
|
||||
["@types/jest", "npm:29.1.1"],\
|
||||
["@types/mixpanel", "npm:2.14.4"],\
|
||||
["@types/newrelic", "npm:7.0.4"],\
|
||||
["@types/node", "npm:18.11.9"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.30.5"],\
|
||||
@@ -2600,6 +2727,7 @@ const RAW_RUNTIME_STATE =
|
||||
["inversify", "npm:6.0.1"],\
|
||||
["ioredis", "npm:5.2.4"],\
|
||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||
["mixpanel", "npm:0.17.0"],\
|
||||
["mysql2", "npm:2.3.3"],\
|
||||
["newrelic", "npm:9.6.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"],\
|
||||
@@ -2633,7 +2761,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
@@ -2689,9 +2817,11 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/auth-server", "workspace:packages/auth"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@sentry/profiling-node", "npm:0.0.12"],\
|
||||
["@sentry/tracing", "npm:7.27.0"],\
|
||||
["@sentry/tracing", "npm:7.28.1"],\
|
||||
["@simplewebauthn/server", "npm:6.2.2"],\
|
||||
["@simplewebauthn/typescript-types", "npm:6.3.0-alpha.1"],\
|
||||
["@standardnotes/api", "npm:1.19.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
@@ -2915,7 +3045,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageLocation": "./packages/files/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/files-server", "workspace:packages/files"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/config", "npm:2.4.3"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
@@ -3050,7 +3180,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@standardnotes/api", "npm:1.19.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
@@ -3095,7 +3225,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
@@ -3156,7 +3286,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@lerna-lite/cli", "npm:1.6.0"],\
|
||||
["@lerna-lite/list", "npm:1.6.0"],\
|
||||
["@lerna-lite/run", "npm:1.6.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@types/jest", "npm:29.1.1"],\
|
||||
["@types/newrelic", "npm:7.0.4"],\
|
||||
["@types/node", "npm:18.11.9"],\
|
||||
@@ -3222,9 +3352,9 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@sentry/profiling-node", "npm:0.0.12"],\
|
||||
["@sentry/tracing", "npm:7.27.0"],\
|
||||
["@sentry/tracing", "npm:7.28.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
@@ -3324,7 +3454,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@standardnotes/api", "npm:1.19.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
@@ -3364,7 +3494,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.27.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@standardnotes/api", "npm:1.19.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
@@ -3771,6 +3901,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/mixpanel", [\
|
||||
["npm:2.14.4", {\
|
||||
"packageLocation": "./.yarn/cache/@types-mixpanel-npm-2.14.4-34bd98306f-a2bf6e633e.zip/node_modules/@types/mixpanel/",\
|
||||
"packageDependencies": [\
|
||||
["@types/mixpanel", "npm:2.14.4"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/newrelic", [\
|
||||
["npm:7.0.4", {\
|
||||
"packageLocation": "./.yarn/cache/@types-newrelic-npm-7.0.4-4f0b179b51-b44215b3ab.zip/node_modules/@types/newrelic/",\
|
||||
@@ -4818,6 +4957,31 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["asn1.js", [\
|
||||
["npm:5.4.1", {\
|
||||
"packageLocation": "./.yarn/cache/asn1.js-npm-5.4.1-37c7edbcb0-5c36f81388.zip/node_modules/asn1.js/",\
|
||||
"packageDependencies": [\
|
||||
["asn1.js", "npm:5.4.1"],\
|
||||
["bn.js", "npm:4.12.0"],\
|
||||
["inherits", "npm:2.0.4"],\
|
||||
["minimalistic-assert", "npm:1.0.1"],\
|
||||
["safer-buffer", "npm:2.1.2"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["asn1js", [\
|
||||
["npm:3.0.5", {\
|
||||
"packageLocation": "./.yarn/cache/asn1js-npm-3.0.5-cf5558af33-d0bc57da97.zip/node_modules/asn1js/",\
|
||||
"packageDependencies": [\
|
||||
["asn1js", "npm:3.0.5"],\
|
||||
["pvtsutils", "npm:1.3.2"],\
|
||||
["pvutils", "npm:1.1.3"],\
|
||||
["tslib", "npm:2.4.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["async", [\
|
||||
["npm:3.2.4", {\
|
||||
"packageLocation": "./.yarn/cache/async-npm-3.2.4-aba13508f9-9719e38d24.zip/node_modules/async/",\
|
||||
@@ -5031,6 +5195,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["base64url", [\
|
||||
["npm:3.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/base64url-npm-3.0.1-4c171c4917-72e1401ffe.zip/node_modules/base64url/",\
|
||||
"packageDependencies": [\
|
||||
["base64url", "npm:3.0.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["bcryptjs", [\
|
||||
["npm:2.4.3", {\
|
||||
"packageLocation": "./.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-bf6a43e9c4.zip/node_modules/bcryptjs/",\
|
||||
@@ -5049,6 +5222,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["bignumber.js", [\
|
||||
["npm:9.1.1", {\
|
||||
"packageLocation": "./.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-e44d008049.zip/node_modules/bignumber.js/",\
|
||||
"packageDependencies": [\
|
||||
["bignumber.js", "npm:9.1.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["binary-extensions", [\
|
||||
["npm:2.2.0", {\
|
||||
"packageLocation": "./.yarn/cache/binary-extensions-npm-2.2.0-180c33fec7-16cf7c0cfd.zip/node_modules/binary-extensions/",\
|
||||
@@ -5070,6 +5252,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["bn.js", [\
|
||||
["npm:4.12.0", {\
|
||||
"packageLocation": "./.yarn/cache/bn.js-npm-4.12.0-3ec6c884f6-bfb4590775.zip/node_modules/bn.js/",\
|
||||
"packageDependencies": [\
|
||||
["bn.js", "npm:4.12.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["body-parser", [\
|
||||
["npm:1.20.1", {\
|
||||
"packageLocation": "./.yarn/cache/body-parser-npm-1.20.1-759fd14db9-33f202c9d5.zip/node_modules/body-parser/",\
|
||||
@@ -5137,6 +5328,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["brorand", [\
|
||||
["npm:1.1.0", {\
|
||||
"packageLocation": "./.yarn/cache/brorand-npm-1.1.0-ea86634c4b-f736e127fb.zip/node_modules/brorand/",\
|
||||
"packageDependencies": [\
|
||||
["brorand", "npm:1.1.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["browserslist", [\
|
||||
["npm:4.21.1", {\
|
||||
"packageLocation": "./.yarn/cache/browserslist-npm-4.21.1-930e90b93a-617d624493.zip/node_modules/browserslist/",\
|
||||
@@ -5372,6 +5572,17 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["cbor", [\
|
||||
["npm:5.2.0", {\
|
||||
"packageLocation": "./.yarn/cache/cbor-npm-5.2.0-4f6440587f-d60986b9d0.zip/node_modules/cbor/",\
|
||||
"packageDependencies": [\
|
||||
["cbor", "npm:5.2.0"],\
|
||||
["bignumber.js", "npm:9.1.1"],\
|
||||
["nofilter", "npm:1.0.4"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["chalk", [\
|
||||
["npm:2.4.2", {\
|
||||
"packageLocation": "./.yarn/cache/chalk-npm-2.4.2-3ea16dd91e-befd2fe888.zip/node_modules/chalk/",\
|
||||
@@ -6496,6 +6707,22 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["elliptic", [\
|
||||
["npm:6.5.4", {\
|
||||
"packageLocation": "./.yarn/cache/elliptic-npm-6.5.4-0ca8204a86-4453b008cf.zip/node_modules/elliptic/",\
|
||||
"packageDependencies": [\
|
||||
["elliptic", "npm:6.5.4"],\
|
||||
["bn.js", "npm:4.12.0"],\
|
||||
["brorand", "npm:1.1.0"],\
|
||||
["hash.js", "npm:1.1.7"],\
|
||||
["hmac-drbg", "npm:1.0.1"],\
|
||||
["inherits", "npm:2.0.4"],\
|
||||
["minimalistic-assert", "npm:1.0.1"],\
|
||||
["minimalistic-crypto-utils", "npm:1.0.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["emittery", [\
|
||||
["npm:0.10.2", {\
|
||||
"packageLocation": "./.yarn/cache/emittery-npm-0.10.2-aac10498b5-c55b286714.zip/node_modules/emittery/",\
|
||||
@@ -8002,6 +8229,17 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["hash.js", [\
|
||||
["npm:1.1.7", {\
|
||||
"packageLocation": "./.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e4266370d1.zip/node_modules/hash.js/",\
|
||||
"packageDependencies": [\
|
||||
["hash.js", "npm:1.1.7"],\
|
||||
["inherits", "npm:2.0.4"],\
|
||||
["minimalistic-assert", "npm:1.0.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["helmet", [\
|
||||
["npm:6.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/helmet-npm-6.0.0-2285459f57-73b6ba802d.zip/node_modules/helmet/",\
|
||||
@@ -8020,6 +8258,18 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["hmac-drbg", [\
|
||||
["npm:1.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/hmac-drbg-npm-1.0.1-3499ad31cd-4e88d58ffc.zip/node_modules/hmac-drbg/",\
|
||||
"packageDependencies": [\
|
||||
["hmac-drbg", "npm:1.0.1"],\
|
||||
["hash.js", "npm:1.1.7"],\
|
||||
["minimalistic-assert", "npm:1.0.1"],\
|
||||
["minimalistic-crypto-utils", "npm:1.0.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["hosted-git-info", [\
|
||||
["npm:2.8.9", {\
|
||||
"packageLocation": "./.yarn/cache/hosted-git-info-npm-2.8.9-62c44fa93f-c24da52f98.zip/node_modules/hosted-git-info/",\
|
||||
@@ -8110,6 +8360,15 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["https-proxy-agent", [\
|
||||
["npm:5.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.0-bb777903c3-77d11b0e2c.zip/node_modules/https-proxy-agent/",\
|
||||
"packageDependencies": [\
|
||||
["https-proxy-agent", "npm:5.0.0"],\
|
||||
["agent-base", "npm:6.0.2"],\
|
||||
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:5.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.1-42d65f358e-8e767faec9.zip/node_modules/https-proxy-agent/",\
|
||||
"packageDependencies": [\
|
||||
@@ -8400,6 +8659,13 @@ const RAW_RUNTIME_STATE =
|
||||
["ipaddr.js", "npm:1.9.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:2.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/ipaddr.js-npm-2.0.1-04e97280d7-04ce6c896c.zip/node_modules/ipaddr.js/",\
|
||||
"packageDependencies": [\
|
||||
["ipaddr.js", "npm:2.0.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["is-arguments", [\
|
||||
@@ -9602,6 +9868,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["jsrsasign", [\
|
||||
["npm:10.6.1", {\
|
||||
"packageLocation": "./.yarn/cache/jsrsasign-npm-10.6.1-a8fa295369-e8e9c1b24f.zip/node_modules/jsrsasign/",\
|
||||
"packageDependencies": [\
|
||||
["jsrsasign", "npm:10.6.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["jwa", [\
|
||||
["npm:1.4.1", {\
|
||||
"packageLocation": "./.yarn/cache/jwa-npm-1.4.1-4f19d6572c-0cc3e68b68.zip/node_modules/jwa/",\
|
||||
@@ -9614,6 +9889,18 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["jwk-to-pem", [\
|
||||
["npm:2.0.5", {\
|
||||
"packageLocation": "./.yarn/cache/jwk-to-pem-npm-2.0.5-aff7d9f125-fced3a75b0.zip/node_modules/jwk-to-pem/",\
|
||||
"packageDependencies": [\
|
||||
["jwk-to-pem", "npm:2.0.5"],\
|
||||
["asn1.js", "npm:5.4.1"],\
|
||||
["elliptic", "npm:6.5.4"],\
|
||||
["safe-buffer", "npm:5.2.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["jws", [\
|
||||
["npm:3.2.2", {\
|
||||
"packageLocation": "./.yarn/cache/jws-npm-3.2.2-c1ae59c7af-347ed7c334.zip/node_modules/jws/",\
|
||||
@@ -10227,6 +10514,24 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["minimalistic-assert", [\
|
||||
["npm:1.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/minimalistic-assert-npm-1.0.1-dc8bb23d29-e2310081d8.zip/node_modules/minimalistic-assert/",\
|
||||
"packageDependencies": [\
|
||||
["minimalistic-assert", "npm:1.0.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["minimalistic-crypto-utils", [\
|
||||
["npm:1.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/minimalistic-crypto-utils-npm-1.0.1-e66b10822e-7d909decd2.zip/node_modules/minimalistic-crypto-utils/",\
|
||||
"packageDependencies": [\
|
||||
["minimalistic-crypto-utils", "npm:1.0.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["minimatch", [\
|
||||
["npm:3.1.2", {\
|
||||
"packageLocation": "./.yarn/cache/minimatch-npm-3.1.2-9405269906-97f5615ee8.zip/node_modules/minimatch/",\
|
||||
@@ -10351,6 +10656,16 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["mixpanel", [\
|
||||
["npm:0.17.0", {\
|
||||
"packageLocation": "./.yarn/cache/mixpanel-npm-0.17.0-3073ce9949-5a945bdbdd.zip/node_modules/mixpanel/",\
|
||||
"packageDependencies": [\
|
||||
["mixpanel", "npm:0.17.0"],\
|
||||
["https-proxy-agent", "npm:5.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["mkdirp", [\
|
||||
["npm:1.0.4", {\
|
||||
"packageLocation": "./.yarn/cache/mkdirp-npm-1.0.4-37f6ef56b9-1233611198.zip/node_modules/mkdirp/",\
|
||||
@@ -10646,6 +10961,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["nofilter", [\
|
||||
["npm:1.0.4", {\
|
||||
"packageLocation": "./.yarn/cache/nofilter-npm-1.0.4-1cbdc6c03a-9a26874e7d.zip/node_modules/nofilter/",\
|
||||
"packageDependencies": [\
|
||||
["nofilter", "npm:1.0.4"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["nopt", [\
|
||||
["npm:1.0.10", {\
|
||||
"packageLocation": "./.yarn/cache/nopt-npm-1.0.10-f3db192976-efa5a9c2c1.zip/node_modules/nopt/",\
|
||||
@@ -11702,6 +12026,25 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["pvtsutils", [\
|
||||
["npm:1.3.2", {\
|
||||
"packageLocation": "./.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-eb22d3df60.zip/node_modules/pvtsutils/",\
|
||||
"packageDependencies": [\
|
||||
["pvtsutils", "npm:1.3.2"],\
|
||||
["tslib", "npm:2.4.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["pvutils", [\
|
||||
["npm:1.1.3", {\
|
||||
"packageLocation": "./.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-0cb4f4878f.zip/node_modules/pvutils/",\
|
||||
"packageDependencies": [\
|
||||
["pvutils", "npm:1.1.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["q", [\
|
||||
["npm:1.5.1", {\
|
||||
"packageLocation": "./.yarn/cache/q-npm-1.5.1-a28b3cfeaf-276b7e93fc.zip/node_modules/q/",\
|
||||
@@ -13386,6 +13729,13 @@ const RAW_RUNTIME_STATE =
|
||||
["tslib", "npm:2.4.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:2.4.1", {\
|
||||
"packageLocation": "./.yarn/cache/tslib-npm-2.4.1-36f0ed04db-a739a21e3f.zip/node_modules/tslib/",\
|
||||
"packageDependencies": [\
|
||||
["tslib", "npm:2.4.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["tsutils", [\
|
||||
|
||||
BIN
.yarn/cache/@noble-ed25519-npm-1.7.1-177d9beb01-b1aa4b9264.zip
vendored
Normal file
BIN
.yarn/cache/@noble-ed25519-npm-1.7.1-177d9beb01-b1aa4b9264.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@peculiar-asn1-android-npm-2.3.3-28df67d7a3-0c7cad544e.zip
vendored
Normal file
BIN
.yarn/cache/@peculiar-asn1-android-npm-2.3.3-28df67d7a3-0c7cad544e.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@peculiar-asn1-schema-npm-2.3.3-7c2b9469c4-f584f79d5a.zip
vendored
Normal file
BIN
.yarn/cache/@peculiar-asn1-schema-npm-2.3.3-7c2b9469c4-f584f79d5a.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@peculiar-asn1-x509-npm-2.3.4-a579005836-10a8659980.zip
vendored
Normal file
BIN
.yarn/cache/@peculiar-asn1-x509-npm-2.3.4-a579005836-10a8659980.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@sentry-core-npm-7.28.1-a468033ea8-f29d747d3e.zip
vendored
Normal file
BIN
.yarn/cache/@sentry-core-npm-7.28.1-a468033ea8-f29d747d3e.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@sentry-node-npm-7.28.1-b0e124fdfc-b4922d1f0a.zip
vendored
Normal file
BIN
.yarn/cache/@sentry-node-npm-7.28.1-b0e124fdfc-b4922d1f0a.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@sentry-tracing-npm-7.28.1-e15d453d8e-be501ca9d7.zip
vendored
Normal file
BIN
.yarn/cache/@sentry-tracing-npm-7.28.1-e15d453d8e-be501ca9d7.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@sentry-types-npm-7.28.1-42d9a8574c-7dc6639cb7.zip
vendored
Normal file
BIN
.yarn/cache/@sentry-types-npm-7.28.1-42d9a8574c-7dc6639cb7.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@sentry-utils-npm-7.28.1-71eaeb767f-a4b5f73db0.zip
vendored
Normal file
BIN
.yarn/cache/@sentry-utils-npm-7.28.1-71eaeb767f-a4b5f73db0.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@simplewebauthn-server-npm-6.2.2-ca870b05c2-5ffb9b1c15.zip
vendored
Normal file
BIN
.yarn/cache/@simplewebauthn-server-npm-6.2.2-ca870b05c2-5ffb9b1c15.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@simplewebauthn-typescript-types-npm-6.3.0-alpha.1-629da05c10-5667c214e9.zip
vendored
Normal file
BIN
.yarn/cache/@simplewebauthn-typescript-types-npm-6.3.0-alpha.1-629da05c10-5667c214e9.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@types-mixpanel-npm-2.14.4-34bd98306f-a2bf6e633e.zip
vendored
Normal file
BIN
.yarn/cache/@types-mixpanel-npm-2.14.4-34bd98306f-a2bf6e633e.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/asn1.js-npm-5.4.1-37c7edbcb0-5c36f81388.zip
vendored
Normal file
BIN
.yarn/cache/asn1.js-npm-5.4.1-37c7edbcb0-5c36f81388.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/asn1js-npm-3.0.5-cf5558af33-d0bc57da97.zip
vendored
Normal file
BIN
.yarn/cache/asn1js-npm-3.0.5-cf5558af33-d0bc57da97.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/base64url-npm-3.0.1-4c171c4917-72e1401ffe.zip
vendored
Normal file
BIN
.yarn/cache/base64url-npm-3.0.1-4c171c4917-72e1401ffe.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-e44d008049.zip
vendored
Normal file
BIN
.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-e44d008049.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/bn.js-npm-4.12.0-3ec6c884f6-bfb4590775.zip
vendored
Normal file
BIN
.yarn/cache/bn.js-npm-4.12.0-3ec6c884f6-bfb4590775.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/brorand-npm-1.1.0-ea86634c4b-f736e127fb.zip
vendored
Normal file
BIN
.yarn/cache/brorand-npm-1.1.0-ea86634c4b-f736e127fb.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/cbor-npm-5.2.0-4f6440587f-d60986b9d0.zip
vendored
Normal file
BIN
.yarn/cache/cbor-npm-5.2.0-4f6440587f-d60986b9d0.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/elliptic-npm-6.5.4-0ca8204a86-4453b008cf.zip
vendored
Normal file
BIN
.yarn/cache/elliptic-npm-6.5.4-0ca8204a86-4453b008cf.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e4266370d1.zip
vendored
Normal file
BIN
.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e4266370d1.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/hmac-drbg-npm-1.0.1-3499ad31cd-4e88d58ffc.zip
vendored
Normal file
BIN
.yarn/cache/hmac-drbg-npm-1.0.1-3499ad31cd-4e88d58ffc.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/https-proxy-agent-npm-5.0.0-bb777903c3-77d11b0e2c.zip
vendored
Normal file
BIN
.yarn/cache/https-proxy-agent-npm-5.0.0-bb777903c3-77d11b0e2c.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/ipaddr.js-npm-2.0.1-04e97280d7-04ce6c896c.zip
vendored
Normal file
BIN
.yarn/cache/ipaddr.js-npm-2.0.1-04e97280d7-04ce6c896c.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/jsrsasign-npm-10.6.1-a8fa295369-e8e9c1b24f.zip
vendored
Normal file
BIN
.yarn/cache/jsrsasign-npm-10.6.1-a8fa295369-e8e9c1b24f.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/jwk-to-pem-npm-2.0.5-aff7d9f125-fced3a75b0.zip
vendored
Normal file
BIN
.yarn/cache/jwk-to-pem-npm-2.0.5-aff7d9f125-fced3a75b0.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/minimalistic-assert-npm-1.0.1-dc8bb23d29-e2310081d8.zip
vendored
Normal file
BIN
.yarn/cache/minimalistic-assert-npm-1.0.1-dc8bb23d29-e2310081d8.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/minimalistic-crypto-utils-npm-1.0.1-e66b10822e-7d909decd2.zip
vendored
Normal file
BIN
.yarn/cache/minimalistic-crypto-utils-npm-1.0.1-e66b10822e-7d909decd2.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/mixpanel-npm-0.17.0-3073ce9949-5a945bdbdd.zip
vendored
Normal file
BIN
.yarn/cache/mixpanel-npm-0.17.0-3073ce9949-5a945bdbdd.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/nofilter-npm-1.0.4-1cbdc6c03a-9a26874e7d.zip
vendored
Normal file
BIN
.yarn/cache/nofilter-npm-1.0.4-1cbdc6c03a-9a26874e7d.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-eb22d3df60.zip
vendored
Normal file
BIN
.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-eb22d3df60.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-0cb4f4878f.zip
vendored
Normal file
BIN
.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-0cb4f4878f.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/tslib-npm-2.4.1-36f0ed04db-a739a21e3f.zip
vendored
Normal file
BIN
.yarn/cache/tslib-npm-2.4.1-36f0ed04db-a739a21e3f.zip
vendored
Normal file
Binary file not shown.
@@ -61,7 +61,7 @@
|
||||
},
|
||||
"packageManager": "yarn@4.0.0-rc.25",
|
||||
"dependencies": {
|
||||
"@sentry/node": "^7.27.0",
|
||||
"@sentry/node": "^7.28.1",
|
||||
"newrelic": "^9.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ DB_DATABASE=analytics
|
||||
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
||||
DB_MIGRATIONS_PATH=dist/migrations/*.js
|
||||
|
||||
ADMIN_EMAILS=test@standardnotes.com
|
||||
|
||||
REDIS_URL=redis://cache
|
||||
REDIS_EVENTS_CHANNEL=events
|
||||
|
||||
@@ -26,3 +28,6 @@ NEW_RELIC_NO_CONFIG_FILE=true
|
||||
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
||||
NEW_RELIC_LOG_ENABLED=false
|
||||
NEW_RELIC_LOG_LEVEL=info
|
||||
|
||||
# (Optional) Mixpanel
|
||||
MIXPANEL_TOKEN=
|
||||
|
||||
@@ -3,6 +3,87 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.19.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.18.0...@standardnotes/analytics@2.19.0) (2022-12-30)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add mixpanel events tracking ([df6e3f0](https://github.com/standardnotes/server/commit/df6e3f06a6868e30e60dd98431122983724644b4))
|
||||
|
||||
# [2.18.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.8...@standardnotes/analytics@2.18.0) (2022-12-30)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add mixpanel ([893d617](https://github.com/standardnotes/server/commit/893d6176c3b0b56c45e5188fe982232db2ceedc4))
|
||||
|
||||
## [2.17.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.7...@standardnotes/analytics@2.17.8) (2022-12-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.17.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.6...@standardnotes/analytics@2.17.7) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** monthly numbers of active users ([b34bbca](https://github.com/standardnotes/server/commit/b34bbcac8b9604283b3a5959ab3218c468ce8a00))
|
||||
|
||||
## [2.17.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.5...@standardnotes/analytics@2.17.6) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** filtered counts for user activity check ([17b2ea1](https://github.com/standardnotes/server/commit/17b2ea126c5ad2d7cf07657def63f9977f239a3c))
|
||||
|
||||
## [2.17.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.4...@standardnotes/analytics@2.17.5) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** accessing analytics in report ([ef26dc8](https://github.com/standardnotes/server/commit/ef26dc8cbb967e088ae7387ff6dbec1e60dc3ee4))
|
||||
|
||||
## [2.17.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.3...@standardnotes/analytics@2.17.4) (2022-12-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.17.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.2...@standardnotes/analytics@2.17.3) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** add debug logs for the report ([031fcd7](https://github.com/standardnotes/server/commit/031fcd75eecdcf4c2f17257754a0ba3f24ba6d6e))
|
||||
|
||||
## [2.17.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.1...@standardnotes/analytics@2.17.2) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** calculating active users ([a304993](https://github.com/standardnotes/server/commit/a3049938a31e21a5867a314ac62bee6aa4990d57))
|
||||
|
||||
## [2.17.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.0...@standardnotes/analytics@2.17.1) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** container binding ([04ffc69](https://github.com/standardnotes/server/commit/04ffc69e000803107d8834c286de97b3d213a842))
|
||||
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
|
||||
|
||||
# [2.17.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.16.0...@standardnotes/analytics@2.17.0) (2022-12-20)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add users activit to the report email ([ed5a4eb](https://github.com/standardnotes/server/commit/ed5a4eb960a6c8fe9d0c77331f29dc3c7ffb9100))
|
||||
|
||||
# [2.16.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.1...@standardnotes/analytics@2.16.0) (2022-12-20)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add active users stats to report ([6e16620](https://github.com/standardnotes/server/commit/6e1662038c3340fb60939464616789bab7639160))
|
||||
|
||||
## [2.15.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.0...@standardnotes/analytics@2.15.1) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
|
||||
|
||||
# [2.15.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.14.0...@standardnotes/analytics@2.15.0) (2022-12-19)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
|
||||
|
||||
# [2.14.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.13.0...@standardnotes/analytics@2.14.0) (2022-12-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -121,6 +121,10 @@ const requestReport = async (
|
||||
StatisticMeasureName.NAMES.FiveYearPlansMRR,
|
||||
StatisticMeasureName.NAMES.PlusPlansMRR,
|
||||
StatisticMeasureName.NAMES.ProPlansMRR,
|
||||
StatisticMeasureName.NAMES.ActiveUsers,
|
||||
StatisticMeasureName.NAMES.ActiveFreeUsers,
|
||||
StatisticMeasureName.NAMES.ActivePlusUsers,
|
||||
StatisticMeasureName.NAMES.ActiveProUsers,
|
||||
]
|
||||
for (const statisticName of thirtyDaysStatisticsNames) {
|
||||
statisticsOverTime.push({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.14.0",
|
||||
"version": "2.19.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
@@ -27,6 +27,7 @@
|
||||
"devDependencies": {
|
||||
"@types/ioredis": "^5.0.0",
|
||||
"@types/jest": "^29.1.1",
|
||||
"@types/mixpanel": "^2.14.4",
|
||||
"@types/newrelic": "^7.0.4",
|
||||
"@types/node": "^18.11.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
@@ -38,7 +39,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.27.0",
|
||||
"@sentry/node": "^7.28.1",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
@@ -49,6 +50,7 @@
|
||||
"dotenv": "^16.0.1",
|
||||
"inversify": "^6.0.1",
|
||||
"ioredis": "^5.2.4",
|
||||
"mixpanel": "^0.17.0",
|
||||
"mysql2": "^2.3.3",
|
||||
"newrelic": "^9.6.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
DomainEventSubscriberFactoryInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Mixpanel = require('mixpanel')
|
||||
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
@@ -134,6 +136,7 @@ export class ContainerConfigLoader {
|
||||
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.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
|
||||
container.bind(TYPES.MIXPANEL_TOKEN).toConstantValue(env.get('MIXPANEL_TOKEN', true))
|
||||
|
||||
// Services
|
||||
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
@@ -157,6 +160,9 @@ export class ContainerConfigLoader {
|
||||
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
||||
)
|
||||
}
|
||||
if (env.get('MIXPANEL_TOKEN', true)) {
|
||||
container.bind<Mixpanel>(TYPES.MixpanelClient).toConstantValue(Mixpanel.init(env.get('MIXPANEL_TOKEN', true)))
|
||||
}
|
||||
|
||||
// Repositories
|
||||
container
|
||||
@@ -218,7 +224,9 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
new StatisticPersistenceRequestedEventHandler(
|
||||
container.get(TYPES.PersistStatistic),
|
||||
container.get(TYPES.Timer),
|
||||
container.get(TYPES.Logger),
|
||||
env.get('MIXPANEL_TOKEN', true) ? container.get(TYPES.MixpanelClient) : null,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const TYPES = {
|
||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||
ADMIN_EMAILS: Symbol.for('ADMIN_EMAILS'),
|
||||
MIXPANEL_TOKEN: Symbol.for('MIXPANEL_TOKEN'),
|
||||
// Repositories
|
||||
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
|
||||
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
|
||||
@@ -48,6 +49,7 @@ const TYPES = {
|
||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||
Timer: Symbol.for('Timer'),
|
||||
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
|
||||
MixpanelClient: Symbol.for('MixpanelClient'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
|
||||
@@ -5,6 +5,38 @@ import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
const countActiveUsers = (measureName: string, data: any): { yesterday: number; last30Days: number } => {
|
||||
const totalActiveUsersLast30DaysIncludingToday = data.statisticsOverTime.find(
|
||||
(a: { name: string; period: number }) => a.name === measureName && a.period === 27,
|
||||
)
|
||||
|
||||
const totalActiveUsersYesterday =
|
||||
totalActiveUsersLast30DaysIncludingToday.counts[totalActiveUsersLast30DaysIncludingToday.counts.length - 2]
|
||||
.totalCount
|
||||
|
||||
const filteredCounts = totalActiveUsersLast30DaysIncludingToday.counts.filter(
|
||||
(count: { totalCount: number }) => count.totalCount !== 0,
|
||||
)
|
||||
if (filteredCounts.length === 0) {
|
||||
return {
|
||||
yesterday: 0,
|
||||
last30Days: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const last30DaysNumbers = filteredCounts.map((count: { totalCount: number }) => count.totalCount)
|
||||
const last30DaysCount = last30DaysNumbers.reduce((previousValue: number, currentValue: number) => {
|
||||
return previousValue + currentValue
|
||||
})
|
||||
|
||||
const averageActiveUsersLast30Days = Math.floor(last30DaysCount / last30DaysNumbers.length)
|
||||
|
||||
return {
|
||||
yesterday: totalActiveUsersYesterday,
|
||||
last30Days: averageActiveUsersLast30Days,
|
||||
}
|
||||
}
|
||||
|
||||
const getChartUrls = (
|
||||
data: any,
|
||||
): {
|
||||
@@ -12,7 +44,6 @@ const getChartUrls = (
|
||||
users: string
|
||||
quarterlyPerformance: string
|
||||
churn: string
|
||||
mrr: string
|
||||
mrrMonthly: string
|
||||
} => {
|
||||
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
|
||||
@@ -237,82 +268,6 @@ const getChartUrls = (
|
||||
},
|
||||
}
|
||||
|
||||
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))
|
||||
@@ -371,7 +326,6 @@ const getChartUrls = (
|
||||
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))}`,
|
||||
}
|
||||
}
|
||||
@@ -608,12 +562,39 @@ export const html = (data: any, timer: TimerInterface) => {
|
||||
(value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
|
||||
)
|
||||
|
||||
const totalActiveUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveUsers, data)
|
||||
const totalActiveFreeUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveFreeUsers, data)
|
||||
const totalActivePlusUsers = countActiveUsers(StatisticMeasureName.NAMES.ActivePlusUsers, data)
|
||||
const totalActiveProUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveProUsers, data)
|
||||
|
||||
return ` <div>
|
||||
<p>Hello,</p>
|
||||
<p>
|
||||
<strong>Here are some statistics from yesterday:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Active Users</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Total:</b> ${totalActiveUsers.yesterday.toLocaleString('en-US')}
|
||||
</li>
|
||||
<li>
|
||||
<b>By Subscription Type:</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>FREE:</b> ${totalActiveFreeUsers.yesterday.toLocaleString('en-US')}
|
||||
</li>
|
||||
<li>
|
||||
<b>PLUS:</b> ${totalActivePlusUsers.yesterday.toLocaleString('en-US')}
|
||||
</li>
|
||||
<li>
|
||||
<b>PRO:</b> ${totalActiveProUsers.yesterday.toLocaleString('en-US')}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Payments</b>
|
||||
<ul>
|
||||
@@ -812,6 +793,28 @@ export const html = (data: any, timer: TimerInterface) => {
|
||||
<strong>Here are some statistics from last 30 days:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Active Users (Average)</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Total:</b> ${totalActiveUsers.last30Days.toLocaleString('en-US')}
|
||||
</li>
|
||||
<li>
|
||||
<b>By Subscription Type:</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>FREE:</b> ${totalActiveFreeUsers.last30Days.toLocaleString('en-US')}
|
||||
</li>
|
||||
<li>
|
||||
<b>PLUS:</b> ${totalActivePlusUsers.last30Days.toLocaleString('en-US')}
|
||||
</li>
|
||||
<li>
|
||||
<b>PRO:</b> ${totalActiveProUsers.last30Days.toLocaleString('en-US')}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Payments (This Month)</b>
|
||||
<ul>
|
||||
@@ -944,10 +947,6 @@ export const html = (data: any, timer: TimerInterface) => {
|
||||
</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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
@@ -17,6 +18,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
) {}
|
||||
|
||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||
@@ -40,5 +42,12 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
])
|
||||
|
||||
await this.analyticsEntityRepository.remove(analyticsEntity)
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsEntity.id.toString(),
|
||||
user_created_at: this.timer.convertMicrosecondsToDate(event.payload.userCreatedAtTimestamp),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
@@ -12,6 +13,7 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentFailedEvent): Promise<void> {
|
||||
@@ -21,5 +23,11 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsId.toString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
|
||||
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
@@ -83,6 +84,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentSuccessEvent): Promise<void> {
|
||||
@@ -113,5 +115,19 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsId.toString(),
|
||||
amount: event.payload.amount,
|
||||
billing_frequency: event.payload.billingFrequency,
|
||||
payment_type: event.payload.paymentType,
|
||||
subscription_name: event.payload.subscriptionName,
|
||||
})
|
||||
|
||||
this.mixpanelClient.people.track_charge(analyticsId.toString(), event.payload.amount)
|
||||
|
||||
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
@injectable()
|
||||
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {}
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
) {}
|
||||
|
||||
async handle(event: RefundProcessedEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
|
||||
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsId.toString(),
|
||||
amount: event.payload.amount,
|
||||
})
|
||||
this.mixpanelClient.people.track_charge(analyticsId.toString(), -event.payload.amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { DomainEventHandlerInterface, StatisticPersistenceRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
import { PersistStatistic } from '../UseCase/PersistStatistic/PersistStatistic'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
export class StatisticPersistenceRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private persistStatistic: PersistStatistic, private logger: Logger) {}
|
||||
constructor(
|
||||
private persistStatistic: PersistStatistic,
|
||||
private timer: TimerInterface,
|
||||
private logger: Logger,
|
||||
private mixpanelClient: Mixpanel | null,
|
||||
) {}
|
||||
|
||||
async handle(event: StatisticPersistenceRequestedEvent): Promise<void> {
|
||||
const result = await this.persistStatistic.execute({
|
||||
date: event.payload.date,
|
||||
date: this.timer.convertMicrosecondsToDate(event.payload.date),
|
||||
statisticMeasureName: event.payload.statisticMeasureName,
|
||||
value: event.payload.value,
|
||||
})
|
||||
@@ -15,5 +22,13 @@ export class StatisticPersistenceRequestedEventHandler implements DomainEventHan
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(result.getError())
|
||||
}
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: 'global-stats',
|
||||
statistic: event.payload.statisticMeasureName,
|
||||
value: event.payload.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
@@ -13,6 +14,7 @@ import { Period } from '../Time/Period'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
||||
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -22,6 +24,8 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||
@@ -50,6 +54,22 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsId.toString(),
|
||||
subscription_name: event.payload.subscriptionName,
|
||||
subscription_created_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionCreatedAt),
|
||||
subscription_updated_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionUpdatedAt),
|
||||
last_payed_at: this.timer.convertMicrosecondsToDate(event.payload.lastPayedAt),
|
||||
subscription_ends_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionEndsAt),
|
||||
offline: event.payload.offline,
|
||||
replaced: event.payload.replaced,
|
||||
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
|
||||
billing_frequency: event.payload.billingFrequency,
|
||||
pay_amount: event.payload.payAmount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
@@ -22,6 +23,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
||||
@@ -54,5 +56,19 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsId.toString(),
|
||||
subscription_name: event.payload.subscriptionName,
|
||||
offline: event.payload.offline,
|
||||
total_active_subscriptions_count: event.payload.totalActiveSubscriptionsCount,
|
||||
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
|
||||
billing_frequency: event.payload.billingFrequency,
|
||||
pay_amount: event.payload.payAmount,
|
||||
})
|
||||
|
||||
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
@@ -22,6 +24,8 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
||||
@@ -78,5 +82,23 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsId.toString(),
|
||||
subscription_name: event.payload.subscriptionName,
|
||||
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
|
||||
offline: event.payload.offline,
|
||||
discount_code: event.payload.discountCode,
|
||||
limited_discount_purchased: event.payload.limitedDiscountPurchased,
|
||||
new_subscriber: event.payload.newSubscriber,
|
||||
total_active_subscriptions_count: event.payload.totalActiveSubscriptionsCount,
|
||||
user_registered_at: this.timer.convertMicrosecondsToDate(event.payload.userRegisteredAt),
|
||||
billing_frequency: event.payload.billingFrequency,
|
||||
pay_amount: event.payload.payAmount,
|
||||
})
|
||||
|
||||
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
@@ -12,6 +14,8 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
|
||||
constructor(
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
|
||||
@@ -21,5 +25,16 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsId.toString(),
|
||||
subscription_name: event.payload.subscriptionName,
|
||||
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
|
||||
discount_code: event.payload.discountCode,
|
||||
})
|
||||
|
||||
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
@@ -22,6 +23,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
||||
@@ -50,6 +52,19 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsId.toString(),
|
||||
subscription_name: event.payload.subscriptionName,
|
||||
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
|
||||
total_active_subscriptions_count: event.payload.totalActiveSubscriptionsCount,
|
||||
offline: event.payload.offline,
|
||||
billing_frequency: event.payload.billingFrequency,
|
||||
pay_amount: event.payload.payAmount,
|
||||
})
|
||||
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
|
||||
}
|
||||
}
|
||||
|
||||
private async markChurnActivity(analyticsId: number, event: SubscriptionRefundedEvent): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
@@ -11,6 +12,7 @@ import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/Save
|
||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||
import { Logger } from 'winston'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -19,6 +21,8 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
||||
@@ -50,5 +54,17 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsId.toString(),
|
||||
subscription_name: event.payload.subscriptionName,
|
||||
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
|
||||
offline: event.payload.offline,
|
||||
billing_frequency: event.payload.billingFrequency,
|
||||
pay_amount: event.payload.payAmount,
|
||||
})
|
||||
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { inject, injectable, optional } from 'inversify'
|
||||
import { Mixpanel } from 'mixpanel'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
@@ -13,6 +14,7 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||
) {}
|
||||
|
||||
async handle(event: UserRegisteredEvent): Promise<void> {
|
||||
@@ -26,5 +28,17 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsEntity.id.toString(),
|
||||
protocol_version: event.payload.protocolVersion,
|
||||
})
|
||||
|
||||
this.mixpanelClient.people.set(analyticsEntity.id.toString(), {
|
||||
subscription: 'free',
|
||||
protocol_version: event.payload.protocolVersion,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ export class StatisticMeasureName extends ValueObject<StatisticMeasureNameProps>
|
||||
FiveYearPlansMRR: 'five-year-plans-mrr',
|
||||
ProPlansMRR: 'pro-plans-mrr',
|
||||
PlusPlansMRR: 'plus-plans-mrr',
|
||||
ActiveUsers: 'active-users',
|
||||
ActiveProUsers: 'active-pro-users',
|
||||
ActivePlusUsers: 'active-plus-users',
|
||||
ActiveFreeUsers: 'active-free-users',
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.44.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.43.0...@standardnotes/api-gateway@1.44.0) (2022-12-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add removing authenticator ([de50d76](https://github.com/standardnotes/api-gateway/commit/de50d76800a4240729763b2df11c4a1718951670))
|
||||
|
||||
# [1.43.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.42.0...@standardnotes/api-gateway@1.43.0) (2022-12-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add listing authenticators ([01837ea](https://github.com/standardnotes/api-gateway/commit/01837eaea9b1f219e7ad3be4d28cd0df099fe423))
|
||||
|
||||
# [1.42.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.3...@standardnotes/api-gateway@1.42.0) (2022-12-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add http endpoints for authenticators ([b6fda90](https://github.com/standardnotes/api-gateway/commit/b6fda901ef66a3e66541bd1e3f041b8268a1c3f5))
|
||||
|
||||
## [1.41.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.2...@standardnotes/api-gateway@1.41.3) (2022-12-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.41.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.1...@standardnotes/api-gateway@1.41.2) (2022-12-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.41.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.0...@standardnotes/api-gateway@1.41.1) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -21,6 +21,7 @@ import '../src/Controller/v1/FilesController'
|
||||
import '../src/Controller/v1/SubscriptionInvitesController'
|
||||
import '../src/Controller/v1/WorkspacesController'
|
||||
import '../src/Controller/v1/InvitesController'
|
||||
import '../src/Controller/v1/AuthenticatorsController'
|
||||
|
||||
import '../src/Controller/v2/PaymentsControllerV2'
|
||||
import '../src/Controller/v2/ActionsControllerV2'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.41.1",
|
||||
"version": "1.44.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.27.0",
|
||||
"@sentry/node": "^7.28.1",
|
||||
"@standardnotes/common": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { inject } from 'inversify'
|
||||
import { Request, Response } from 'express'
|
||||
import { controller, BaseHttpController, httpPost, httpGet, httpDelete } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/authenticators', TYPES.AuthMiddleware)
|
||||
export class AuthenticatorsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpDelete('/:authenticatorId')
|
||||
async delete(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`authenticators/${request.params.authenticatorId}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/')
|
||||
async list(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'authenticators/', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/generate-registration-options')
|
||||
async generateRegistrationOptions(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
'authenticators/generate-registration-options',
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/generate-authentication-options')
|
||||
async generateAuthenticationOptions(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
'authenticators/generate-authentication-options',
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/verify-registration')
|
||||
async verifyRegistration(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'authenticators/verify-registration', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/verify-authentication')
|
||||
async verifyAuthentication(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'authenticators/verify-authentication', request.body)
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ ENCRYPTION_SERVER_KEY=change-me-!
|
||||
|
||||
PORT=3000
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_REPLICA_HOST=localhost
|
||||
DB_HOST=127.0.0.1
|
||||
DB_REPLICA_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=auth
|
||||
DB_PASSWORD=changeme123
|
||||
|
||||
@@ -3,6 +3,142 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.79.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.78.0...@standardnotes/auth-server@1.79.0) (2022-12-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add removing authenticator ([de50d76](https://github.com/standardnotes/server/commit/de50d76800a4240729763b2df11c4a1718951670))
|
||||
|
||||
# [1.78.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.77.1...@standardnotes/auth-server@1.78.0) (2022-12-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add listing authenticators ([01837ea](https://github.com/standardnotes/server/commit/01837eaea9b1f219e7ad3be4d28cd0df099fe423))
|
||||
|
||||
## [1.77.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.77.0...@standardnotes/auth-server@1.77.1) (2022-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** specs for verifying authenticator authentication response ([5455972](https://github.com/standardnotes/server/commit/5455972be2c62d7862c351b1328beacf4bd5c3da))
|
||||
|
||||
# [1.77.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.76.0...@standardnotes/auth-server@1.77.0) (2022-12-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add http endpoints for authenticators ([b6fda90](https://github.com/standardnotes/server/commit/b6fda901ef66a3e66541bd1e3f041b8268a1c3f5))
|
||||
|
||||
# [1.76.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.75.0...@standardnotes/auth-server@1.76.0) (2022-12-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add verifying authenticator authentication response ([64525a6](https://github.com/standardnotes/server/commit/64525a65f2e1677f942868903f318d6700c34c74))
|
||||
|
||||
# [1.75.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.74.1...@standardnotes/auth-server@1.75.0) (2022-12-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add generating authenticator authentication options ([8c7c1e4](https://github.com/standardnotes/server/commit/8c7c1e4745647004f3dc361ec374014390952486))
|
||||
|
||||
## [1.74.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.74.0...@standardnotes/auth-server@1.74.1) (2022-12-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** migrations to not include unique index for credentials id ([384dfc8](https://github.com/standardnotes/server/commit/384dfc8da4b1b640964fa6da207a67fcd68dc7ec))
|
||||
|
||||
# [1.74.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.73.1...@standardnotes/auth-server@1.74.0) (2022-12-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add verifying authenticator registration response ([f5683cf](https://github.com/standardnotes/server/commit/f5683cfd9494db8e25010e9c4ef5fd4d8fcd6bc7))
|
||||
|
||||
## [1.73.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.73.0...@standardnotes/auth-server@1.73.1) (2022-12-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** temporarily remove credential id index due to mysql 5.6 limitations ([a5e7132](https://github.com/standardnotes/server/commit/a5e7132d3c4b74ed13877d7d437062c509201874))
|
||||
|
||||
# [1.73.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.72.0...@standardnotes/auth-server@1.73.0) (2022-12-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add generating authencator registration options ([51ad06b](https://github.com/standardnotes/server/commit/51ad06b303d7dc994920818872fdf8bd37fc445c))
|
||||
|
||||
# [1.72.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.71.1...@standardnotes/auth-server@1.72.0) (2022-12-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add authenticator challenges model ([fa9bf0b](https://github.com/standardnotes/server/commit/fa9bf0b448acb3f19ab44c4b431ce367dab37b76))
|
||||
|
||||
## [1.71.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.71.0...@standardnotes/auth-server@1.71.1) (2022-12-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** credential id field type ([98e3d18](https://github.com/standardnotes/server/commit/98e3d1833530dcd9e3e34a4c4a6b14a2a01afea1))
|
||||
|
||||
# [1.71.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.9...@standardnotes/auth-server@1.71.0) (2022-12-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add authenticators model ([1e69a13](https://github.com/standardnotes/server/commit/1e69a13a97c4d9022aa96397cce1b349d3cede89))
|
||||
|
||||
## [1.70.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.8...@standardnotes/auth-server@1.70.9) (2022-12-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.70.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.7...@standardnotes/auth-server@1.70.8) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** move tracing sessions to session creation instead of cross service token creation ([5255cfb](https://github.com/standardnotes/server/commit/5255cfbb257cc9e6ac437fe0c5b28d938e3e599b))
|
||||
|
||||
## [1.70.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.6...@standardnotes/auth-server@1.70.7) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** change severity on tracing session errors - most probably hazardous reads ([cf0b918](https://github.com/standardnotes/server/commit/cf0b91891370e1c1799ad80c10ee9f6b98087a94))
|
||||
|
||||
## [1.70.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.5...@standardnotes/auth-server@1.70.6) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** query for session traces ([14eb775](https://github.com/standardnotes/server/commit/14eb775749bfa9972dc3c07049505f3d15f0b556))
|
||||
|
||||
## [1.70.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.4...@standardnotes/auth-server@1.70.5) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add session traces index ([b9e1e47](https://github.com/standardnotes/server/commit/b9e1e4787129f00fab8f98cb721141f2e7d75600))
|
||||
|
||||
## [1.70.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.3...@standardnotes/auth-server@1.70.4) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
|
||||
|
||||
## [1.70.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.2...@standardnotes/auth-server@1.70.3) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
|
||||
|
||||
## [1.70.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.1...@standardnotes/auth-server@1.70.2) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** docker command ([85d2f42](https://github.com/standardnotes/server/commit/85d2f42f473110e8dfca975bfecc7a56823bdef4))
|
||||
|
||||
## [1.70.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.0...@standardnotes/auth-server@1.70.1) (2022-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** saving subscription plan name in session traces ([3064d03](https://github.com/standardnotes/server/commit/3064d03aa9a2ac9ca3acfff30480ea8629faeb14))
|
||||
|
||||
# [1.70.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.1...@standardnotes/auth-server@1.70.0) (2022-12-19)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
|
||||
|
||||
## [1.69.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.0...@standardnotes/auth-server@1.69.1) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -21,6 +21,7 @@ import '../src/Controller/ListedController'
|
||||
import '../src/Controller/SubscriptionSettingsController'
|
||||
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'
|
||||
|
||||
40
packages/auth/bin/stats.ts
Normal file
40
packages/auth/bin/stats.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import { PersistStatistics } from '../src/Domain/UseCase/PersistStatistics/PersistStatistics'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
|
||||
logger.info('Starting session traces cleanup')
|
||||
|
||||
const persistStats: PersistStatistics = container.get(TYPES.PersistStatistics)
|
||||
const timer: TimerInterface = container.get(TYPES.Timer)
|
||||
|
||||
Promise.resolve(
|
||||
persistStats.execute({
|
||||
sessionsInADay: timer.getUTCDateNDaysAgo(1),
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
logger.info('Stats persisted.')
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not persist stats: ${error.message}`)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
@@ -24,6 +24,11 @@ case "$COMMAND" in
|
||||
yarn workspace @standardnotes/auth-server cleanup
|
||||
;;
|
||||
|
||||
'stats' )
|
||||
echo "[Docker] Starting Persisting Stats..."
|
||||
yarn workspace @standardnotes/auth-server stats
|
||||
;;
|
||||
|
||||
'email-daily-backup' )
|
||||
echo "[Docker] Starting Email Daily Backup..."
|
||||
yarn workspace @standardnotes/auth-server daily-backup:email
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addSessionTracesCompoundIndex1671561748264 implements MigrationInterface {
|
||||
name = 'addSessionTracesCompoundIndex1671561748264'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE UNIQUE INDEX `user_uuid_and_creation_date` ON `session_traces` (`user_uuid`, `creation_date`)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `user_uuid_and_creation_date` ON `session_traces`')
|
||||
}
|
||||
}
|
||||
15
packages/auth/migrations/1672223738686-add_authenticators.ts
Normal file
15
packages/auth/migrations/1672223738686-add_authenticators.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addAuthenticators1672223738686 implements MigrationInterface {
|
||||
name = 'addAuthenticators1672223738686'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE `authenticators` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `credential_id` varbinary(1024) NOT NULL, `credential_public_key` blob NOT NULL, `counter` bigint NOT NULL, `credential_device_type` varchar(32) NOT NULL, `credential_backed_up` tinyint NOT NULL, `transports` varchar(255) NULL, `created_at` bigint NOT NULL, `updated_at` bigint NOT NULL, PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP TABLE `authentticators`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addAuthenticatorChallenges1672227471677 implements MigrationInterface {
|
||||
name = 'addAuthenticatorChallenges1672227471677'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE `authenticator_challenges` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `challenge` varchar(255) NOT NULL, `created_at` bigint NOT NULL, INDEX `user_uuid_and_challenge` (`user_uuid`, `challenge`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `user_uuid_and_challenge` ON `authenticator_challenges`')
|
||||
await queryRunner.query('DROP TABLE `authenticator_challenges`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class fixAuthenticatorDataTypes1672232035280 implements MigrationInterface {
|
||||
name = 'fixAuthenticatorDataTypes1672232035280'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `created_at`')
|
||||
await queryRunner.query('ALTER TABLE `authenticators` ADD `created_at` datetime NOT NULL')
|
||||
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `updated_at`')
|
||||
await queryRunner.query('ALTER TABLE `authenticators` ADD `updated_at` datetime NOT NULL')
|
||||
await queryRunner.query('ALTER TABLE `authenticator_challenges` DROP COLUMN `created_at`')
|
||||
await queryRunner.query('ALTER TABLE `authenticator_challenges` ADD `created_at` datetime NOT NULL')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `authenticator_challenges` DROP COLUMN `created_at`')
|
||||
await queryRunner.query('ALTER TABLE `authenticator_challenges` ADD `created_at` bigint NOT NULL')
|
||||
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `updated_at`')
|
||||
await queryRunner.query('ALTER TABLE `authenticators` ADD `updated_at` bigint NOT NULL')
|
||||
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `created_at`')
|
||||
await queryRunner.query('ALTER TABLE `authenticators` ADD `created_at` bigint NOT NULL')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addUniqueIndexOnChallenges1672299743840 implements MigrationInterface {
|
||||
name = 'addUniqueIndexOnChallenges1672299743840'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `user_uuid_and_challenge` ON `authenticator_challenges`')
|
||||
await queryRunner.query('CREATE UNIQUE INDEX `unique_user_uuid` ON `authenticator_challenges` (`user_uuid`)')
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX `user_uuid_and_challenge` ON `authenticator_challenges` (`user_uuid`, `challenge`)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `user_uuid_and_challenge` ON `authenticator_challenges`')
|
||||
await queryRunner.query('DROP INDEX `unique_user_uuid` ON `authenticator_challenges`')
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX `user_uuid_and_challenge` ON `authenticator_challenges` (`user_uuid`, `challenge`)',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class removeCompoundIndex1672307975117 implements MigrationInterface {
|
||||
name = 'removeCompoundIndex1672307975117'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `user_uuid_and_challenge` ON `authenticator_challenges`')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX `user_uuid_and_challenge` ON `authenticator_challenges` (`user_uuid`, `challenge`)',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addAuthenticatorName1672317378817 implements MigrationInterface {
|
||||
name = 'addAuthenticatorName1672317378817'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `authenticators` ADD `name` varchar(255) NOT NULL')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `name`')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.69.1",
|
||||
"version": "1.79.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
@@ -20,6 +20,7 @@
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"cleanup": "yarn node dist/bin/cleanup.js",
|
||||
"stats": "yarn node dist/bin/stats.js",
|
||||
"daily-backup:email": "yarn node dist/bin/backup.js email daily",
|
||||
"user-email-backup": "yarn node dist/bin/user_email_backup.js",
|
||||
"daily-backup:dropbox": "yarn node dist/bin/backup.js dropbox daily",
|
||||
@@ -32,9 +33,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.27.0",
|
||||
"@sentry/node": "^7.28.1",
|
||||
"@sentry/profiling-node": "^0.0.12",
|
||||
"@sentry/tracing": "^7.27.0",
|
||||
"@sentry/tracing": "^7.28.1",
|
||||
"@simplewebauthn/server": "^6.2.2",
|
||||
"@simplewebauthn/typescript-types": "^6.3.0-alpha.1",
|
||||
"@standardnotes/api": "^1.19.0",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
|
||||
@@ -202,6 +202,26 @@ import { SessionTrace } from '../Domain/Session/SessionTrace'
|
||||
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
|
||||
import { TraceSession } from '../Domain/UseCase/TraceSession/TraceSession'
|
||||
import { CleanupSessionTraces } from '../Domain/UseCase/CleanupSessionTraces/CleanupSessionTraces'
|
||||
import { PersistStatistics } from '../Domain/UseCase/PersistStatistics/PersistStatistics'
|
||||
import { TypeORMAuthenticator } from '../Infra/TypeORM/TypeORMAuthenticator'
|
||||
import { Authenticator } from '../Domain/Authenticator/Authenticator'
|
||||
import { AuthenticatorPersistenceMapper } from '../Mapping/AuthenticatorPersistenceMapper'
|
||||
import { AuthenticatorChallenge } from '../Domain/Authenticator/AuthenticatorChallenge'
|
||||
import { TypeORMAuthenticatorChallenge } from '../Infra/TypeORM/TypeORMAuthenticatorChallenge'
|
||||
import { AuthenticatorChallengePersistenceMapper } from '../Mapping/AuthenticatorChallengePersistenceMapper'
|
||||
import { AuthenticatorRepositoryInterface } from '../Domain/Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { MySQLAuthenticatorRepository } from '../Infra/MySQL/MySQLAuthenticatorRepository'
|
||||
import { AuthenticatorChallengeRepositoryInterface } from '../Domain/Authenticator/AuthenticatorChallengeRepositoryInterface'
|
||||
import { MySQLAuthenticatorChallengeRepository } from '../Infra/MySQL/MySQLAuthenticatorChallengeRepository'
|
||||
import { GenerateAuthenticatorRegistrationOptions } from '../Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions'
|
||||
import { VerifyAuthenticatorRegistrationResponse } from '../Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse'
|
||||
import { GenerateAuthenticatorAuthenticationOptions } from '../Domain/UseCase/GenerateAuthenticatorAuthenticationOptions/GenerateAuthenticatorAuthenticationOptions'
|
||||
import { VerifyAuthenticatorAuthenticationResponse } from '../Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse'
|
||||
import { AuthenticatorsController } from '../Controller/AuthenticatorsController'
|
||||
import { ListAuthenticators } from '../Domain/UseCase/ListAuthenticators/ListAuthenticators'
|
||||
import { AuthenticatorHttpProjection } from '../Infra/Http/Projection/AuthenticatorHttpProjection'
|
||||
import { AuthenticatorHttpMapper } from '../Mapping/AuthenticatorHttpMapper'
|
||||
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -279,11 +299,17 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<MapperInterface<SessionTrace, TypeORMSessionTrace>>(TYPES.SessionTracePersistenceMapper)
|
||||
.toConstantValue(new SessionTracePersistenceMapper())
|
||||
|
||||
// Controller
|
||||
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
|
||||
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
|
||||
container.bind<UserRequestsController>(TYPES.UserRequestsController).to(UserRequestsController)
|
||||
container
|
||||
.bind<MapperInterface<Authenticator, TypeORMAuthenticator>>(TYPES.AuthenticatorPersistenceMapper)
|
||||
.toConstantValue(new AuthenticatorPersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<Authenticator, AuthenticatorHttpProjection>>(TYPES.AuthenticatorHttpMapper)
|
||||
.toConstantValue(new AuthenticatorHttpMapper())
|
||||
container
|
||||
.bind<MapperInterface<AuthenticatorChallenge, TypeORMAuthenticatorChallenge>>(
|
||||
TYPES.AuthenticatorChallengePersistenceMapper,
|
||||
)
|
||||
.toConstantValue(new AuthenticatorChallengePersistenceMapper())
|
||||
|
||||
// ORM
|
||||
container
|
||||
@@ -315,6 +341,12 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<Repository<TypeORMSessionTrace>>(TYPES.ORMSessionTraceRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMSessionTrace))
|
||||
container
|
||||
.bind<Repository<TypeORMAuthenticator>>(TYPES.ORMAuthenticatorRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMAuthenticator))
|
||||
container
|
||||
.bind<Repository<TypeORMAuthenticatorChallenge>>(TYPES.ORMAuthenticatorChallengeRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMAuthenticatorChallenge))
|
||||
|
||||
// Repositories
|
||||
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)
|
||||
@@ -354,6 +386,22 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.SessionTracePersistenceMapper),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AuthenticatorRepositoryInterface>(TYPES.AuthenticatorRepository)
|
||||
.toConstantValue(
|
||||
new MySQLAuthenticatorRepository(
|
||||
container.get(TYPES.ORMAuthenticatorRepository),
|
||||
container.get(TYPES.AuthenticatorPersistenceMapper),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AuthenticatorChallengeRepositoryInterface>(TYPES.AuthenticatorChallengeRepository)
|
||||
.toConstantValue(
|
||||
new MySQLAuthenticatorChallengeRepository(
|
||||
container.get(TYPES.ORMAuthenticatorChallengeRepository),
|
||||
container.get(TYPES.AuthenticatorChallengePersistenceMapper),
|
||||
),
|
||||
)
|
||||
|
||||
// Middleware
|
||||
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
|
||||
@@ -500,6 +548,54 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.SESSION_TRACE_DAYS_TTL),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<PersistStatistics>(TYPES.PersistStatistics)
|
||||
.toConstantValue(
|
||||
new PersistStatistics(
|
||||
container.get(TYPES.SessionTraceRepository),
|
||||
container.get(TYPES.DomainEventPublisher),
|
||||
container.get(TYPES.DomainEventFactory),
|
||||
container.get(TYPES.Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GenerateAuthenticatorRegistrationOptions>(TYPES.GenerateAuthenticatorRegistrationOptions)
|
||||
.toConstantValue(
|
||||
new GenerateAuthenticatorRegistrationOptions(
|
||||
container.get(TYPES.AuthenticatorRepository),
|
||||
container.get(TYPES.AuthenticatorChallengeRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<VerifyAuthenticatorRegistrationResponse>(TYPES.VerifyAuthenticatorRegistrationResponse)
|
||||
.toConstantValue(
|
||||
new VerifyAuthenticatorRegistrationResponse(
|
||||
container.get(TYPES.AuthenticatorRepository),
|
||||
container.get(TYPES.AuthenticatorChallengeRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GenerateAuthenticatorAuthenticationOptions>(TYPES.GenerateAuthenticatorAuthenticationOptions)
|
||||
.toConstantValue(
|
||||
new GenerateAuthenticatorAuthenticationOptions(
|
||||
container.get(TYPES.AuthenticatorRepository),
|
||||
container.get(TYPES.AuthenticatorChallengeRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<VerifyAuthenticatorAuthenticationResponse>(TYPES.VerifyAuthenticatorAuthenticationResponse)
|
||||
.toConstantValue(
|
||||
new VerifyAuthenticatorAuthenticationResponse(
|
||||
container.get(TYPES.AuthenticatorRepository),
|
||||
container.get(TYPES.AuthenticatorChallengeRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ListAuthenticators>(TYPES.ListAuthenticators)
|
||||
.toConstantValue(new ListAuthenticators(container.get(TYPES.AuthenticatorRepository)))
|
||||
container
|
||||
.bind<DeleteAuthenticator>(TYPES.DeleteAuthenticator)
|
||||
.toConstantValue(new DeleteAuthenticator(container.get(TYPES.AuthenticatorRepository)))
|
||||
|
||||
container
|
||||
.bind<CleanupSessionTraces>(TYPES.CleanupSessionTraces)
|
||||
@@ -554,6 +650,24 @@ export class ContainerConfigLoader {
|
||||
container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
|
||||
container.bind<ProcessUserRequest>(TYPES.ProcessUserRequest).to(ProcessUserRequest)
|
||||
|
||||
// Controller
|
||||
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
|
||||
container
|
||||
.bind<AuthenticatorsController>(TYPES.AuthenticatorsController)
|
||||
.toConstantValue(
|
||||
new AuthenticatorsController(
|
||||
container.get(TYPES.GenerateAuthenticatorRegistrationOptions),
|
||||
container.get(TYPES.VerifyAuthenticatorRegistrationResponse),
|
||||
container.get(TYPES.GenerateAuthenticatorAuthenticationOptions),
|
||||
container.get(TYPES.VerifyAuthenticatorAuthenticationResponse),
|
||||
container.get(TYPES.ListAuthenticators),
|
||||
container.get(TYPES.DeleteAuthenticator),
|
||||
container.get(TYPES.AuthenticatorHttpMapper),
|
||||
),
|
||||
)
|
||||
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
|
||||
container.bind<UserRequestsController>(TYPES.UserRequestsController).to(UserRequestsController)
|
||||
|
||||
// Handlers
|
||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||
container
|
||||
|
||||
@@ -10,6 +10,8 @@ import { SharedSubscriptionInvitation } from '../Domain/SharedSubscription/Share
|
||||
import { OfflineUserSubscription } from '../Domain/Subscription/OfflineUserSubscription'
|
||||
import { UserSubscription } from '../Domain/Subscription/UserSubscription'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { TypeORMAuthenticator } from '../Infra/TypeORM/TypeORMAuthenticator'
|
||||
import { TypeORMAuthenticatorChallenge } from '../Infra/TypeORM/TypeORMAuthenticatorChallenge'
|
||||
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
|
||||
import { Env } from './Env'
|
||||
|
||||
@@ -58,6 +60,8 @@ export const AppDataSource = new DataSource({
|
||||
SharedSubscriptionInvitation,
|
||||
SubscriptionSetting,
|
||||
TypeORMSessionTrace,
|
||||
TypeORMAuthenticator,
|
||||
TypeORMAuthenticatorChallenge,
|
||||
],
|
||||
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
|
||||
migrationsRun: true,
|
||||
|
||||
@@ -5,8 +5,12 @@ const TYPES = {
|
||||
SQS: Symbol.for('SQS'),
|
||||
// Mapping
|
||||
SessionTracePersistenceMapper: Symbol.for('SessionTracePersistenceMapper'),
|
||||
AuthenticatorChallengePersistenceMapper: Symbol.for('AuthenticatorChallengePersistenceMapper'),
|
||||
AuthenticatorPersistenceMapper: Symbol.for('AuthenticatorPersistenceMapper'),
|
||||
AuthenticatorHttpMapper: Symbol.for('AuthenticatorHttpMapper'),
|
||||
// Controller
|
||||
AuthController: Symbol.for('AuthController'),
|
||||
AuthenticatorsController: Symbol.for('AuthenticatorsController'),
|
||||
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
|
||||
UserRequestsController: Symbol.for('UserRequestsController'),
|
||||
// Repositories
|
||||
@@ -26,6 +30,8 @@ const TYPES = {
|
||||
SharedSubscriptionInvitationRepository: Symbol.for('SharedSubscriptionInvitationRepository'),
|
||||
PKCERepository: Symbol.for('PKCERepository'),
|
||||
SessionTraceRepository: Symbol.for('SessionTraceRepository'),
|
||||
AuthenticatorRepository: Symbol.for('AuthenticatorRepository'),
|
||||
AuthenticatorChallengeRepository: Symbol.for('AuthenticatorChallengeRepository'),
|
||||
// ORM
|
||||
ORMOfflineSettingRepository: Symbol.for('ORMOfflineSettingRepository'),
|
||||
ORMOfflineUserSubscriptionRepository: Symbol.for('ORMOfflineUserSubscriptionRepository'),
|
||||
@@ -38,6 +44,8 @@ const TYPES = {
|
||||
ORMUserRepository: Symbol.for('ORMUserRepository'),
|
||||
ORMUserSubscriptionRepository: Symbol.for('ORMUserSubscriptionRepository'),
|
||||
ORMSessionTraceRepository: Symbol.for('ORMSessionTraceRepository'),
|
||||
ORMAuthenticatorRepository: Symbol.for('ORMAuthenticatorRepository'),
|
||||
ORMAuthenticatorChallengeRepository: Symbol.for('ORMAuthenticatorChallengeRepository'),
|
||||
// Middleware
|
||||
AuthMiddleware: Symbol.for('AuthMiddleware'),
|
||||
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),
|
||||
@@ -126,6 +134,13 @@ const TYPES = {
|
||||
ProcessUserRequest: Symbol.for('ProcessUserRequest'),
|
||||
TraceSession: Symbol.for('TraceSession'),
|
||||
CleanupSessionTraces: Symbol.for('CleanupSessionTraces'),
|
||||
PersistStatistics: Symbol.for('PersistStatistics'),
|
||||
GenerateAuthenticatorRegistrationOptions: Symbol.for('GenerateAuthenticatorRegistrationOptions'),
|
||||
VerifyAuthenticatorRegistrationResponse: Symbol.for('VerifyAuthenticatorRegistrationResponse'),
|
||||
GenerateAuthenticatorAuthenticationOptions: Symbol.for('GenerateAuthenticatorAuthenticationOptions'),
|
||||
VerifyAuthenticatorAuthenticationResponse: Symbol.for('VerifyAuthenticatorAuthenticationResponse'),
|
||||
ListAuthenticators: Symbol.for('ListAuthenticators'),
|
||||
DeleteAuthenticator: Symbol.for('DeleteAuthenticator'),
|
||||
// Handlers
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
|
||||
164
packages/auth/src/Controller/AuthenticatorsController.ts
Normal file
164
packages/auth/src/Controller/AuthenticatorsController.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { HttpStatusCode } from '@standardnotes/api'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { Authenticator } from '../Domain/Authenticator/Authenticator'
|
||||
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
|
||||
|
||||
import { GenerateAuthenticatorAuthenticationOptions } from '../Domain/UseCase/GenerateAuthenticatorAuthenticationOptions/GenerateAuthenticatorAuthenticationOptions'
|
||||
import { GenerateAuthenticatorRegistrationOptions } from '../Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions'
|
||||
import { ListAuthenticators } from '../Domain/UseCase/ListAuthenticators/ListAuthenticators'
|
||||
import { VerifyAuthenticatorAuthenticationResponse } from '../Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse'
|
||||
import { VerifyAuthenticatorRegistrationResponse } from '../Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse'
|
||||
import { AuthenticatorHttpProjection } from '../Infra/Http/Projection/AuthenticatorHttpProjection'
|
||||
import { DeleteAuthenticatorRequestParams } from '../Infra/Http/Request/DeleteAuthenticatorRequestParams'
|
||||
import { GenerateAuthenticatorAuthenticationOptionsRequestParams } from '../Infra/Http/Request/GenerateAuthenticatorAuthenticationOptionsRequestParams'
|
||||
import { GenerateAuthenticatorRegistrationOptionsRequestParams } from '../Infra/Http/Request/GenerateAuthenticatorRegistrationOptionsRequestParams'
|
||||
import { ListAuthenticatorsRequestParams } from '../Infra/Http/Request/ListAuthenticatorsRequestParams'
|
||||
import { VerifyAuthenticatorAuthenticationResponseRequestParams } from '../Infra/Http/Request/VerifyAuthenticatorAuthenticationResponseRequestParams'
|
||||
import { VerifyAuthenticatorRegistrationResponseRequestParams } from '../Infra/Http/Request/VerifyAuthenticatorRegistrationResponseRequestParams'
|
||||
import { DeleteAuthenticatorResponse } from '../Infra/Http/Response/DeleteAuthenticatorResponse'
|
||||
import { GenerateAuthenticatorAuthenticationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponse'
|
||||
import { GenerateAuthenticatorRegistrationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponse'
|
||||
import { ListAuthenticatorsResponse } from '../Infra/Http/Response/ListAuthenticatorsResponse'
|
||||
import { VerifyAuthenticatorAuthenticationResponseResponse } from '../Infra/Http/Response/VerifyAuthenticatorAuthenticationResponseResponse'
|
||||
import { VerifyAuthenticatorRegistrationResponseResponse } from '../Infra/Http/Response/VerifyAuthenticatorRegistrationResponseResponse'
|
||||
|
||||
export class AuthenticatorsController {
|
||||
constructor(
|
||||
private generateAuthenticatorRegistrationOptions: GenerateAuthenticatorRegistrationOptions,
|
||||
private verifyAuthenticatorRegistrationResponse: VerifyAuthenticatorRegistrationResponse,
|
||||
private generateAuthenticatorAuthenticationOptions: GenerateAuthenticatorAuthenticationOptions,
|
||||
private verifyAuthenticatorAuthenticationResponse: VerifyAuthenticatorAuthenticationResponse,
|
||||
private listAuthenticators: ListAuthenticators,
|
||||
private deleteAuthenticator: DeleteAuthenticator,
|
||||
private authenticatorHttpMapper: MapperInterface<Authenticator, AuthenticatorHttpProjection>,
|
||||
) {}
|
||||
|
||||
async list(params: ListAuthenticatorsRequestParams): Promise<ListAuthenticatorsResponse> {
|
||||
const result = await this.listAuthenticators.execute({
|
||||
userUuid: params.userUuid,
|
||||
})
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: {
|
||||
authenticators: result
|
||||
.getValue()
|
||||
.map((authenticator) => this.authenticatorHttpMapper.toProjection(authenticator)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async delete(params: DeleteAuthenticatorRequestParams): Promise<DeleteAuthenticatorResponse> {
|
||||
const result = await this.deleteAuthenticator.execute({
|
||||
userUuid: params.userUuid,
|
||||
authenticatorId: params.authenticatorId,
|
||||
})
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: {
|
||||
message: result.getValue(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async generateRegistrationOptions(
|
||||
params: GenerateAuthenticatorRegistrationOptionsRequestParams,
|
||||
): Promise<GenerateAuthenticatorRegistrationOptionsResponse> {
|
||||
const result = await this.generateAuthenticatorRegistrationOptions.execute({
|
||||
userUuid: params.userUuid,
|
||||
username: params.username,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: result.getError(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: { options: result.getValue() },
|
||||
}
|
||||
}
|
||||
|
||||
async verifyRegistrationResponse(
|
||||
params: VerifyAuthenticatorRegistrationResponseRequestParams,
|
||||
): Promise<VerifyAuthenticatorRegistrationResponseResponse> {
|
||||
const result = await this.verifyAuthenticatorRegistrationResponse.execute({
|
||||
userUuid: params.userUuid,
|
||||
name: params.name,
|
||||
registrationCredential: params.registrationCredential,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return {
|
||||
status: HttpStatusCode.Unauthorized,
|
||||
data: {
|
||||
error: {
|
||||
message: result.getError(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: { success: result.getValue() },
|
||||
}
|
||||
}
|
||||
|
||||
async generateAuthenticationOptions(
|
||||
params: GenerateAuthenticatorAuthenticationOptionsRequestParams,
|
||||
): Promise<GenerateAuthenticatorAuthenticationOptionsResponse> {
|
||||
const result = await this.generateAuthenticatorAuthenticationOptions.execute({
|
||||
userUuid: params.userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: result.getError(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: { options: result.getValue() },
|
||||
}
|
||||
}
|
||||
|
||||
async verifyAuthenticationResponse(
|
||||
params: VerifyAuthenticatorAuthenticationResponseRequestParams,
|
||||
): Promise<VerifyAuthenticatorAuthenticationResponseResponse> {
|
||||
const result = await this.verifyAuthenticatorAuthenticationResponse.execute({
|
||||
userUuid: params.userUuid,
|
||||
authenticationCredential: params.authenticationCredential,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return {
|
||||
status: HttpStatusCode.Unauthorized,
|
||||
data: {
|
||||
error: {
|
||||
message: result.getError(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: { success: result.getValue() },
|
||||
}
|
||||
}
|
||||
}
|
||||
22
packages/auth/src/Domain/Authenticator/Authenticator.spec.ts
Normal file
22
packages/auth/src/Domain/Authenticator/Authenticator.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Dates, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Authenticator } from './Authenticator'
|
||||
|
||||
describe('Authenticator', () => {
|
||||
it('should create an entity', () => {
|
||||
const entityOrError = Authenticator.create({
|
||||
counter: 1,
|
||||
name: 'my-key',
|
||||
credentialBackedUp: true,
|
||||
credentialDeviceType: 'singleDevice',
|
||||
credentialId: Buffer.from('credentialId'),
|
||||
credentialPublicKey: Buffer.from('credentialPublicKey'),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
dates: Dates.create(new Date(1), new Date(1)).getValue(),
|
||||
transports: ['usb'],
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(entityOrError.getValue().id).not.toBeNull()
|
||||
})
|
||||
})
|
||||
17
packages/auth/src/Domain/Authenticator/Authenticator.ts
Normal file
17
packages/auth/src/Domain/Authenticator/Authenticator.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
import { AuthenticatorProps } from './AuthenticatorProps'
|
||||
|
||||
export class Authenticator extends Entity<AuthenticatorProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: AuthenticatorProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
static create(props: AuthenticatorProps, id?: UniqueEntityId): Result<Authenticator> {
|
||||
return Result.ok<Authenticator>(new Authenticator(props, id))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { AuthenticatorChallenge } from './AuthenticatorChallenge'
|
||||
|
||||
describe('AuthenticatorChallenge', () => {
|
||||
it('should create an entity', () => {
|
||||
const entityOrError = AuthenticatorChallenge.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
createdAt: new Date(1),
|
||||
challenge: Buffer.from('challenge'),
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(entityOrError.getValue().id).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
import { AuthenticatorChallengeProps } from './AuthenticatorChallengeProps'
|
||||
|
||||
export class AuthenticatorChallenge extends Entity<AuthenticatorChallengeProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: AuthenticatorChallengeProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
static create(props: AuthenticatorChallengeProps, id?: UniqueEntityId): Result<AuthenticatorChallenge> {
|
||||
return Result.ok<AuthenticatorChallenge>(new AuthenticatorChallenge(props, id))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface AuthenticatorChallengeProps {
|
||||
userUuid: Uuid
|
||||
challenge: Buffer
|
||||
createdAt: Date
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { AuthenticatorChallenge } from './AuthenticatorChallenge'
|
||||
|
||||
export interface AuthenticatorChallengeRepositoryInterface {
|
||||
findByUserUuid(userUuid: Uuid): Promise<AuthenticatorChallenge | null>
|
||||
save(authenticatorChallenge: AuthenticatorChallenge): Promise<void>
|
||||
}
|
||||
13
packages/auth/src/Domain/Authenticator/AuthenticatorProps.ts
Normal file
13
packages/auth/src/Domain/Authenticator/AuthenticatorProps.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Dates, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface AuthenticatorProps {
|
||||
name: string
|
||||
userUuid: Uuid
|
||||
credentialId: Buffer
|
||||
credentialPublicKey: Buffer
|
||||
counter: number
|
||||
credentialDeviceType: string
|
||||
credentialBackedUp: boolean
|
||||
transports?: string[]
|
||||
dates: Dates
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Authenticator } from './Authenticator'
|
||||
|
||||
export interface AuthenticatorRepositoryInterface {
|
||||
findByUserUuid(userUuid: Uuid): Promise<Authenticator[]>
|
||||
findById(id: UniqueEntityId): Promise<Authenticator | null>
|
||||
findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: Buffer): Promise<Authenticator | null>
|
||||
save(authenticator: Authenticator): Promise<void>
|
||||
remove(authenticator: Authenticator): Promise<void>
|
||||
}
|
||||
4
packages/auth/src/Domain/Authenticator/RelyingParty.ts
Normal file
4
packages/auth/src/Domain/Authenticator/RelyingParty.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum RelyingParty {
|
||||
RP_NAME = 'Standard Notes',
|
||||
RP_ID = 'standardnotes.com',
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
UserContentSizeRecalculationRequestedEvent,
|
||||
MuteEmailsSettingChangedEvent,
|
||||
EmailRequestedEvent,
|
||||
StatisticPersistenceRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
@@ -31,6 +32,25 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
|
||||
createStatisticPersistenceRequestedEvent(dto: {
|
||||
statisticMeasureName: string
|
||||
value: number
|
||||
date: number
|
||||
}): StatisticPersistenceRequestedEvent {
|
||||
return {
|
||||
type: 'STATISTIC_PERSISTENCE_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '-',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createMuteEmailsSettingChangedEvent(dto: {
|
||||
username: string
|
||||
mute: boolean
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
UserContentSizeRecalculationRequestedEvent,
|
||||
MuteEmailsSettingChangedEvent,
|
||||
EmailRequestedEvent,
|
||||
StatisticPersistenceRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||
|
||||
@@ -88,4 +89,9 @@ export interface DomainEventFactoryInterface {
|
||||
mute: boolean
|
||||
emailSubscriptionRejectionLevel: string
|
||||
}): MuteEmailsSettingChangedEvent
|
||||
createStatisticPersistenceRequestedEvent(dto: {
|
||||
statisticMeasureName: string
|
||||
value: number
|
||||
date: number
|
||||
}): StatisticPersistenceRequestedEvent
|
||||
}
|
||||
|
||||
@@ -41,4 +41,23 @@ describe('RoleToSubscriptionMap', () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should filter our subscription roles from an array of roles', () => {
|
||||
const roles = [
|
||||
{
|
||||
name: RoleName.CoreUser,
|
||||
} as jest.Mocked<Role>,
|
||||
{
|
||||
name: RoleName.FilesBetaUser,
|
||||
} as jest.Mocked<Role>,
|
||||
{
|
||||
name: RoleName.PlusUser,
|
||||
} as jest.Mocked<Role>,
|
||||
]
|
||||
expect(createMap().filterSubscriptionRoles(roles)).toEqual([
|
||||
{
|
||||
name: RoleName.PlusUser,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,10 @@ export class RoleToSubscriptionMap implements RoleToSubscriptionMapInterface {
|
||||
return roles.filter((role) => this.nonSubscriptionRoles.includes(role.name as RoleName))
|
||||
}
|
||||
|
||||
filterSubscriptionRoles(roles: Role[]): Array<Role> {
|
||||
return roles.filter((role) => !this.nonSubscriptionRoles.includes(role.name as RoleName))
|
||||
}
|
||||
|
||||
getSubscriptionNameForRoleName(roleName: RoleName): SubscriptionName | undefined {
|
||||
return this.roleNameToSubscriptionNameMap.get(roleName)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Role } from './Role'
|
||||
|
||||
export interface RoleToSubscriptionMapInterface {
|
||||
filterNonSubscriptionRoles(roles: Role[]): Array<Role>
|
||||
filterSubscriptionRoles(roles: Role[]): Array<Role>
|
||||
getSubscriptionNameForRoleName(roleName: RoleName): SubscriptionName | undefined
|
||||
getRoleNameForSubscriptionName(subscriptionName: SubscriptionName): RoleName | undefined
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { LogSessionUserAgentOption } from '@standardnotes/settings'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
import { CryptoNode } from '@standardnotes/sncrypto-node'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { TraceSession } from '../UseCase/TraceSession/TraceSession'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('SessionService', () => {
|
||||
let sessionRepository: SessionRepositoryInterface
|
||||
@@ -28,6 +32,8 @@ describe('SessionService', () => {
|
||||
let timer: TimerInterface
|
||||
let logger: winston.Logger
|
||||
let cryptoNode: CryptoNode
|
||||
let traceSession: TraceSession
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
|
||||
const createService = () =>
|
||||
new SessionService(
|
||||
@@ -41,6 +47,8 @@ describe('SessionService', () => {
|
||||
234,
|
||||
settingService,
|
||||
cryptoNode,
|
||||
traceSession,
|
||||
userSubscriptionRepository,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -106,6 +114,14 @@ describe('SessionService', () => {
|
||||
cryptoNode = {} as jest.Mocked<CryptoNode>
|
||||
cryptoNode.generateRandomKey = jest.fn().mockReturnValue('foo bar')
|
||||
cryptoNode.base64URLEncode = jest.fn().mockReturnValue('foobar')
|
||||
|
||||
traceSession = {} as jest.Mocked<TraceSession>
|
||||
traceSession.execute = jest.fn()
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue({
|
||||
planName: 'PRO_PLAN',
|
||||
} as jest.Mocked<UserSubscription>)
|
||||
})
|
||||
|
||||
it('should mark a revoked session as received', async () => {
|
||||
@@ -204,6 +220,129 @@ describe('SessionService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should trace a session', async () => {
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
user.email = 'test@test.te'
|
||||
|
||||
await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(traceSession.execute).toHaveBeenCalledWith({
|
||||
userUuid: '123',
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: 'PRO_PLAN',
|
||||
})
|
||||
})
|
||||
|
||||
it('should trace a session without a subscription', async () => {
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
user.email = 'test@test.te'
|
||||
|
||||
await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(traceSession.execute).toHaveBeenCalledWith({
|
||||
userUuid: '123',
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a session if tracing session throws an error', async () => {
|
||||
traceSession.execute = jest.fn().mockRejectedValue(new Error('foo bar'))
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
user.email = 'test@test.te'
|
||||
|
||||
const sessionPayload = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(traceSession.execute).toHaveBeenCalledWith({
|
||||
userUuid: '123',
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: null,
|
||||
})
|
||||
expect(sessionPayload).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
refresh_token: expect.any(String),
|
||||
readonly_access: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a session if tracing session throws an error', async () => {
|
||||
traceSession.execute = jest.fn().mockRejectedValue(new Error('foo bar'))
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
user.email = 'test@test.te'
|
||||
|
||||
const sessionPayload = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(traceSession.execute).toHaveBeenCalledWith({
|
||||
userUuid: '123',
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: null,
|
||||
})
|
||||
expect(sessionPayload).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
refresh_token: expect.any(String),
|
||||
readonly_access: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a session if tracing session fails', async () => {
|
||||
traceSession.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
user.email = 'test@test.te'
|
||||
|
||||
const sessionPayload = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(traceSession.execute).toHaveBeenCalledWith({
|
||||
userUuid: '123',
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: null,
|
||||
})
|
||||
expect(sessionPayload).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
refresh_token: expect.any(String),
|
||||
readonly_access: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create new ephemeral session for a user', async () => {
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as crypto from 'crypto'
|
||||
import * as winston from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
import { LogSessionUserAgentOption, SettingName } from '@standardnotes/settings'
|
||||
import { SessionBody } from '@standardnotes/responses'
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { CryptoNode } from '@standardnotes/sncrypto-node'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { Session } from './Session'
|
||||
@@ -16,10 +20,8 @@ import { EphemeralSession } from './EphemeralSession'
|
||||
import { RevokedSession } from './RevokedSession'
|
||||
import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { LogSessionUserAgentOption, SettingName } from '@standardnotes/settings'
|
||||
import { SessionBody } from '@standardnotes/responses'
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { CryptoNode } from '@standardnotes/sncrypto-node'
|
||||
import { TraceSession } from '../UseCase/TraceSession/TraceSession'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class SessionService implements SessionServiceInterface {
|
||||
@@ -31,11 +33,13 @@ export class SessionService implements SessionServiceInterface {
|
||||
@inject(TYPES.RevokedSessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
|
||||
@inject(TYPES.DeviceDetector) private deviceDetector: UAParser,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Logger) private logger: winston.Logger,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
@inject(TYPES.ACCESS_TOKEN_AGE) private accessTokenAge: number,
|
||||
@inject(TYPES.REFRESH_TOKEN_AGE) private refreshTokenAge: number,
|
||||
@inject(TYPES.SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.CryptoNode) private cryptoNode: CryptoNode,
|
||||
@inject(TYPES.TraceSession) private traceSession: TraceSession,
|
||||
@inject(TYPES.UserSubscriptionRepository) private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async createNewSessionForUser(dto: {
|
||||
@@ -53,6 +57,20 @@ export class SessionService implements SessionServiceInterface {
|
||||
|
||||
await this.sessionRepository.save(session)
|
||||
|
||||
try {
|
||||
const userSubscription = await this.userSubscriptionRepository.findOneByUserUuid(dto.user.uuid)
|
||||
const traceSessionResult = await this.traceSession.execute({
|
||||
userUuid: dto.user.uuid,
|
||||
username: dto.user.email,
|
||||
subscriptionPlanName: userSubscription ? userSubscription.planName : null,
|
||||
})
|
||||
if (traceSessionResult.isFailed()) {
|
||||
this.logger.error(traceSessionResult.getError())
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not trace session while creating cross service token.: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
return sessionPayload
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { SubscriptionPlanName, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { SessionTrace } from './SessionTrace'
|
||||
|
||||
@@ -6,4 +6,6 @@ export interface SessionTraceRepositoryInterface {
|
||||
save(sessionTrace: SessionTrace): Promise<void>
|
||||
removeExpiredBefore(date: Date): Promise<void>
|
||||
findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null>
|
||||
countByDate(date: Date): Promise<number>
|
||||
countByDateAndSubscriptionPlanName(date: Date, subscriptionPlanName: SubscriptionPlanName): Promise<number>
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@ import { Role } from '../../Role/Role'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
|
||||
import { RoleToSubscriptionMapInterface } from '../../Role/RoleToSubscriptionMapInterface'
|
||||
import { TraceSession } from '../TraceSession/TraceSession'
|
||||
import { Logger } from 'winston'
|
||||
import { Result, RoleName, SubscriptionPlanName } from '@standardnotes/domain-core'
|
||||
|
||||
describe('CreateCrossServiceToken', () => {
|
||||
let userProjector: ProjectorInterface<User>
|
||||
@@ -19,9 +15,6 @@ describe('CreateCrossServiceToken', () => {
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
let userRepository: UserRepositoryInterface
|
||||
let roleToSubscriptionMap: RoleToSubscriptionMapInterface
|
||||
let traceSession: TraceSession
|
||||
let logger: Logger
|
||||
const jwtTTL = 60
|
||||
|
||||
let session: Session
|
||||
@@ -29,17 +22,7 @@ describe('CreateCrossServiceToken', () => {
|
||||
let role: Role
|
||||
|
||||
const createUseCase = () =>
|
||||
new CreateCrossServiceToken(
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
userRepository,
|
||||
jwtTTL,
|
||||
roleToSubscriptionMap,
|
||||
traceSession,
|
||||
logger,
|
||||
)
|
||||
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
@@ -65,18 +48,6 @@ describe('CreateCrossServiceToken', () => {
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
|
||||
roleToSubscriptionMap = {} as jest.Mocked<RoleToSubscriptionMapInterface>
|
||||
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([RoleName.NAMES.PlusUser])
|
||||
roleToSubscriptionMap.getSubscriptionNameForRoleName = jest
|
||||
.fn()
|
||||
.mockReturnValue(SubscriptionPlanName.NAMES.PlusPlan)
|
||||
|
||||
traceSession = {} as jest.Mocked<TraceSession>
|
||||
traceSession.execute = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should create a cross service token for user', async () => {
|
||||
@@ -85,11 +56,6 @@ describe('CreateCrossServiceToken', () => {
|
||||
session,
|
||||
})
|
||||
|
||||
expect(traceSession.execute).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: 'PLUS_PLAN',
|
||||
})
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
@@ -168,126 +134,4 @@ describe('CreateCrossServiceToken', () => {
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should trace session without a subscription role', async () => {
|
||||
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([])
|
||||
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(traceSession.execute).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: null,
|
||||
})
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.te',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should trace session without a subscription', async () => {
|
||||
roleToSubscriptionMap.getSubscriptionNameForRoleName = jest.fn().mockReturnValue(undefined)
|
||||
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(traceSession.execute).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: null,
|
||||
})
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.te',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create token if tracing session throws an error', async () => {
|
||||
traceSession.execute = jest.fn().mockRejectedValue(new Error('test'))
|
||||
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.te',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create token if tracing session fails', async () => {
|
||||
traceSession.execute = jest.fn().mockReturnValue(Result.fail('Ooops'))
|
||||
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.te',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
|
||||
import { Role } from '../../Role/Role'
|
||||
import { RoleToSubscriptionMapInterface } from '../../Role/RoleToSubscriptionMapInterface'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { TraceSession } from '../TraceSession/TraceSession'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
|
||||
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
|
||||
@@ -25,9 +22,6 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
@inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number,
|
||||
@inject(TYPES.RoleToSubscriptionMap) private roleToSubscriptionMap: RoleToSubscriptionMapInterface,
|
||||
@inject(TYPES.TraceSession) private traceSession: TraceSession,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
|
||||
@@ -51,19 +45,6 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
authTokenData.session = this.projectSession(dto.session)
|
||||
}
|
||||
|
||||
try {
|
||||
const traceSessionResult = await this.traceSession.execute({
|
||||
userUuid: user.uuid,
|
||||
username: user.email,
|
||||
subscriptionPlanName: this.getSubscriptionNameFromRoles(roles),
|
||||
})
|
||||
if (traceSessionResult.isFailed()) {
|
||||
this.logger.error(traceSessionResult.getError())
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not trace session while creating cross service token: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
|
||||
}
|
||||
@@ -100,17 +81,4 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
private projectRoles(roles: Array<Role>): Array<{ uuid: string; name: RoleName }> {
|
||||
return roles.map((role) => <{ uuid: string; name: RoleName }>this.roleProjector.projectSimple(role))
|
||||
}
|
||||
|
||||
private getSubscriptionNameFromRoles(roles: Array<Role>): string | null {
|
||||
const nonSubscriptionRoles = this.roleToSubscriptionMap.filterNonSubscriptionRoles(roles)
|
||||
if (nonSubscriptionRoles.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const subscriptionName = this.roleToSubscriptionMap.getSubscriptionNameForRoleName(
|
||||
nonSubscriptionRoles[0].name as RoleName,
|
||||
)
|
||||
|
||||
return subscriptionName === undefined ? null : subscriptionName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Dates, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Authenticator } from '../../Authenticator/Authenticator'
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { DeleteAuthenticator } from './DeleteAuthenticator'
|
||||
|
||||
describe('DeleteAuthenticator', () => {
|
||||
let authenticatorRepository: AuthenticatorRepositoryInterface
|
||||
let authenticator: Authenticator
|
||||
const createUseCase = () => new DeleteAuthenticator(authenticatorRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
authenticator = Authenticator.create({
|
||||
counter: 1,
|
||||
name: 'my-key',
|
||||
credentialBackedUp: true,
|
||||
credentialDeviceType: 'singleDevice',
|
||||
credentialId: Buffer.from('credentialId'),
|
||||
credentialPublicKey: Buffer.from('credentialPublicKey'),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
dates: Dates.create(new Date(1), new Date(1)).getValue(),
|
||||
transports: ['usb'],
|
||||
}).getValue()
|
||||
|
||||
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>
|
||||
authenticatorRepository.findById = jest.fn().mockReturnValue(authenticator)
|
||||
authenticatorRepository.remove = jest.fn()
|
||||
})
|
||||
|
||||
it('should return error if authenticator not found', async () => {
|
||||
authenticatorRepository.findById = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
authenticatorId: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toEqual('Authenticator not found')
|
||||
})
|
||||
|
||||
it('should return error if authenticator does not belong to user', async () => {
|
||||
authenticatorRepository.findById = jest.fn().mockReturnValue({
|
||||
...authenticator,
|
||||
props: {
|
||||
...authenticator.props,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-00000000a000').getValue(),
|
||||
},
|
||||
})
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
authenticatorId: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toEqual('Authenticator not found')
|
||||
})
|
||||
|
||||
it('should delete authenticator', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
authenticatorId: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(result.getValue()).toEqual('Authenticator deleted')
|
||||
expect(authenticatorRepository.remove).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Result, UniqueEntityId, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { DeleteAuthenticatorDTO } from './DeleteAuthenticatorDTO'
|
||||
|
||||
export class DeleteAuthenticator implements UseCaseInterface<string> {
|
||||
constructor(private authenticatorRepository: AuthenticatorRepositoryInterface) {}
|
||||
async execute(dto: DeleteAuthenticatorDTO): Promise<Result<string>> {
|
||||
const authenticator = await this.authenticatorRepository.findById(new UniqueEntityId(dto.authenticatorId))
|
||||
if (!authenticator || authenticator.props.userUuid.value !== dto.userUuid) {
|
||||
return Result.fail('Authenticator not found')
|
||||
}
|
||||
|
||||
await this.authenticatorRepository.remove(authenticator)
|
||||
|
||||
return Result.ok('Authenticator deleted')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface DeleteAuthenticatorDTO {
|
||||
userUuid: string
|
||||
authenticatorId: string
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Dates, Result, Uuid } from '@standardnotes/domain-core'
|
||||
import { Authenticator } from '../../Authenticator/Authenticator'
|
||||
|
||||
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
|
||||
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { GenerateAuthenticatorAuthenticationOptions } from './GenerateAuthenticatorAuthenticationOptions'
|
||||
|
||||
describe('GenerateAuthenticatorAuthenticationOptions', () => {
|
||||
let authenticatorRepository: AuthenticatorRepositoryInterface
|
||||
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new GenerateAuthenticatorAuthenticationOptions(authenticatorRepository, authenticatorChallengeRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
const authenticator = Authenticator.create({
|
||||
counter: 1,
|
||||
name: 'my-key',
|
||||
credentialBackedUp: true,
|
||||
credentialDeviceType: 'singleDevice',
|
||||
credentialId: Buffer.from('credentialId'),
|
||||
credentialPublicKey: Buffer.from('credentialPublicKey'),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
dates: Dates.create(new Date(1), new Date(1)).getValue(),
|
||||
transports: ['usb'],
|
||||
}).getValue()
|
||||
|
||||
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>
|
||||
authenticatorRepository.findByUserUuid = jest.fn().mockReturnValue([authenticator])
|
||||
|
||||
authenticatorChallengeRepository = {} as jest.Mocked<AuthenticatorChallengeRepositoryInterface>
|
||||
authenticatorChallengeRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should return error if userUuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe(
|
||||
'Could not generate authenticator registration options: Given value is not a valid uuid: invalid',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error if authenticator challenge is invalid', async () => {
|
||||
const mock = jest.spyOn(AuthenticatorChallenge, 'create')
|
||||
mock.mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Could not generate authenticator registration options: Oops')
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
|
||||
it('should return authentication options', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(authenticatorChallengeRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user