Compare commits

...

53 Commits

Author SHA1 Message Date
standardci
02705ea3ad chore(release): publish new version
- @standardnotes/analytics@2.19.0
2022-12-30 11:44:19 +00:00
Karol Sójko
df6e3f06a6 feat(analytics): add mixpanel events tracking 2022-12-30 12:41:42 +01:00
standardci
1cb5ee9fd6 chore(release): publish new version
- @standardnotes/analytics@2.18.0
2022-12-30 07:41:42 +00:00
Karol Sójko
893d6176c3 feat(analytics): add mixpanel 2022-12-30 08:39:19 +01:00
standardci
2c1b512e40 chore(release): publish new version
- @standardnotes/api-gateway@1.44.0
 - @standardnotes/auth-server@1.79.0
2022-12-29 12:57:00 +00:00
Karol Sójko
de50d76800 feat(auth): add removing authenticator 2022-12-29 13:55:08 +01:00
standardci
401b78e477 chore(release): publish new version
- @standardnotes/api-gateway@1.43.0
 - @standardnotes/auth-server@1.78.0
2022-12-29 12:39:51 +00:00
Karol Sójko
01837eaea9 feat(auth): add listing authenticators 2022-12-29 13:37:30 +01:00
standardci
7df699353c chore(release): publish new version
- @standardnotes/auth-server@1.77.1
2022-12-29 11:57:38 +00:00
Karol Sójko
5455972be2 fix(auth): specs for verifying authenticator authentication response 2022-12-29 12:55:43 +01:00
standardci
57488bcd16 chore(release): publish new version
- @standardnotes/api-gateway@1.42.0
 - @standardnotes/auth-server@1.77.0
2022-12-29 10:31:18 +00:00
Karol Sójko
b6fda901ef feat(auth): add http endpoints for authenticators 2022-12-29 11:29:23 +01:00
standardci
14669df890 chore(release): publish new version
- @standardnotes/auth-server@1.76.0
2022-12-29 08:43:05 +00:00
Karol Sójko
64525a65f2 feat(auth): add verifying authenticator authentication response 2022-12-29 09:41:10 +01:00
standardci
61fc7efecb chore(release): publish new version
- @standardnotes/auth-server@1.75.0
2022-12-29 07:47:43 +00:00
Karol Sójko
8c7c1e4745 feat(auth): add generating authenticator authentication options 2022-12-29 08:45:32 +01:00
standardci
f64d30ec88 chore(release): publish new version
- @standardnotes/auth-server@1.74.1
2022-12-28 15:18:15 +00:00
Karol Sójko
384dfc8da4 fix(auth): migrations to not include unique index for credentials id 2022-12-28 16:16:10 +01:00
standardci
841784ae8c chore(release): publish new version
- @standardnotes/auth-server@1.74.0
2022-12-28 14:53:11 +00:00
Karol Sójko
f5683cfd94 feat(auth): add verifying authenticator registration response 2022-12-28 15:50:48 +01:00
standardci
0a420ce30e chore(release): publish new version
- @standardnotes/auth-server@1.73.1
2022-12-28 13:12:37 +00:00
Karol Sójko
a5e7132d3c fix(auth): temporarily remove credential id index due to mysql 5.6 limitations 2022-12-28 14:10:40 +01:00
standardci
6dfb2be4a2 chore(release): publish new version
- @standardnotes/auth-server@1.73.0
2022-12-28 13:09:57 +00:00
Karol Sójko
d81cbad550 Merge pull request #381 from standardnotes/authenticator_registration
feat(auth): add generating authencator registration options
2022-12-28 14:08:02 +01:00
Karol Sójko
51ad06b303 feat(auth): add generating authencator registration options 2022-12-28 13:56:06 +01:00
standardci
27048ad95c chore(release): publish new version
- @standardnotes/auth-server@1.72.0
2022-12-28 11:42:03 +00:00
Karol Sójko
fa9bf0b448 feat(auth): add authenticator challenges model 2022-12-28 12:40:13 +01:00
standardci
305190b64e chore(release): publish new version
- @standardnotes/auth-server@1.71.1
2022-12-28 11:27:35 +00:00
Karol Sójko
98e3d18335 fix(auth): credential id field type 2022-12-28 12:25:36 +01:00
standardci
72e398956b chore(release): publish new version
- @standardnotes/auth-server@1.71.0
2022-12-28 10:39:38 +00:00
Karol Sójko
1e69a13a97 feat(auth): add authenticators model 2022-12-28 11:37:06 +01:00
standardci
7f9e6e2f44 chore(release): publish new version
- @standardnotes/analytics@2.17.8
 - @standardnotes/api-gateway@1.41.3
 - @standardnotes/auth-server@1.70.9
 - @standardnotes/files-server@1.9.3
 - @standardnotes/revisions-server@1.10.3
 - @standardnotes/scheduler-server@1.16.4
 - @standardnotes/syncing-server@1.28.3
 - @standardnotes/websockets-server@1.5.3
 - @standardnotes/workspace-server@1.19.4
2022-12-28 07:07:42 +00:00
Karol Sójko
d3c6c0d48e chore(upgrade): sentry deps 2022-12-28 08:05:42 +01:00
Karol Sójko
6c83476fd2 chore: workflow disptach name 2022-12-27 15:50:40 +01:00
Karol Sójko
9cdf7e2c51 Revert "feat: add workflow for tagging latest versions as stable"
This reverts commit a2c484e0f3.
2022-12-27 15:37:32 +01:00
Karol Sójko
599119e14e chore: move e2e test suite to self-hosted repo 2022-12-27 15:00:11 +01:00
Karol Sójko
a2c484e0f3 feat: add workflow for tagging latest versions as stable 2022-12-27 14:43:36 +01:00
standardci
97ff4d5ac2 chore(release): publish new version
- @standardnotes/auth-server@1.70.8
2022-12-20 20:24:56 +00:00
Karol Sójko
5255cfbb25 fix(auth): move tracing sessions to session creation instead of cross service token creation 2022-12-20 21:22:24 +01:00
standardci
780358368b chore(release): publish new version
- @standardnotes/auth-server@1.70.7
2022-12-20 19:55:43 +00:00
Karol Sójko
cf0b918913 fix(auth): change severity on tracing session errors - most probably hazardous reads 2022-12-20 20:53:26 +01:00
standardci
4ea690204e chore(release): publish new version
- @standardnotes/auth-server@1.70.6
2022-12-20 18:54:47 +00:00
Karol Sójko
14eb775749 fix(auth): query for session traces 2022-12-20 19:52:32 +01:00
standardci
bf4a3be6d9 chore(release): publish new version
- @standardnotes/auth-server@1.70.5
2022-12-20 18:48:19 +00:00
Karol Sójko
b9e1e47871 fix(auth): add session traces index 2022-12-20 19:46:05 +01:00
standardci
ff532ecb22 chore(release): publish new version
- @standardnotes/scheduler-server@1.16.3
2022-12-20 18:21:59 +00:00
Karol Sójko
eb21872db1 fix(scheduler): new pricing for subscription encouragement email 2022-12-20 19:19:59 +01:00
standardci
8e3df184dc chore(release): publish new version
- @standardnotes/analytics@2.17.7
2022-12-20 14:43:24 +00:00
Karol Sójko
b34bbcac8b fix(analytics): monthly numbers of active users 2022-12-20 15:41:03 +01:00
standardci
226965a1d7 chore(release): publish new version
- @standardnotes/analytics@2.17.6
2022-12-20 14:04:05 +00:00
Karol Sójko
17b2ea126c fix(analytics): filtered counts for user activity check 2022-12-20 15:02:09 +01:00
standardci
59fc4a089c chore(release): publish new version
- @standardnotes/analytics@2.17.5
2022-12-20 13:16:57 +00:00
Karol Sójko
ef26dc8cbb fix(analytics): accessing analytics in report 2022-12-20 14:13:54 +01:00
149 changed files with 3491 additions and 293 deletions

View File

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

@@ -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", [\

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.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

View File

@@ -29,7 +29,6 @@ const requestReport = async (
calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
timer: TimerInterface,
adminEmails: string[],
logger: Logger,
): Promise<void> => {
await calculateMonthlyRecurringRevenue.execute({})
@@ -134,7 +133,6 @@ const requestReport = async (
counts: await statisticsStore.calculateTotalCountOverPeriod(statisticName, Period.Last30DaysIncludingToday),
})
}
logger.info('Calculated thirty days statistics: %O', statisticsOverTime)
const monthlyStatisticsNames = [StatisticMeasureName.NAMES.MRR]
for (const statisticName of monthlyStatisticsNames) {
@@ -284,7 +282,6 @@ void container.load().then((container) => {
calculateMonthlyRecurringRevenue,
timer,
adminEmails,
logger,
),
)
.then(() => {

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.17.4",
"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",

View File

@@ -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
@@ -220,6 +226,7 @@ export class ContainerConfigLoader {
container.get(TYPES.PersistStatistic),
container.get(TYPES.Timer),
container.get(TYPES.Logger),
env.get('MIXPANEL_TOKEN', true) ? container.get(TYPES.MixpanelClient) : null,
),
)

View File

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

View File

@@ -6,29 +6,30 @@ import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { Period } from '../Time/Period'
const countActiveUsers = (measureName: string, data: any): { yesterday: number; last30Days: number } => {
const totalActiveUsersLast30DaysIncludingToday = data.statisticMeasures.find(
const totalActiveUsersLast30DaysIncludingToday = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === measureName && a.period === 27,
)
// eslint-disable-next-line no-console
console.log(totalActiveUsersLast30DaysIncludingToday)
const totalActiveUsersYesterday =
totalActiveUsersLast30DaysIncludingToday.counts[totalActiveUsersLast30DaysIncludingToday.counts.length - 2]
.totalCount
// eslint-disable-next-line no-console
console.log(totalActiveUsersYesterday)
const filteredCounts = totalActiveUsersLast30DaysIncludingToday.counts.filter(
(count: { totalCount: number }) => count.totalCount !== 0,
)
// eslint-disable-next-line no-console
console.log(filteredCounts)
const averageActiveUsersLast30Days = Math.floor(
filteredCounts.reduce((previousValue: { totalCount: any }, currentValue: { totalCount: any }) => {
return previousValue.totalCount + currentValue.totalCount
}) / filteredCounts.length,
)
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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,15 @@ import { DomainEventHandlerInterface, StatisticPersistenceRequestedEvent } from
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 timer: TimerInterface, 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({
@@ -16,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,
})
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.41.2",
"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:*",

View File

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

View File

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

View File

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

View File

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

View File

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

View 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`')
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.70.4",
"version": "1.79.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -33,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:^",

View File

@@ -203,6 +203,25 @@ 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')
@@ -280,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
@@ -316,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)
@@ -355,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)
@@ -511,6 +558,44 @@ export class ContainerConfigLoader {
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)
@@ -565,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

View File

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

View File

@@ -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'),
@@ -127,6 +135,12 @@ const TYPES = {
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'),

View 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() },
}
}
}

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

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
import { Uuid } from '@standardnotes/domain-core'
export interface AuthenticatorChallengeProps {
userUuid: Uuid
challenge: Buffer
createdAt: Date
}

View File

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

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

View File

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

View File

@@ -0,0 +1,4 @@
export enum RelyingParty {
RP_NAME = 'Standard Notes',
RP_ID = 'standardnotes.com',
}

View File

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

View File

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

View File

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

View File

@@ -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.filterSubscriptionRoles(roles)
if (nonSubscriptionRoles.length === 0) {
return null
}
const subscriptionName = this.roleToSubscriptionMap.getSubscriptionNameForRoleName(
nonSubscriptionRoles[0].name as RoleName,
)
return subscriptionName === undefined ? null : subscriptionName
}
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
export interface DeleteAuthenticatorDTO {
userUuid: string
authenticatorId: string
}

View File

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

View File

@@ -0,0 +1,48 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { generateAuthenticationOptions } from '@simplewebauthn/server'
import { GenerateAuthenticatorAuthenticationOptionsDTO } from './GenerateAuthenticatorAuthenticationOptionsDTO'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
export class GenerateAuthenticatorAuthenticationOptions implements UseCaseInterface<Record<string, unknown>> {
constructor(
private authenticatorRepository: AuthenticatorRepositoryInterface,
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
) {}
async execute(dto: GenerateAuthenticatorAuthenticationOptionsDTO): Promise<Result<Record<string, unknown>>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not generate authenticator registration options: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid)
const options = generateAuthenticationOptions({
allowCredentials: authenticators.map((authenticator) => ({
id: authenticator.props.credentialId,
type: 'public-key',
transports: authenticator.props.transports,
})),
userVerification: 'preferred',
})
const authenticatorChallengeOrError = AuthenticatorChallenge.create({
challenge: options.challenge,
userUuid,
createdAt: new Date(),
})
if (authenticatorChallengeOrError.isFailed()) {
return Result.fail(
`Could not generate authenticator registration options: ${authenticatorChallengeOrError.getError()}`,
)
}
const authenticatorChallenge = authenticatorChallengeOrError.getValue()
await this.authenticatorChallengeRepository.save(authenticatorChallenge)
return Result.ok(options)
}
}

View File

@@ -0,0 +1,3 @@
export interface GenerateAuthenticatorAuthenticationOptionsDTO {
userUuid: string
}

View File

@@ -0,0 +1,90 @@
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 { GenerateAuthenticatorRegistrationOptions } from './GenerateAuthenticatorRegistrationOptions'
describe('GenerateAuthenticatorRegistrationOptions', () => {
let authenticatorRepository: AuthenticatorRepositoryInterface
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
const createUseCase = () =>
new GenerateAuthenticatorRegistrationOptions(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',
username: 'username',
})
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 username is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
username: '',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not generate authenticator registration options: Username cannot be empty')
})
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',
username: 'username',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not generate authenticator registration options: Oops')
mock.mockRestore()
})
it('should return registration options', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
username: 'username',
})
expect(result.isFailed()).toBe(false)
expect(authenticatorChallengeRepository.save).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,59 @@
import { Result, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core'
import { generateRegistrationOptions } from '@simplewebauthn/server'
import { GenerateAuthenticatorRegistrationOptionsDTO } from './GenerateAuthenticatorRegistrationOptionsDTO'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
import { RelyingParty } from '../../Authenticator/RelyingParty'
export class GenerateAuthenticatorRegistrationOptions implements UseCaseInterface<Record<string, unknown>> {
constructor(
private authenticatorRepository: AuthenticatorRepositoryInterface,
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
) {}
async execute(dto: GenerateAuthenticatorRegistrationOptionsDTO): Promise<Result<Record<string, unknown>>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not generate authenticator registration options: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const usernameOrError = Username.create(dto.username)
if (usernameOrError.isFailed()) {
return Result.fail(`Could not generate authenticator registration options: ${usernameOrError.getError()}`)
}
const username = usernameOrError.getValue()
const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid)
const options = generateRegistrationOptions({
rpID: RelyingParty.RP_ID,
rpName: RelyingParty.RP_NAME,
userID: userUuid.value,
userName: username.value,
attestationType: 'none',
excludeCredentials: authenticators.map((authenticator) => ({
id: authenticator.props.credentialId,
type: 'public-key',
transports: authenticator.props.transports,
})),
})
const authenticatorChallengeOrError = AuthenticatorChallenge.create({
challenge: options.challenge,
userUuid,
createdAt: new Date(),
})
if (authenticatorChallengeOrError.isFailed()) {
return Result.fail(
`Could not generate authenticator registration options: ${authenticatorChallengeOrError.getError()}`,
)
}
const authenticatorChallenge = authenticatorChallengeOrError.getValue()
await this.authenticatorChallengeRepository.save(authenticatorChallenge)
return Result.ok(options)
}
}

View File

@@ -0,0 +1,4 @@
export interface GenerateAuthenticatorRegistrationOptionsDTO {
userUuid: string
username: string
}

View File

@@ -0,0 +1,30 @@
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { ListAuthenticators } from './ListAuthenticators'
describe('ListAuthenticators', () => {
let authenticatorRepository: AuthenticatorRepositoryInterface
const createUseCase = () => new ListAuthenticators(authenticatorRepository)
beforeEach(() => {
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>
authenticatorRepository.findByUserUuid = jest.fn().mockReturnValue([])
})
it('should list authenticators', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect(result.isFailed()).toBeFalsy()
expect(authenticatorRepository.findByUserUuid).toHaveBeenCalled()
})
it('should fail if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: 'invalid' })
expect(result.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,20 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Authenticator } from '../../Authenticator/Authenticator'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { ListAuthenticatorsDTO } from './ListAuthenticatorsDTO'
export class ListAuthenticators implements UseCaseInterface<Authenticator[]> {
constructor(private authenticatorRepository: AuthenticatorRepositoryInterface) {}
async execute(dto: ListAuthenticatorsDTO): Promise<Result<Authenticator[]>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not list authenticators: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid)
return Result.ok(authenticators)
}
}

View File

@@ -0,0 +1,3 @@
export interface ListAuthenticatorsDTO {
userUuid: string
}

View File

@@ -0,0 +1,220 @@
import { Uuid, Dates } from '@standardnotes/domain-core'
import * as simeplWebAuthnServer from '@simplewebauthn/server'
import { VerifiedAuthenticationResponse } from '@simplewebauthn/server'
import { Authenticator } from '../../Authenticator/Authenticator'
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { VerifyAuthenticatorAuthenticationResponse } from './VerifyAuthenticatorAuthenticationResponse'
describe('VerifyAuthenticatorAuthenticationResponse', () => {
let authenticatorRepository: AuthenticatorRepositoryInterface
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
const createUseCase = () =>
new VerifyAuthenticatorAuthenticationResponse(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.findByUserUuidAndCredentialId = jest.fn().mockReturnValue(authenticator)
authenticatorRepository.save = jest.fn()
authenticatorChallengeRepository = {} as jest.Mocked<AuthenticatorChallengeRepositoryInterface>
authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue({
props: {
challenge: Buffer.from('challenge'),
},
} as jest.Mocked<AuthenticatorChallenge>)
})
it('should return error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
authenticationCredential: {
authenticatorAttachment: 'platform',
clientExtensionResults: {},
id: 'id',
rawId: 'rawId',
response: {
authenticatorData: 'authenticatorData',
clientDataJSON: 'clientDataJSON',
signature: 'signature',
userHandle: 'userHandle',
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Could not verify authenticator authentication response: Given value is not a valid uuid: invalid',
)
})
it('should return error if authenticator is not found', async () => {
authenticatorRepository.findByUserUuidAndCredentialId = jest.fn().mockReturnValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
authenticationCredential: {
authenticatorAttachment: 'platform',
clientExtensionResults: {},
id: 'id',
rawId: 'rawId',
response: {
authenticatorData: 'authenticatorData',
clientDataJSON: 'clientDataJSON',
signature: 'signature',
userHandle: 'userHandle',
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Could not verify authenticator authentication response: authenticator id not found',
)
})
it('should return error if authenticator challenge is not found', async () => {
authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
authenticationCredential: {
authenticatorAttachment: 'platform',
clientExtensionResults: {},
id: 'id',
rawId: 'rawId',
response: {
authenticatorData: 'authenticatorData',
clientDataJSON: 'clientDataJSON',
signature: 'signature',
userHandle: 'userHandle',
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not verify authenticator authentication response: challenge not found')
})
it('should return error if verification throws error', async () => {
const useCase = createUseCase()
const mock = jest.spyOn(simeplWebAuthnServer, 'verifyAuthenticationResponse')
mock.mockImplementation(() => {
throw new Error('error')
})
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
authenticationCredential: {
authenticatorAttachment: 'platform',
clientExtensionResults: {},
id: 'id',
rawId: 'rawId',
response: {
authenticatorData: 'authenticatorData',
clientDataJSON: 'clientDataJSON',
signature: 'signature',
userHandle: 'userHandle',
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not verify authenticator authentication response: error')
mock.mockRestore()
})
it('should return error if verification is not successful', async () => {
const useCase = createUseCase()
const mock = jest.spyOn(simeplWebAuthnServer, 'verifyAuthenticationResponse')
mock.mockReturnValue(
Promise.resolve({
verified: false,
} as jest.Mocked<VerifiedAuthenticationResponse>),
)
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
authenticationCredential: {
authenticatorAttachment: 'platform',
clientExtensionResults: {},
id: 'id',
rawId: 'rawId',
response: {
authenticatorData: 'authenticatorData',
clientDataJSON: 'clientDataJSON',
signature: 'signature',
userHandle: 'userHandle',
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not verify authenticator authentication response: verification failed')
mock.mockRestore()
})
it('should persist new authenticator counter', async () => {
const useCase = createUseCase()
const mock = jest.spyOn(simeplWebAuthnServer, 'verifyAuthenticationResponse')
mock.mockReturnValue(
Promise.resolve({
verified: true,
authenticationInfo: {
newCounter: 2,
},
} as jest.Mocked<VerifiedAuthenticationResponse>),
)
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
authenticationCredential: {
authenticatorAttachment: 'platform',
clientExtensionResults: {},
id: 'id',
rawId: 'rawId',
response: {
authenticatorData: 'authenticatorData',
clientDataJSON: 'clientDataJSON',
signature: 'signature',
userHandle: 'userHandle',
},
type: 'type',
},
})
expect(result.isFailed()).toBeFalsy()
expect(authenticatorRepository.save).toHaveBeenCalled()
})
})

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