Compare commits

..

46 Commits

Author SHA1 Message Date
standardci
972a91d59f chore(release): publish new version
- @standardnotes/auth-server@1.177.18
 - @standardnotes/home-server@1.22.57
2024-01-15 12:09:42 +00:00
Karol Sójko
045358ddbf fix(auth): add renewal for shared offline subscriptions 2024-01-15 12:42:54 +01:00
Karol Sójko
c7217a92ba fix(auth): add more logs to syncing subscription 2024-01-15 12:39:27 +01:00
standardci
3da7a21cde chore(release): publish new version
- @standardnotes/auth-server@1.177.17
 - @standardnotes/home-server@1.22.56
2024-01-15 10:27:24 +00:00
Karol Sójko
351e18f638 fix(auth): add debug logs for subscription sync requested event 2024-01-15 11:06:19 +01:00
standardci
4f2129c4e0 chore(release): publish new version
- @standardnotes/auth-server@1.177.16
 - @standardnotes/home-server@1.22.55
2024-01-15 09:44:23 +00:00
Karol Sójko
d7a1c667dd fix(auth): update shared subscriptions upon subscription sync (#1022) 2024-01-15 10:23:51 +01:00
standardci
4de0bfa36d chore(release): publish new version
- @standardnotes/home-server@1.22.54
 - @standardnotes/syncing-server@1.134.0
2024-01-12 15:06:26 +00:00
Karol Sójko
0443de88ce feat(syncing-server): reduced abuse thresholds for free users (#1021) 2024-01-12 15:45:00 +01:00
Karol Sójko
f830bac873 fix: reduce the transfer limit on e2e tests (#1020) 2024-01-11 13:12:33 +01:00
standardci
517ae5ded9 chore(release): publish new version
- @standardnotes/api-gateway@1.89.19
 - @standardnotes/files-server@1.37.9
 - @standardnotes/home-server@1.22.53
 - @standardnotes/syncing-server@1.133.6
2024-01-10 14:40:54 +00:00
Karol Sójko
6062f85000 fix: add dedicated http code response upon a request with too large payload (#1019)
* fix: add dedicated http code response upon a request with too large payload

* fix error log
2024-01-10 15:19:26 +01:00
standardci
e2205c3849 chore(release): publish new version
- @standardnotes/auth-server@1.177.15
 - @standardnotes/home-server@1.22.52
2024-01-09 09:17:45 +00:00
Karol Sójko
0b46eff16e fix(auth): check for user agent persisting on session during a session refresh (#1016) 2024-01-09 09:57:02 +01:00
standardci
df67982bca chore(release): publish new version
- @standardnotes/api-gateway@1.89.18
 - @standardnotes/auth-server@1.177.14
 - @standardnotes/home-server@1.22.51
 - @standardnotes/syncing-server@1.133.5
2024-01-08 12:25:14 +00:00
Karol Sójko
d44866b3c0 chore: add types for response locals (#1015) 2024-01-08 13:04:25 +01:00
standardci
6ee18bffe6 chore(release): publish new version
- @standardnotes/home-server@1.22.50
 - @standardnotes/syncing-server@1.133.4
2024-01-08 05:37:21 +00:00
Karol Sójko
a881dd2d79 fix(syncing-server): disable ot tracing 2024-01-08 06:16:59 +01:00
standardci
b767e1f072 chore(release): publish new version
- @standardnotes/home-server@1.22.49
 - @standardnotes/syncing-server@1.133.3
2024-01-05 12:28:02 +00:00
Karol Sójko
e3cb1faba4 fix(syncing-server): add traffic abuse check in gRPC coms 2024-01-05 13:07:00 +01:00
Karol Sójko
5c5f988055 fix(syncing-server): remove excessive debug logs 2024-01-05 13:07:00 +01:00
standardci
c7d2adf091 chore(release): publish new version
- @standardnotes/home-server@1.22.48
 - @standardnotes/syncing-server@1.133.2
2024-01-05 12:06:26 +00:00
Karol Sójko
a4ad37f309 fix(syncing-server): add debug logs to redis metrics store 2024-01-05 12:45:54 +01:00
Karol Sójko
73c2cc1222 fix(syncing-server): add metadata to transfer breach logs 2024-01-05 12:45:54 +01:00
standardci
9380900aaf chore(release): publish new version
- @standardnotes/home-server@1.22.47
 - @standardnotes/syncing-server@1.133.1
2024-01-05 11:43:04 +00:00
Karol Sójko
02f4d5c717 fix(syncing-server): metadata in logs 2024-01-05 12:22:28 +01:00
Karol Sójko
1f4b26d269 fix(syncing-server): add debug logs for checking traffic abuse 2024-01-05 12:21:24 +01:00
Karol Sójko
e253825da6 fix(syncing-server): error message 2024-01-05 12:09:34 +01:00
standardci
2bddd947ba chore(release): publish new version
- @standardnotes/home-server@1.22.46
 - @standardnotes/syncing-server@1.133.0
2024-01-05 10:50:34 +00:00
Karol Sójko
b7173346d2 feat(syncing-server): add traffic abuse checks (#1014) 2024-01-05 11:30:15 +01:00
standardci
01641975c0 chore(release): publish new version
- @standardnotes/api-gateway@1.89.17
 - @standardnotes/home-server@1.22.45
2024-01-04 18:33:43 +00:00
Karol Sójko
7abd80cdba fix(api-gateway): disable http call retries 2024-01-04 19:13:17 +01:00
standardci
aeb5ea1874 chore(release): publish new version
- @standardnotes/api-gateway@1.89.16
 - @standardnotes/home-server@1.22.44
2024-01-04 18:09:29 +00:00
Karol Sójko
d2a371b92c fix(api-gateway): disable sync request retries 2024-01-04 18:48:55 +01:00
standardci
3ea3b24bb6 chore(release): publish new version
- @standardnotes/home-server@1.22.43
 - @standardnotes/syncing-server@1.132.0
2024-01-04 16:43:22 +00:00
Karol Sójko
0c3bc0cae6 feat(syncing-server): send user based metrics to cloudwatch 2024-01-04 17:22:47 +01:00
standardci
d56410984a chore(release): publish new version
- @standardnotes/home-server@1.22.42
 - @standardnotes/syncing-server@1.131.0
2024-01-04 15:12:28 +00:00
Karol Sójko
4dd2eb9349 feat(syncing-server): store per user content size utilization and item operations metrics 2024-01-04 15:51:10 +01:00
standardci
709aec5eeb chore(release): publish new version
- @standardnotes/home-server@1.22.41
 - @standardnotes/syncing-server@1.130.3
2024-01-04 14:15:26 +00:00
Karol Sójko
f1aa431c22 fix(syncing-server): decrease metric expiration time 2024-01-04 14:54:39 +01:00
standardci
86d0e565ed chore(release): publish new version
- @standardnotes/home-server@1.22.40
 - @standardnotes/syncing-server@1.130.2
2024-01-04 12:53:16 +00:00
Karol Sójko
92bb447cac fix(syncing-server): amount of minutes to process for metrics 2024-01-04 13:32:33 +01:00
standardci
08966e7af7 chore(release): publish new version
- @standardnotes/home-server@1.22.39
 - @standardnotes/syncing-server@1.130.1
2024-01-04 12:24:35 +00:00
Karol Sójko
2c732ff713 fix(syncing-server): skip sending empty metrics 2024-01-04 13:04:03 +01:00
standardci
1493b7c478 chore(release): publish new version
- @standardnotes/analytics@2.34.14
 - @standardnotes/api-gateway@1.89.15
 - @standardnotes/auth-server@1.177.13
 - @standardnotes/files-server@1.37.8
 - @standardnotes/home-server@1.22.38
 - @standardnotes/revisions-server@1.51.14
 - @standardnotes/scheduler-server@1.27.19
 - @standardnotes/syncing-server@1.130.0
 - @standardnotes/time@1.19.0
 - @standardnotes/websockets-server@1.22.10
2024-01-04 11:33:36 +00:00
Karol Sójko
efd816a627 feat: add storing item saving and modifying metrics (#1013)
* feat: add storing item saving and modifying metrics

* fix: missing region in cloudwatch client

* fix: metric store binding
2024-01-04 12:13:02 +01:00
127 changed files with 2803 additions and 389 deletions

2
.github/ci.env vendored
View File

@@ -27,4 +27,4 @@ AUTH_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
SYNCING_SERVER_CONTENT_SIZE_TRANSFER_LIMIT=1000000
SYNCING_SERVER_CONTENT_SIZE_TRANSFER_LIMIT=100000

View File

@@ -70,7 +70,7 @@ jobs:
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=2" >> packages/home-server/.env
echo "CONTENT_SIZE_TRANSFER_LIMIT=1000000" >> packages/home-server/.env
echo "CONTENT_SIZE_TRANSFER_LIMIT=100000" >> packages/home-server/.env
echo "DB_HOST=localhost" >> packages/home-server/.env
echo "DB_PORT=3306" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env

407
.pnp.cjs generated
View File

@@ -300,6 +300,57 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/client-cloudwatch", [\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-client-cloudwatch-npm-3.485.0-afe4ac001f-0e02739ef1.zip/node_modules/@aws-sdk/client-cloudwatch/",\
"packageDependencies": [\
["@aws-sdk/client-cloudwatch", "npm:3.485.0"],\
["@aws-crypto/sha256-browser", "npm:3.0.0"],\
["@aws-crypto/sha256-js", "npm:3.0.0"],\
["@aws-sdk/client-sts", "npm:3.485.0"],\
["@aws-sdk/core", "npm:3.485.0"],\
["@aws-sdk/credential-provider-node", "npm:3.485.0"],\
["@aws-sdk/middleware-host-header", "npm:3.485.0"],\
["@aws-sdk/middleware-logger", "npm:3.485.0"],\
["@aws-sdk/middleware-recursion-detection", "npm:3.485.0"],\
["@aws-sdk/middleware-signing", "npm:3.485.0"],\
["@aws-sdk/middleware-user-agent", "npm:3.485.0"],\
["@aws-sdk/region-config-resolver", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@aws-sdk/util-endpoints", "npm:3.485.0"],\
["@aws-sdk/util-user-agent-browser", "npm:3.485.0"],\
["@aws-sdk/util-user-agent-node", "virtual:5f6733bd23aee10dd05576af160f1b93e0bb4a20b288e9b818dc0b69bdb08ea1a09d5836816f02bdafc9c01487816ae339c6b680c2f7849dfe249436c5f2b499#npm:3.485.0"],\
["@smithy/config-resolver", "npm:2.0.23"],\
["@smithy/core", "npm:1.2.2"],\
["@smithy/fetch-http-handler", "npm:2.3.2"],\
["@smithy/hash-node", "npm:2.0.18"],\
["@smithy/invalid-dependency", "npm:2.0.16"],\
["@smithy/middleware-content-length", "npm:2.0.18"],\
["@smithy/middleware-endpoint", "npm:2.3.0"],\
["@smithy/middleware-retry", "npm:2.0.26"],\
["@smithy/middleware-serde", "npm:2.0.16"],\
["@smithy/middleware-stack", "npm:2.0.10"],\
["@smithy/node-config-provider", "npm:2.1.9"],\
["@smithy/node-http-handler", "npm:2.2.2"],\
["@smithy/protocol-http", "npm:3.0.12"],\
["@smithy/smithy-client", "npm:2.2.1"],\
["@smithy/types", "npm:2.8.0"],\
["@smithy/url-parser", "npm:2.0.16"],\
["@smithy/util-base64", "npm:2.0.1"],\
["@smithy/util-body-length-browser", "npm:2.0.1"],\
["@smithy/util-body-length-node", "npm:2.1.0"],\
["@smithy/util-defaults-mode-browser", "npm:2.0.24"],\
["@smithy/util-defaults-mode-node", "npm:2.0.32"],\
["@smithy/util-endpoints", "npm:1.0.8"],\
["@smithy/util-retry", "npm:2.0.9"],\
["@smithy/util-utf8", "npm:2.0.2"],\
["@smithy/util-waiter", "npm:2.0.16"],\
["fast-xml-parser", "npm:4.2.5"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/client-s3", [\
["npm:3.484.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-client-s3-npm-3.484.0-681638ab7a-701523f3b3.zip/node_modules/@aws-sdk/client-s3/",\
@@ -512,6 +563,50 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-client-sso-npm-3.485.0-5f6733bd23-635de0e310.zip/node_modules/@aws-sdk/client-sso/",\
"packageDependencies": [\
["@aws-sdk/client-sso", "npm:3.485.0"],\
["@aws-crypto/sha256-browser", "npm:3.0.0"],\
["@aws-crypto/sha256-js", "npm:3.0.0"],\
["@aws-sdk/core", "npm:3.485.0"],\
["@aws-sdk/middleware-host-header", "npm:3.485.0"],\
["@aws-sdk/middleware-logger", "npm:3.485.0"],\
["@aws-sdk/middleware-recursion-detection", "npm:3.485.0"],\
["@aws-sdk/middleware-user-agent", "npm:3.485.0"],\
["@aws-sdk/region-config-resolver", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@aws-sdk/util-endpoints", "npm:3.485.0"],\
["@aws-sdk/util-user-agent-browser", "npm:3.485.0"],\
["@aws-sdk/util-user-agent-node", "virtual:5f6733bd23aee10dd05576af160f1b93e0bb4a20b288e9b818dc0b69bdb08ea1a09d5836816f02bdafc9c01487816ae339c6b680c2f7849dfe249436c5f2b499#npm:3.485.0"],\
["@smithy/config-resolver", "npm:2.0.23"],\
["@smithy/core", "npm:1.2.2"],\
["@smithy/fetch-http-handler", "npm:2.3.2"],\
["@smithy/hash-node", "npm:2.0.18"],\
["@smithy/invalid-dependency", "npm:2.0.16"],\
["@smithy/middleware-content-length", "npm:2.0.18"],\
["@smithy/middleware-endpoint", "npm:2.3.0"],\
["@smithy/middleware-retry", "npm:2.0.26"],\
["@smithy/middleware-serde", "npm:2.0.16"],\
["@smithy/middleware-stack", "npm:2.0.10"],\
["@smithy/node-config-provider", "npm:2.1.9"],\
["@smithy/node-http-handler", "npm:2.2.2"],\
["@smithy/protocol-http", "npm:3.0.12"],\
["@smithy/smithy-client", "npm:2.2.1"],\
["@smithy/types", "npm:2.8.0"],\
["@smithy/url-parser", "npm:2.0.16"],\
["@smithy/util-base64", "npm:2.0.1"],\
["@smithy/util-body-length-browser", "npm:2.0.1"],\
["@smithy/util-body-length-node", "npm:2.1.0"],\
["@smithy/util-defaults-mode-browser", "npm:2.0.24"],\
["@smithy/util-defaults-mode-node", "npm:2.0.32"],\
["@smithy/util-endpoints", "npm:1.0.8"],\
["@smithy/util-retry", "npm:2.0.9"],\
["@smithy/util-utf8", "npm:2.0.2"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/client-sts", [\
@@ -561,6 +656,53 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-client-sts-npm-3.485.0-cc69ab3505-98c7f4d722.zip/node_modules/@aws-sdk/client-sts/",\
"packageDependencies": [\
["@aws-sdk/client-sts", "npm:3.485.0"],\
["@aws-crypto/sha256-browser", "npm:3.0.0"],\
["@aws-crypto/sha256-js", "npm:3.0.0"],\
["@aws-sdk/core", "npm:3.485.0"],\
["@aws-sdk/credential-provider-node", "npm:3.485.0"],\
["@aws-sdk/middleware-host-header", "npm:3.485.0"],\
["@aws-sdk/middleware-logger", "npm:3.485.0"],\
["@aws-sdk/middleware-recursion-detection", "npm:3.485.0"],\
["@aws-sdk/middleware-user-agent", "npm:3.485.0"],\
["@aws-sdk/region-config-resolver", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@aws-sdk/util-endpoints", "npm:3.485.0"],\
["@aws-sdk/util-user-agent-browser", "npm:3.485.0"],\
["@aws-sdk/util-user-agent-node", "virtual:5f6733bd23aee10dd05576af160f1b93e0bb4a20b288e9b818dc0b69bdb08ea1a09d5836816f02bdafc9c01487816ae339c6b680c2f7849dfe249436c5f2b499#npm:3.485.0"],\
["@smithy/config-resolver", "npm:2.0.23"],\
["@smithy/core", "npm:1.2.2"],\
["@smithy/fetch-http-handler", "npm:2.3.2"],\
["@smithy/hash-node", "npm:2.0.18"],\
["@smithy/invalid-dependency", "npm:2.0.16"],\
["@smithy/middleware-content-length", "npm:2.0.18"],\
["@smithy/middleware-endpoint", "npm:2.3.0"],\
["@smithy/middleware-retry", "npm:2.0.26"],\
["@smithy/middleware-serde", "npm:2.0.16"],\
["@smithy/middleware-stack", "npm:2.0.10"],\
["@smithy/node-config-provider", "npm:2.1.9"],\
["@smithy/node-http-handler", "npm:2.2.2"],\
["@smithy/protocol-http", "npm:3.0.12"],\
["@smithy/smithy-client", "npm:2.2.1"],\
["@smithy/types", "npm:2.8.0"],\
["@smithy/url-parser", "npm:2.0.16"],\
["@smithy/util-base64", "npm:2.0.1"],\
["@smithy/util-body-length-browser", "npm:2.0.1"],\
["@smithy/util-body-length-node", "npm:2.1.0"],\
["@smithy/util-defaults-mode-browser", "npm:2.0.24"],\
["@smithy/util-defaults-mode-node", "npm:2.0.32"],\
["@smithy/util-endpoints", "npm:1.0.8"],\
["@smithy/util-middleware", "npm:2.0.9"],\
["@smithy/util-retry", "npm:2.0.9"],\
["@smithy/util-utf8", "npm:2.0.2"],\
["fast-xml-parser", "npm:4.2.5"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/core", [\
@@ -576,6 +718,19 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-core-npm-3.485.0-77ed30ee18-b84dafb213.zip/node_modules/@aws-sdk/core/",\
"packageDependencies": [\
["@aws-sdk/core", "npm:3.485.0"],\
["@smithy/core", "npm:1.2.2"],\
["@smithy/protocol-http", "npm:3.0.12"],\
["@smithy/signature-v4", "npm:2.0.5"],\
["@smithy/smithy-client", "npm:2.2.1"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/credential-provider-env", [\
@@ -589,6 +744,17 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-credential-provider-env-npm-3.485.0-0fda7f74e0-b8346ea6f5.zip/node_modules/@aws-sdk/credential-provider-env/",\
"packageDependencies": [\
["@aws-sdk/credential-provider-env", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/property-provider", "npm:2.0.5"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/credential-provider-ini", [\
@@ -608,6 +774,23 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.485.0-bec3aaa989-3176b03ee1.zip/node_modules/@aws-sdk/credential-provider-ini/",\
"packageDependencies": [\
["@aws-sdk/credential-provider-ini", "npm:3.485.0"],\
["@aws-sdk/credential-provider-env", "npm:3.485.0"],\
["@aws-sdk/credential-provider-process", "npm:3.485.0"],\
["@aws-sdk/credential-provider-sso", "npm:3.485.0"],\
["@aws-sdk/credential-provider-web-identity", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/credential-provider-imds", "npm:2.0.5"],\
["@smithy/property-provider", "npm:2.0.5"],\
["@smithy/shared-ini-file-loader", "npm:2.0.6"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/credential-provider-node", [\
@@ -628,6 +811,24 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-credential-provider-node-npm-3.485.0-9f40e4a3cf-d31e5a95ea.zip/node_modules/@aws-sdk/credential-provider-node/",\
"packageDependencies": [\
["@aws-sdk/credential-provider-node", "npm:3.485.0"],\
["@aws-sdk/credential-provider-env", "npm:3.485.0"],\
["@aws-sdk/credential-provider-ini", "npm:3.485.0"],\
["@aws-sdk/credential-provider-process", "npm:3.485.0"],\
["@aws-sdk/credential-provider-sso", "npm:3.485.0"],\
["@aws-sdk/credential-provider-web-identity", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/credential-provider-imds", "npm:2.0.5"],\
["@smithy/property-provider", "npm:2.0.5"],\
["@smithy/shared-ini-file-loader", "npm:2.0.6"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/credential-provider-process", [\
@@ -642,6 +843,18 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-credential-provider-process-npm-3.485.0-62d3460338-e740fb949e.zip/node_modules/@aws-sdk/credential-provider-process/",\
"packageDependencies": [\
["@aws-sdk/credential-provider-process", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/property-provider", "npm:2.0.5"],\
["@smithy/shared-ini-file-loader", "npm:2.0.6"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/credential-provider-sso", [\
@@ -658,6 +871,20 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.485.0-42db25db09-7269315797.zip/node_modules/@aws-sdk/credential-provider-sso/",\
"packageDependencies": [\
["@aws-sdk/credential-provider-sso", "npm:3.485.0"],\
["@aws-sdk/client-sso", "npm:3.485.0"],\
["@aws-sdk/token-providers", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/property-provider", "npm:2.0.5"],\
["@smithy/shared-ini-file-loader", "npm:2.0.6"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/credential-provider-web-identity", [\
@@ -671,6 +898,17 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.485.0-420b04bcce-33125ce0b7.zip/node_modules/@aws-sdk/credential-provider-web-identity/",\
"packageDependencies": [\
["@aws-sdk/credential-provider-web-identity", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/property-provider", "npm:2.0.5"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/middleware-bucket-endpoint", [\
@@ -730,6 +968,17 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-middleware-host-header-npm-3.485.0-2e625f9614-9ca3da2a26.zip/node_modules/@aws-sdk/middleware-host-header/",\
"packageDependencies": [\
["@aws-sdk/middleware-host-header", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/protocol-http", "npm:3.0.12"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/middleware-location-constraint", [\
@@ -754,6 +1003,16 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-middleware-logger-npm-3.485.0-3ff7eeabbb-2fcb731794.zip/node_modules/@aws-sdk/middleware-logger/",\
"packageDependencies": [\
["@aws-sdk/middleware-logger", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/middleware-recursion-detection", [\
@@ -767,6 +1026,17 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-middleware-recursion-detection-npm-3.485.0-af05ed4810-afdea18930.zip/node_modules/@aws-sdk/middleware-recursion-detection/",\
"packageDependencies": [\
["@aws-sdk/middleware-recursion-detection", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/protocol-http", "npm:3.0.12"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/middleware-sdk-s3", [\
@@ -815,6 +1085,20 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-middleware-signing-npm-3.485.0-3117db6053-f9dbb39d8d.zip/node_modules/@aws-sdk/middleware-signing/",\
"packageDependencies": [\
["@aws-sdk/middleware-signing", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/property-provider", "npm:2.0.5"],\
["@smithy/protocol-http", "npm:3.0.12"],\
["@smithy/signature-v4", "npm:2.0.5"],\
["@smithy/types", "npm:2.8.0"],\
["@smithy/util-middleware", "npm:2.0.9"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/middleware-ssec", [\
@@ -841,6 +1125,18 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-middleware-user-agent-npm-3.485.0-983204fccf-a8fc812aff.zip/node_modules/@aws-sdk/middleware-user-agent/",\
"packageDependencies": [\
["@aws-sdk/middleware-user-agent", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@aws-sdk/util-endpoints", "npm:3.485.0"],\
["@smithy/protocol-http", "npm:3.0.12"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/region-config-resolver", [\
@@ -855,6 +1151,18 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-region-config-resolver-npm-3.485.0-1a69e46754-55bc5128b8.zip/node_modules/@aws-sdk/region-config-resolver/",\
"packageDependencies": [\
["@aws-sdk/region-config-resolver", "npm:3.485.0"],\
["@smithy/node-config-provider", "npm:2.1.9"],\
["@smithy/types", "npm:2.8.0"],\
["@smithy/util-config-provider", "npm:2.1.0"],\
["@smithy/util-middleware", "npm:2.0.9"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/signature-v4-multi-region", [\
@@ -916,6 +1224,50 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-token-providers-npm-3.485.0-1fb5ab9dfb-aed270b625.zip/node_modules/@aws-sdk/token-providers/",\
"packageDependencies": [\
["@aws-sdk/token-providers", "npm:3.485.0"],\
["@aws-crypto/sha256-browser", "npm:3.0.0"],\
["@aws-crypto/sha256-js", "npm:3.0.0"],\
["@aws-sdk/middleware-host-header", "npm:3.485.0"],\
["@aws-sdk/middleware-logger", "npm:3.485.0"],\
["@aws-sdk/middleware-recursion-detection", "npm:3.485.0"],\
["@aws-sdk/middleware-user-agent", "npm:3.485.0"],\
["@aws-sdk/region-config-resolver", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@aws-sdk/util-endpoints", "npm:3.485.0"],\
["@aws-sdk/util-user-agent-browser", "npm:3.485.0"],\
["@aws-sdk/util-user-agent-node", "virtual:5f6733bd23aee10dd05576af160f1b93e0bb4a20b288e9b818dc0b69bdb08ea1a09d5836816f02bdafc9c01487816ae339c6b680c2f7849dfe249436c5f2b499#npm:3.485.0"],\
["@smithy/config-resolver", "npm:2.0.23"],\
["@smithy/fetch-http-handler", "npm:2.3.2"],\
["@smithy/hash-node", "npm:2.0.18"],\
["@smithy/invalid-dependency", "npm:2.0.16"],\
["@smithy/middleware-content-length", "npm:2.0.18"],\
["@smithy/middleware-endpoint", "npm:2.3.0"],\
["@smithy/middleware-retry", "npm:2.0.26"],\
["@smithy/middleware-serde", "npm:2.0.16"],\
["@smithy/middleware-stack", "npm:2.0.10"],\
["@smithy/node-config-provider", "npm:2.1.9"],\
["@smithy/node-http-handler", "npm:2.2.2"],\
["@smithy/property-provider", "npm:2.0.5"],\
["@smithy/protocol-http", "npm:3.0.12"],\
["@smithy/shared-ini-file-loader", "npm:2.0.6"],\
["@smithy/smithy-client", "npm:2.2.1"],\
["@smithy/types", "npm:2.8.0"],\
["@smithy/url-parser", "npm:2.0.16"],\
["@smithy/util-base64", "npm:2.0.1"],\
["@smithy/util-body-length-browser", "npm:2.0.1"],\
["@smithy/util-body-length-node", "npm:2.1.0"],\
["@smithy/util-defaults-mode-browser", "npm:2.0.24"],\
["@smithy/util-defaults-mode-node", "npm:2.0.32"],\
["@smithy/util-endpoints", "npm:1.0.8"],\
["@smithy/util-retry", "npm:2.0.9"],\
["@smithy/util-utf8", "npm:2.0.2"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/types", [\
@@ -935,6 +1287,15 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-types-npm-3.485.0-6aa8cab069-588aae4b49.zip/node_modules/@aws-sdk/types/",\
"packageDependencies": [\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/types", "npm:2.8.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/util-arn-parser", [\
@@ -957,6 +1318,16 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-util-endpoints-npm-3.485.0-5e0fad395e-c1844fed8b.zip/node_modules/@aws-sdk/util-endpoints/",\
"packageDependencies": [\
["@aws-sdk/util-endpoints", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/util-endpoints", "npm:1.0.8"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/util-locate-window", [\
@@ -980,6 +1351,17 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-util-user-agent-browser-npm-3.485.0-23925a5581-d1e4d4c635.zip/node_modules/@aws-sdk/util-user-agent-browser/",\
"packageDependencies": [\
["@aws-sdk/util-user-agent-browser", "npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/types", "npm:2.8.0"],\
["bowser", "npm:2.11.0"],\
["tslib", "npm:2.5.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@aws-sdk/util-user-agent-node", [\
@@ -990,6 +1372,30 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "SOFT"\
}],\
["npm:3.485.0", {\
"packageLocation": "./.yarn/cache/@aws-sdk-util-user-agent-node-npm-3.485.0-7991a74cb3-e2805ef37b.zip/node_modules/@aws-sdk/util-user-agent-node/",\
"packageDependencies": [\
["@aws-sdk/util-user-agent-node", "npm:3.485.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:5f6733bd23aee10dd05576af160f1b93e0bb4a20b288e9b818dc0b69bdb08ea1a09d5836816f02bdafc9c01487816ae339c6b680c2f7849dfe249436c5f2b499#npm:3.485.0", {\
"packageLocation": "./.yarn/__virtual__/@aws-sdk-util-user-agent-node-virtual-c26ab353dd/0/cache/@aws-sdk-util-user-agent-node-npm-3.485.0-7991a74cb3-e2805ef37b.zip/node_modules/@aws-sdk/util-user-agent-node/",\
"packageDependencies": [\
["@aws-sdk/util-user-agent-node", "virtual:5f6733bd23aee10dd05576af160f1b93e0bb4a20b288e9b818dc0b69bdb08ea1a09d5836816f02bdafc9c01487816ae339c6b680c2f7849dfe249436c5f2b499#npm:3.485.0"],\
["@aws-sdk/types", "npm:3.485.0"],\
["@smithy/node-config-provider", "npm:2.1.9"],\
["@smithy/types", "npm:2.8.0"],\
["@types/aws-crt", null],\
["aws-crt", null],\
["tslib", "npm:2.5.2"]\
],\
"packagePeers": [\
"@types/aws-crt",\
"aws-crt"\
],\
"linkType": "HARD"\
}],\
["virtual:f5e5a564ba918754c8b3b080eb29a2688af35b9c1a773de63eb148390564369a40f304a7384da1d22d51cb840254882c972d18d82a96c4804ada9a0446f624d6#npm:3.470.0", {\
"packageLocation": "./.yarn/__virtual__/@aws-sdk-util-user-agent-node-virtual-92b1b7bd6e/0/cache/@aws-sdk-util-user-agent-node-npm-3.470.0-99b784cecc-05571ba83d.zip/node_modules/@aws-sdk/util-user-agent-node/",\
"packageDependencies": [\
@@ -6125,6 +6531,7 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/syncing-server/",\
"packageDependencies": [\
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
["@aws-sdk/client-cloudwatch", "npm:3.485.0"],\
["@aws-sdk/client-s3", "npm:3.484.0"],\
["@aws-sdk/client-sns", "npm:3.484.0"],\
["@aws-sdk/client-sqs", "npm:3.484.0"],\

Binary file not shown.

Binary file not shown.

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.34.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.13...@standardnotes/analytics@2.34.14) (2024-01-04)
**Note:** Version bump only for package @standardnotes/analytics
## [2.34.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.12...@standardnotes/analytics@2.34.13) (2024-01-03)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.34.13",
"version": "2.34.14",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.89.19](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.18...@standardnotes/api-gateway@1.89.19) (2024-01-10)
### Bug Fixes
* add dedicated http code response upon a request with too large payload ([#1019](https://github.com/standardnotes/server/issues/1019)) ([6062f85](https://github.com/standardnotes/server/commit/6062f850000477983315d2d9b7c913956f755ebb))
## [1.89.18](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.17...@standardnotes/api-gateway@1.89.18) (2024-01-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.89.17](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.16...@standardnotes/api-gateway@1.89.17) (2024-01-04)
### Bug Fixes
* **api-gateway:** disable http call retries ([7abd80c](https://github.com/standardnotes/server/commit/7abd80cdbaba53840f632d418bd557b35b722699))
## [1.89.16](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.15...@standardnotes/api-gateway@1.89.16) (2024-01-04)
### Bug Fixes
* **api-gateway:** disable sync request retries ([d2a371b](https://github.com/standardnotes/server/commit/d2a371b92c8b2b7f8921fe57f162e74d4944715d))
## [1.89.15](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.14...@standardnotes/api-gateway@1.89.15) (2024-01-04)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.89.14](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.13...@standardnotes/api-gateway@1.89.14) (2024-01-03)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -36,12 +36,17 @@ import { InversifyExpressServer } from 'inversify-express-utils'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import { TYPES } from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { ResponseLocals } from '../src/Controller/ResponseLocals'
const container = new ContainerConfigLoader()
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const requestPayloadLimit = env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)
? `${+env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)}mb`
: '50mb'
const server = new InversifyExpressServer(container)
server.setConfig((app) => {
@@ -72,7 +77,7 @@ void container.load().then((container) => {
}),
)
app.use(json({ limit: '50mb' }))
app.use(json({ limit: requestPayloadLimit }))
app.use(
text({
type: ['text/plain', 'application/x-www-form-urlencoded', 'application/x-www-form-urlencoded; charset=utf-8'],
@@ -91,12 +96,14 @@ void container.load().then((container) => {
server.setErrorConfig((app) => {
app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
const locals = response.locals as ResponseLocals
logger.error(`${error.stack}`, {
method: request.method,
url: request.url,
snjs: request.headers['x-snjs-version'],
application: request.headers['x-application-version'],
userId: response.locals.user ? response.locals.user.uuid : undefined,
userId: locals.user ? locals.user.uuid : undefined,
})
logger.debug(
`[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${
@@ -104,6 +111,16 @@ void container.load().then((container) => {
}] Request body: ${JSON.stringify(request.body)}`,
)
if ('type' in error && error.type === 'entity.too.large') {
response.status(413).send({
error: {
message: 'The request payload is too large.',
},
})
return
}
response.status(500).send({
error: {
message:

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.89.14",
"version": "1.89.19",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -8,6 +8,8 @@ import { Logger } from 'winston'
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface'
import { ResponseLocals } from './ResponseLocals'
import { RoleName } from '@standardnotes/domain-core'
export abstract class AuthMiddleware extends BaseMiddleware {
constructor(
@@ -55,33 +57,27 @@ export abstract class AuthMiddleware extends BaseMiddleware {
crossServiceTokenFetchedFromCache = false
}
response.locals.authToken = crossServiceToken
const decodedToken = <CrossServiceTokenData>(
verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
)
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({
key: cacheKey,
encodedCrossServiceToken: response.locals.authToken,
encodedCrossServiceToken: crossServiceToken,
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
userUuid: decodedToken.user.uuid,
})
}
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
if (response.locals.readOnlyAccess) {
this.logger.debug('User operates on read-only access', {
codeTag: 'AuthMiddleware',
userId: response.locals.user.uuid,
})
}
response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
Object.assign(response.locals, {
authToken: crossServiceToken,
user: decodedToken.user,
session: decodedToken.session,
roles: decodedToken.roles,
sharedVaultOwnerContext: decodedToken.shared_vault_owner_context,
readOnlyAccess: decodedToken.session?.readonly_access ?? false,
isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
belongsToSharedVaults: decodedToken.belongs_to_shared_vaults ?? [],
} as ResponseLocals)
} catch (error) {
let detailedErrorMessage = (error as Error).message
if (error instanceof AxiosError) {

View File

@@ -6,6 +6,7 @@ import { verify } from 'jsonwebtoken'
import { Logger } from 'winston'
import { ConnectionValidationResponse, IAuthClient, WebsocketConnectionAuthorizationHeader } from '@standardnotes/grpc'
import { RoleName } from '@standardnotes/domain-core'
import { ResponseLocals } from './ResponseLocals'
export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
constructor(
@@ -90,15 +91,16 @@ export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
const crossServiceToken = authResponse.data.authToken as string
response.locals.authToken = crossServiceToken
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
response.locals.isFreeUser =
decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser
Object.assign(response.locals, {
authToken: crossServiceToken,
user: decodedToken.user,
session: decodedToken.session,
roles: decodedToken.roles,
isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
readOnlyAccess: decodedToken.session?.readonly_access ?? false,
} as ResponseLocals)
} catch (error) {
this.logger.error(
`Could not pass the request to websocket connection validation on underlying service: ${

View File

@@ -0,0 +1,5 @@
export interface OfflineResponseLocals {
offlineAuthToken: string
userEmail: string
featuresToken: string
}

View File

@@ -0,0 +1,29 @@
import { Role } from '@standardnotes/security'
export interface ResponseLocals {
authToken: string
user: {
uuid: string
email: string
}
roles: Array<Role>
session?: {
uuid: string
api_version: string
created_at: string
updated_at: string
device_info: string
readonly_access: boolean
access_expiration: string
refresh_expiration: string
}
readOnlyAccess: boolean
isFreeUser: boolean
belongsToSharedVaults?: Array<{
shared_vault_uuid: string
permission: string
}>
sharedVaultOwnerContext?: {
upload_bytes_limit: number
}
}

View File

@@ -0,0 +1,5 @@
import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
export interface SubscriptionResponseLocals {
tokenAuthenticationMethod: TokenAuthenticationMethod
}

View File

@@ -7,6 +7,9 @@ import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
import { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types'
import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
import { ResponseLocals } from './ResponseLocals'
import { OfflineResponseLocals } from './OfflineResponseLocals'
import { SubscriptionResponseLocals } from './SubscriptionResponseLocals'
@injectable()
export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
@@ -34,13 +37,16 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
return
}
response.locals.tokenAuthenticationMethod = email
? TokenAuthenticationMethod.OfflineSubscriptionToken
: TokenAuthenticationMethod.SubscriptionToken
const locals = {
tokenAuthenticationMethod: email
? TokenAuthenticationMethod.OfflineSubscriptionToken
: TokenAuthenticationMethod.SubscriptionToken,
} as SubscriptionResponseLocals
Object.assign(response.locals, locals)
try {
const url =
response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken
locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken
? `${this.authServerUrl}/offline/subscription-tokens/${subscriptionToken}/validate`
: `${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate`
@@ -65,7 +71,7 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
return
}
if (response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) {
if (locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) {
this.handleOfflineAuthTokenValidationResponse(response, authResponse)
return next()
@@ -101,24 +107,26 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
}
private handleOfflineAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
response.locals.offlineAuthToken = authResponse.data.authToken
const decodedToken = <OfflineUserTokenData>(
verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
)
response.locals.offlineUserEmail = decodedToken.userEmail
response.locals.offlineFeaturesToken = decodedToken.featuresToken
Object.assign(response.locals, {
offlineAuthToken: authResponse.data.authToken,
userEmail: decodedToken.userEmail,
featuresToken: decodedToken.featuresToken,
} as OfflineResponseLocals)
}
private handleAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
response.locals.authToken = authResponse.data.authToken
const decodedToken = <CrossServiceTokenData>(
verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
)
response.locals.user = decodedToken.user
response.locals.roles = decodedToken.roles
Object.assign(response.locals, {
authToken: authResponse.data.authToken,
user: decodedToken.user,
roles: decodedToken.roles,
} as ResponseLocals)
}
}

View File

@@ -7,6 +7,7 @@ import { AxiosError, AxiosInstance } from 'axios'
import { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types'
import { ResponseLocals } from './ResponseLocals'
@injectable()
export class WebSocketAuthMiddleware extends BaseMiddleware {
@@ -55,13 +56,14 @@ export class WebSocketAuthMiddleware extends BaseMiddleware {
const crossServiceToken = authResponse.data.authToken
response.locals.authToken = crossServiceToken
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
Object.assign(response.locals, {
authToken: crossServiceToken,
user: decodedToken.user,
session: decodedToken.session,
roles: decodedToken.roles,
} as ResponseLocals)
} catch (error) {
const errorMessage = (error as AxiosError).isAxiosError
? JSON.stringify((error as AxiosError).response?.data)

View File

@@ -16,6 +16,8 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
import { TokenAuthenticationMethod } from '../TokenAuthenticationMethod'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
import { ResponseLocals } from '../ResponseLocals'
import { SubscriptionResponseLocals } from '../SubscriptionResponseLocals'
@controller('/v1/users')
export class UsersController extends BaseHttpController {
@@ -214,7 +216,9 @@ export class UsersController extends BaseHttpController {
@httpGet('/subscription', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async getSubscriptionBySubscriptionToken(request: Request, response: Response): Promise<void> {
if (response.locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) {
const locals = response.locals as SubscriptionResponseLocals & ResponseLocals
if (locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) {
await this.httpService.callAuthServer(
request,
response,
@@ -227,11 +231,7 @@ export class UsersController extends BaseHttpController {
await this.httpService.callAuthServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier(
'GET',
'users/:userUuid/subscription',
response.locals.user.uuid,
),
this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/:userUuid/subscription', locals.user.uuid),
)
}

View File

@@ -2,6 +2,7 @@ import { Request, Response } from 'express'
import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core'
import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
import { ResponseLocals } from '../../Controller/ResponseLocals'
export class DirectCallServiceProxy implements ServiceProxyInterface {
constructor(
@@ -134,11 +135,13 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
response: Response,
serviceResponse: { statusCode: number; json: Record<string, unknown> },
): void {
const locals = response.locals as ResponseLocals
void response.status(serviceResponse.statusCode).send({
meta: {
auth: {
userUuid: response.locals.user?.uuid,
roles: response.locals.roles,
userUuid: locals.user?.uuid,
roles: locals.roles,
},
server: {
filesServerUrl: this.filesServerUrl,

View File

@@ -8,6 +8,8 @@ import { TYPES } from '../../Bootstrap/Types'
import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
import { TimerInterface } from '@standardnotes/time'
import { ResponseLocals } from '../../Controller/ResponseLocals'
import { OfflineResponseLocals } from '../../Controller/OfflineResponseLocals'
@injectable()
export class HttpServiceProxy implements ServiceProxyInterface {
@@ -175,8 +177,9 @@ export class HttpServiceProxy implements ServiceProxyInterface {
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
retryAttempt?: number,
): Promise<AxiosResponse | undefined> {
const locals = response.locals as ResponseLocals | OfflineResponseLocals
try {
const headers: Record<string, string> = {}
for (const headerName of Object.keys(request.headers)) {
@@ -186,12 +189,12 @@ export class HttpServiceProxy implements ServiceProxyInterface {
delete headers.host
delete headers['content-length']
if (response.locals.authToken) {
headers['X-Auth-Token'] = response.locals.authToken
if ('authToken' in locals && locals.authToken) {
headers['X-Auth-Token'] = locals.authToken
}
if (response.locals.offlineAuthToken) {
headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken
if ('offlineAuthToken' in locals && locals.offlineAuthToken) {
headers['X-Auth-Offline-Token'] = locals.offlineAuthToken
}
const serviceResponse = await this.httpClient.request({
@@ -213,35 +216,17 @@ export class HttpServiceProxy implements ServiceProxyInterface {
await this.crossServiceTokenCache.invalidate(userUuid)
}
if (retryAttempt) {
this.logger.debug(`Request to ${serverUrl}/${endpoint} succeeded after ${retryAttempt} retries`)
}
return serviceResponse
} catch (error) {
const requestDidNotMakeIt = this.requestTimedOutOrDidNotReachDestination(error as Record<string, unknown>)
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
await this.timer.sleep(50)
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
this.logger.debug(`Retrying request to ${serverUrl}/${endpoint} for the ${nextRetryAttempt} time`)
return this.getServerResponse(serverUrl, request, response, endpoint, payload, nextRetryAttempt)
}
let detailedErrorMessage = (error as Error).message
if (error instanceof AxiosError) {
detailedErrorMessage = `Status: ${error.status}, code: ${error.code}, message: ${error.message}`
}
this.logger.error(
tooManyRetryAttempts
? `Request to ${serverUrl}/${endpoint} timed out after ${retryAttempt} retries`
: `Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`,
`Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`,
{
userId: response.locals.user ? response.locals.user.uuid : undefined,
userId: (locals as ResponseLocals).user ? (locals as ResponseLocals).user.uuid : undefined,
},
)
@@ -276,6 +261,7 @@ export class HttpServiceProxy implements ServiceProxyInterface {
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
const locals = response.locals as ResponseLocals
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
if (!serviceResponse) {
@@ -293,8 +279,8 @@ export class HttpServiceProxy implements ServiceProxyInterface {
response.status(serviceResponse.status).send({
meta: {
auth: {
userUuid: response.locals.user?.uuid,
roles: response.locals.roles,
userUuid: locals.user?.uuid,
roles: locals.roles,
},
server: {
filesServerUrl: this.filesServerUrl,

View File

@@ -40,7 +40,6 @@ export class EndpointResolver implements EndpointResolverInterface {
// Tokens Controller
['[POST]:subscription-tokens', 'auth.subscription-tokens.create'],
// Users Controller
['[PATCH]:users/:userId', 'auth.users.update'],
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
['[DELETE]:users/:userUuid', 'auth.users.delete'],
['[POST]:listed', 'auth.users.createListedAccount'],

View File

@@ -9,6 +9,8 @@ import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCache
import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
import { GRPCSyncingServerServiceProxy } from './GRPCSyncingServerServiceProxy'
import { Status } from '@grpc/grpc-js/build/src/constants'
import { ResponseLocals } from '../../Controller/ResponseLocals'
import { OfflineResponseLocals } from '../../Controller/OfflineResponseLocals'
export class GRPCServiceProxy implements ServiceProxyInterface {
constructor(
@@ -134,48 +136,23 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
request: Request,
response: Response,
payload?: Record<string, unknown> | string,
retryAttempt?: number,
): Promise<void> {
try {
const result = await this.gRPCSyncingServerServiceProxy.sync(request, response, payload)
const locals = response.locals as ResponseLocals
response.status(result.status).send({
meta: {
auth: {
userUuid: response.locals.user?.uuid,
roles: response.locals.roles,
},
server: {
filesServerUrl: this.filesServerUrl,
},
const result = await this.gRPCSyncingServerServiceProxy.sync(request, response, payload)
response.status(result.status).send({
meta: {
auth: {
userUuid: locals.user?.uuid,
roles: locals.roles,
},
data: result.data,
})
if (retryAttempt) {
this.logger.debug(`Request to Syncing Server succeeded after ${retryAttempt} retries`, {
userId: response.locals.user ? response.locals.user.uuid : undefined,
})
}
} catch (error) {
const requestDidNotMakeIt =
'code' in (error as Record<string, unknown>) && (error as Record<string, unknown>).code === Status.UNAVAILABLE
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
await this.timer.sleep(50)
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
this.logger.debug(`Retrying request to Syncing Server for the ${nextRetryAttempt} time`, {
userId: response.locals.user ? response.locals.user.uuid : undefined,
})
return this.callSyncingServerGRPC(request, response, payload, nextRetryAttempt)
}
throw error
}
server: {
filesServerUrl: this.filesServerUrl,
},
},
data: result.data,
})
}
async callRevisionsServer(
@@ -277,6 +254,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
payload?: Record<string, unknown> | string,
retryAttempt?: number,
): Promise<AxiosResponse | undefined> {
const locals = response.locals as ResponseLocals | OfflineResponseLocals
try {
const headers: Record<string, string> = {}
for (const headerName of Object.keys(request.headers)) {
@@ -286,12 +265,12 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
delete headers.host
delete headers['content-length']
if (response.locals.authToken) {
headers['X-Auth-Token'] = response.locals.authToken
if ('authToken' in locals && locals.authToken) {
headers['X-Auth-Token'] = locals.authToken
}
if (response.locals.offlineAuthToken) {
headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken
if ('offlineAuthToken' in locals && locals.offlineAuthToken) {
headers['X-Auth-Offline-Token'] = locals.offlineAuthToken
}
const serviceResponse = await this.httpClient.request({
@@ -341,7 +320,7 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
? `Request to ${serverUrl}/${endpoint} timed out after ${retryAttempt} retries`
: `Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`,
{
userId: response.locals.user ? response.locals.user.uuid : undefined,
userId: (locals as ResponseLocals).user ? (locals as ResponseLocals).user.uuid : undefined,
},
)
@@ -376,6 +355,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
const locals = response.locals as ResponseLocals
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
if (!serviceResponse) {
@@ -393,8 +374,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
response.status(serviceResponse.status).send({
meta: {
auth: {
userUuid: response.locals.user?.uuid,
roles: response.locals.roles,
userUuid: locals.user?.uuid,
roles: locals.roles,
},
server: {
filesServerUrl: this.filesServerUrl,

View File

@@ -6,6 +6,7 @@ import { Metadata } from '@grpc/grpc-js'
import { SyncResponseHttpRepresentation } from '../../Mapping/Sync/Http/SyncResponseHttpRepresentation'
import { Status } from '@grpc/grpc-js/build/src/constants'
import { Logger } from 'winston'
import { ResponseLocals } from '../../Controller/ResponseLocals'
export class GRPCSyncingServerServiceProxy {
constructor(
@@ -20,24 +21,26 @@ export class GRPCSyncingServerServiceProxy {
response: Response,
payload?: Record<string, unknown> | string,
): Promise<{ status: number; data: unknown }> {
const locals = response.locals as ResponseLocals
return new Promise((resolve, reject) => {
try {
const syncRequest = this.syncRequestGRPCMapper.toProjection(payload as Record<string, unknown>)
const metadata = new Metadata()
metadata.set('x-user-uuid', response.locals.user.uuid)
metadata.set('x-user-uuid', locals.user.uuid)
metadata.set('x-snjs-version', request.headers['x-snjs-version'] as string)
metadata.set('x-read-only-access', response.locals.readOnlyAccess ? 'true' : 'false')
if (response.locals.readOnlyAccess) {
metadata.set('x-read-only-access', locals.readOnlyAccess ? 'true' : 'false')
if (locals.readOnlyAccess) {
this.logger.debug('Syncing with read-only access', {
codeTag: 'GRPCSyncingServerServiceProxy',
userId: response.locals.user.uuid,
userId: locals.user.uuid,
})
}
if (response.locals.session) {
metadata.set('x-session-uuid', response.locals.session.uuid)
if (locals.session) {
metadata.set('x-session-uuid', locals.session.uuid)
}
metadata.set('x-is-free-user', response.locals.isFreeUser ? 'true' : 'false')
metadata.set('x-is-free-user', locals.isFreeUser ? 'true' : 'false')
this.syncingClient.syncItems(syncRequest, metadata, (error, syncResponse) => {
if (error) {
@@ -52,7 +55,7 @@ export class GRPCSyncingServerServiceProxy {
if (error.code === Status.INTERNAL) {
this.logger.error(`Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(payload)}`, {
codeTag: 'GRPCSyncingServerServiceProxy',
userId: response.locals.user.uuid,
userId: locals.user.uuid,
})
}
@@ -68,7 +71,7 @@ export class GRPCSyncingServerServiceProxy {
) {
this.logger.error(`Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(payload)}`, {
codeTag: 'GRPCSyncingServerServiceProxy.catch',
userId: response.locals.user.uuid,
userId: locals.user.uuid,
})
}

View File

@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.177.18](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.17...@standardnotes/auth-server@1.177.18) (2024-01-15)
### Bug Fixes
* **auth:** add more logs to syncing subscription ([c7217a9](https://github.com/standardnotes/server/commit/c7217a92ba89d8b5f4963a832aa7561dd146ca0d))
* **auth:** add renewal for shared offline subscriptions ([045358d](https://github.com/standardnotes/server/commit/045358ddbf300996a23bba8d6945b1d7b5f6e862))
## [1.177.17](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.16...@standardnotes/auth-server@1.177.17) (2024-01-15)
### Bug Fixes
* **auth:** add debug logs for subscription sync requested event ([351e18f](https://github.com/standardnotes/server/commit/351e18f6389c2dbaa2107e6549be9928c2e8834f))
## [1.177.16](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.15...@standardnotes/auth-server@1.177.16) (2024-01-15)
### Bug Fixes
* **auth:** update shared subscriptions upon subscription sync ([#1022](https://github.com/standardnotes/server/issues/1022)) ([d7a1c66](https://github.com/standardnotes/server/commit/d7a1c667dd62dacc1ef15f2a4f408dc07045fcad))
## [1.177.15](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.14...@standardnotes/auth-server@1.177.15) (2024-01-09)
### Bug Fixes
* **auth:** check for user agent persisting on session during a session refresh ([#1016](https://github.com/standardnotes/server/issues/1016)) ([0b46eff](https://github.com/standardnotes/server/commit/0b46eff16ea0c32cac91ead04474303500359f4f))
## [1.177.14](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.13...@standardnotes/auth-server@1.177.14) (2024-01-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.177.13](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.12...@standardnotes/auth-server@1.177.13) (2024-01-04)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.177.12](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.11...@standardnotes/auth-server@1.177.12) (2024-01-03)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -35,6 +35,7 @@ import { AuthService } from '@standardnotes/grpc'
import { AuthenticateRequest } from '../src/Domain/UseCase/AuthenticateRequest'
import { CreateCrossServiceToken } from '../src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
import { ResponseLocals } from '../src/Infra/InversifyExpressUtils/ResponseLocals'
const container = new ContainerConfigLoader()
void container.load().then((container) => {
@@ -59,12 +60,13 @@ void container.load().then((container) => {
server.setErrorConfig((app) => {
app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
const locals = response.locals as ResponseLocals
logger.error(`${error.stack}`, {
method: request.method,
url: request.url,
snjs: request.headers['x-snjs-version'],
application: request.headers['x-application-version'],
userId: response.locals.user ? response.locals.user.uuid : undefined,
userId: locals.user ? locals.user.uuid : undefined,
})
response.status(500).send({

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.177.12",
"version": "1.177.18",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -284,6 +284,7 @@ import { AccountDeletionVerificationPassedEventHandler } from '../Domain/Handler
import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -986,7 +987,18 @@ export class ContainerConfigLoader {
.toConstantValue(new CleanupExpiredSessions(container.get(TYPES.Auth_SessionRepository)))
container.bind<AuthenticateUser>(TYPES.Auth_AuthenticateUser).to(AuthenticateUser)
container.bind<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest)
container.bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken).to(RefreshSessionToken)
container
.bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken)
.toConstantValue(
new RefreshSessionToken(
container.get<SessionServiceInterface>(TYPES.Auth_SessionService),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<GetSetting>(TYPES.Auth_GetSetting),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container.bind<SignIn>(TYPES.Auth_SignIn).to(SignIn)
container
.bind<VerifyMFA>(TYPES.Auth_VerifyMFA)
@@ -1409,6 +1421,7 @@ export class ContainerConfigLoader {
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
container.get<RenewSharedSubscriptions>(TYPES.Auth_RenewSharedSubscriptions),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
@@ -1708,7 +1721,6 @@ export class ContainerConfigLoader {
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
.toConstantValue(
new BaseUsersController(
container.get<UpdateUser>(TYPES.Auth_UpdateUser),
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription),
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),

View File

@@ -34,6 +34,19 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
event.payload.timestamp,
)
const renewalResult = await this.renewSharedSubscriptions.execute({
inviterEmail: event.payload.userEmail,
newSubscriptionId: event.payload.subscriptionId,
newSubscriptionName: event.payload.subscriptionName,
newSubscriptionExpiresAt: event.payload.subscriptionExpiresAt,
timestamp: event.payload.timestamp,
})
if (renewalResult.isFailed()) {
this.logger.error(`Could not renew shared offline subscriptions: ${renewalResult.getError()}`, {
subscriptionId: event.payload.subscriptionId,
})
}
await this.roleService.setOfflineUserRole(offlineUserSubscription)
return

View File

@@ -16,6 +16,7 @@ import { OfflineSettingName } from '../Setting/OfflineSettingName'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
import { RenewSharedSubscriptions } from '../UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
export class SubscriptionSyncRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
@@ -27,11 +28,20 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
private setSettingValue: SetSettingValue,
private offlineSettingService: OfflineSettingServiceInterface,
private contentDecoder: ContentDecoderInterface,
private renewSharedSubscriptions: RenewSharedSubscriptions,
private logger: Logger,
) {}
async handle(event: SubscriptionSyncRequestedEvent): Promise<void> {
this.logger.info('Subscription sync requested', {
subscriptionId: event.payload.subscriptionId,
})
if (event.payload.offline) {
this.logger.info('Syncing offline subscription', {
subscriptionId: event.payload.subscriptionId,
})
const offlineUserSubscription = await this.createOrUpdateOfflineSubscription(
event.payload.subscriptionId,
event.payload.subscriptionName,
@@ -41,6 +51,19 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
event.payload.timestamp,
)
const renewalResult = await this.renewSharedSubscriptions.execute({
inviterEmail: event.payload.userEmail,
newSubscriptionId: event.payload.subscriptionId,
newSubscriptionName: event.payload.subscriptionName,
newSubscriptionExpiresAt: event.payload.subscriptionExpiresAt,
timestamp: event.payload.timestamp,
})
if (renewalResult.isFailed()) {
this.logger.error(`Could not renew shared offline subscriptions for user: ${renewalResult.getError()}`, {
subscriptionId: event.payload.subscriptionId,
})
}
await this.roleService.setOfflineUserRole(offlineUserSubscription)
const offlineFeaturesTokenDecoded = this.contentDecoder.decode(
@@ -60,11 +83,19 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
value: offlineFeaturesTokenDecoded.extensionKey,
})
this.logger.info('Offline subscription synced', {
subscriptionId: event.payload.subscriptionId,
})
return
}
const usernameOrError = Username.create(event.payload.userEmail)
if (usernameOrError.isFailed()) {
this.logger.warn(`Could not sync subscription: ${usernameOrError.getError()}`, {
subscriptionId: event.payload.subscriptionId,
})
return
}
const username = usernameOrError.getValue()
@@ -72,10 +103,18 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
const user = await this.userRepository.findOneByUsernameOrEmail(username)
if (user === null) {
this.logger.warn(`Could not find user with email: ${username.value}`)
this.logger.warn(`Could not find user with email: ${username.value}`, {
subscriptionId: event.payload.subscriptionId,
})
return
}
this.logger.info('Syncing subscription', {
userId: user.uuid,
subscriptionId: event.payload.subscriptionId,
})
const userSubscription = await this.createOrUpdateSubscription(
event.payload.subscriptionId,
event.payload.subscriptionName,
@@ -85,6 +124,19 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
event.payload.timestamp,
)
const renewalResult = await this.renewSharedSubscriptions.execute({
inviterEmail: user.email,
newSubscriptionId: event.payload.subscriptionId,
newSubscriptionName: event.payload.subscriptionName,
newSubscriptionExpiresAt: event.payload.subscriptionExpiresAt,
timestamp: event.payload.timestamp,
})
if (renewalResult.isFailed()) {
this.logger.error(`Could not renew shared subscriptions for user: ${renewalResult.getError()}`, {
userId: user.uuid,
})
}
await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({
@@ -107,6 +159,11 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
if (result.isFailed()) {
this.logger.error(`Could not set extension key for user ${user.uuid}`)
}
this.logger.info('Subscription synced', {
userId: user.uuid,
subscriptionId: event.payload.subscriptionId,
})
}
private async createOrUpdateSubscription(

View File

@@ -7,6 +7,10 @@ import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { GetSetting } from './GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
import { LogSessionUserAgentOption } from '@standardnotes/settings'
import { Setting } from '../Setting/Setting'
describe('RefreshSessionToken', () => {
let sessionService: SessionServiceInterface
@@ -14,16 +18,20 @@ describe('RefreshSessionToken', () => {
let domainEventFactory: DomainEventFactoryInterface
let domainEventPublisher: DomainEventPublisherInterface
let timer: TimerInterface
let getSetting: GetSetting
let logger: Logger
const createUseCase = () =>
new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, logger)
new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, getSetting, logger)
beforeEach(() => {
session = {} as jest.Mocked<Session>
session.uuid = '1-2-3'
session.refreshExpiration = new Date(123)
getSetting = {} as jest.Mocked<GetSetting>
getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found'))
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true)
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
@@ -69,6 +77,35 @@ describe('RefreshSessionToken', () => {
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should refresh session token and update user agent if enabled', async () => {
getSetting.execute = jest.fn().mockReturnValue(
Result.ok({
setting: {} as jest.Mocked<Setting>,
decryptedValue: LogSessionUserAgentOption.Enabled,
}),
)
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(sessionService.refreshTokens).toHaveBeenCalledWith({ session, isEphemeral: false })
expect(result).toEqual({
success: true,
sessionPayload: {
access_token: 'token1',
refresh_token: 'token2',
access_expiration: 123,
refresh_expiration: 234,
},
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should refresh a session token even if publishing domain event fails', async () => {
domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test'))

View File

@@ -1,23 +1,24 @@
import { inject, injectable } from 'inversify'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { SettingName } from '@standardnotes/domain-core'
import { LogSessionUserAgentOption } from '@standardnotes/settings'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { RefreshSessionTokenResponse } from './RefreshSessionTokenResponse'
import { RefreshSessionTokenDTO } from './RefreshSessionTokenDTO'
import { GetSetting } from './GetSetting/GetSetting'
@injectable()
export class RefreshSessionToken {
constructor(
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface,
@inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
private sessionService: SessionServiceInterface,
private domainEventFactory: DomainEventFactoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private timer: TimerInterface,
private getSetting: GetSetting,
private logger: Logger,
) {}
async execute(dto: RefreshSessionTokenDTO): Promise<RefreshSessionTokenResponse> {
@@ -46,7 +47,9 @@ export class RefreshSessionToken {
}
}
session.userAgent = dto.userAgent
if (await this.isLoggingUserAgentEnabledOnSessions(session.userUuid)) {
session.userAgent = dto.userAgent
}
const sessionPayload = await this.sessionService.refreshTokens({ session, isEphemeral })
@@ -64,4 +67,19 @@ export class RefreshSessionToken {
userUuid: session.userUuid,
}
}
private async isLoggingUserAgentEnabledOnSessions(userUuid: string): Promise<boolean> {
const loggingSettingOrError = await this.getSetting.execute({
settingName: SettingName.NAMES.LogSessionUserAgent,
decrypted: true,
userUuid: userUuid,
allowSensitiveRetrieval: true,
})
if (loggingSettingOrError.isFailed()) {
return true
}
const loggingSetting = loggingSettingOrError.getValue()
return loggingSetting.decryptedValue === LogSessionUserAgentOption.Enabled
}
}

View File

@@ -4,14 +4,12 @@ import {
controller,
httpDelete,
httpGet,
httpPatch,
httpPut,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
results,
} from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount'
import { UpdateUser } from '../../Domain/UseCase/UpdateUser'
import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/GetUserSubscription'
import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
@@ -21,26 +19,13 @@ import { BaseUsersController } from './Base/BaseUsersController'
@controller('/users')
export class AnnotatedUsersController extends BaseUsersController {
constructor(
@inject(TYPES.Auth_UpdateUser) override updateUser: UpdateUser,
@inject(TYPES.Auth_DeleteAccount) override doDeleteAccount: DeleteAccount,
@inject(TYPES.Auth_GetUserSubscription) override doGetUserSubscription: GetUserSubscription,
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
) {
super(
updateUser,
doDeleteAccount,
doGetUserSubscription,
clearLoginAttempts,
increaseLoginAttempts,
changeCredentialsUseCase,
)
}
@httpPatch('/:userId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async update(request: Request, response: Response): Promise<results.JsonResult> {
return super.update(request, response)
super(doDeleteAccount, doGetUserSubscription, clearLoginAttempts, increaseLoginAttempts, changeCredentialsUseCase)
}
@httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)

View File

@@ -8,6 +8,7 @@ import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAtte
import { SignIn } from '../../../Domain/UseCase/SignIn'
import { VerifyMFA } from '../../../Domain/UseCase/VerifyMFA'
import { AuthController } from '../../../Controller/AuthController'
import { ResponseLocals } from '../ResponseLocals'
import { BaseHttpController, results } from 'inversify-express-utils'
export class BaseAuthController extends BaseHttpController {
@@ -37,9 +38,11 @@ export class BaseAuthController extends BaseHttpController {
}
async params(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.session) {
const locals = response.locals as ResponseLocals
if (locals.session) {
const result = await this.getUserKeyParams.execute({
email: response.locals.user.email,
email: locals.user.email,
authenticated: true,
})
@@ -145,6 +148,8 @@ export class BaseAuthController extends BaseHttpController {
}
async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
if (!request.body.code_challenge) {
return this.json(
{
@@ -156,9 +161,9 @@ export class BaseAuthController extends BaseHttpController {
)
}
if (response.locals.session) {
if (locals.session) {
const result = await this.getUserKeyParams.execute({
email: response.locals.user.email,
email: locals.user.email,
authenticated: true,
codeChallenge: request.body.code_challenge as string,
})
@@ -248,8 +253,10 @@ export class BaseAuthController extends BaseHttpController {
}
async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authController.generateRecoveryCodes({
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
})
return this.json(result.data, result.status)
@@ -280,8 +287,10 @@ export class BaseAuthController extends BaseHttpController {
}
async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
const locals = response.locals as ResponseLocals
const result = await this.authController.signOut({
readOnlyAccess: response.locals.readOnlyAccess,
readOnlyAccess: locals.readOnlyAccess,
authorizationHeader: <string>request.headers.authorization,
})

View File

@@ -3,6 +3,7 @@ import { Request, Response } from 'express'
import { AuthenticatorsController } from '../../../Controller/AuthenticatorsController'
import { BaseHttpController, results } from 'inversify-express-utils'
import { ResponseLocals } from '../ResponseLocals'
export class BaseAuthenticatorsController extends BaseHttpController {
constructor(
@@ -30,16 +31,20 @@ export class BaseAuthenticatorsController extends BaseHttpController {
}
async list(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authenticatorsController.list({
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
})
return this.json(result.data, result.status)
}
async delete(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authenticatorsController.delete({
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
authenticatorId: request.params.authenticatorId,
})
@@ -47,17 +52,21 @@ export class BaseAuthenticatorsController extends BaseHttpController {
}
async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authenticatorsController.generateRegistrationOptions({
username: response.locals.user.email,
userUuid: response.locals.user.uuid,
username: locals.user.email,
userUuid: locals.user.uuid,
})
return this.json(result.data, result.status)
}
async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authenticatorsController.verifyRegistrationResponse({
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
attestationResponse: request.body.attestationResponse,
})

View File

@@ -3,6 +3,7 @@ import { Request, Response } from 'express'
import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { BaseHttpController, results } from 'inversify-express-utils'
import { ResponseLocals } from '../ResponseLocals'
export class BaseFeaturesController extends BaseHttpController {
constructor(
@@ -17,7 +18,9 @@ export class BaseFeaturesController extends BaseHttpController {
}
async getFeatures(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
const locals = response.locals as ResponseLocals
if (request.params.userUuid !== locals.user.uuid) {
return this.json(
{
error: {

View File

@@ -4,6 +4,7 @@ import { Request, Response } from 'express'
import { CreateListedAccount } from '../../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
import { BaseHttpController, results } from 'inversify-express-utils'
import { ResponseLocals } from '../ResponseLocals'
export class BaseListedController extends BaseHttpController {
constructor(
@@ -18,7 +19,9 @@ export class BaseListedController extends BaseHttpController {
}
async createListedAccount(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
const locals = response.locals as ResponseLocals
if (locals.readOnlyAccess) {
return this.json(
{
error: {
@@ -31,8 +34,8 @@ export class BaseListedController extends BaseHttpController {
}
await this.doCreateListedAccount.execute({
userUuid: response.locals.user.uuid,
userEmail: response.locals.user.email,
userUuid: locals.user.uuid,
userEmail: locals.user.email,
})
return this.json({

View File

@@ -8,6 +8,7 @@ import { AuthenticateOfflineSubscriptionToken } from '../../../Domain/UseCase/Au
import { CreateOfflineSubscriptionToken } from '../../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { GetUserOfflineSubscription } from '../../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
import { OfflineResponseLocals } from '../OfflineResponseLocals'
export class BaseOfflineController extends BaseHttpController {
constructor(
@@ -30,8 +31,10 @@ export class BaseOfflineController extends BaseHttpController {
}
async getOfflineFeatures(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as OfflineResponseLocals
const result = await this.doGetUserFeatures.execute({
email: response.locals.offlineUserEmail,
email: locals.userEmail,
offline: true,
})
@@ -115,8 +118,10 @@ export class BaseOfflineController extends BaseHttpController {
}
async getSubscription(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as OfflineResponseLocals
const result = await this.getUserOfflineSubscription.execute({
userEmail: response.locals.userEmail,
userEmail: locals.userEmail,
})
if (result.success) {

View File

@@ -6,6 +6,7 @@ import { ErrorTag } from '@standardnotes/responses'
import { DeleteOtherSessionsForUser } from '../../../Domain/UseCase/DeleteOtherSessionsForUser'
import { DeleteSessionForUser } from '../../../Domain/UseCase/DeleteSessionForUser'
import { RefreshSessionToken } from '../../../Domain/UseCase/RefreshSessionToken'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSessionController extends BaseHttpController {
constructor(
@@ -24,7 +25,9 @@ export class BaseSessionController extends BaseHttpController {
}
async deleteSession(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
if (response.locals.readOnlyAccess) {
const locals = response.locals as ResponseLocals
if (locals.readOnlyAccess) {
return this.json(
{
error: {
@@ -36,7 +39,7 @@ export class BaseSessionController extends BaseHttpController {
)
}
if (!request.body.uuid) {
if (!request.body.uuid || !locals.session) {
return this.json(
{
error: {
@@ -47,7 +50,7 @@ export class BaseSessionController extends BaseHttpController {
)
}
if (request.body.uuid === response.locals.session.uuid) {
if (request.body.uuid === locals.session.uuid) {
return this.json(
{
error: {
@@ -59,7 +62,7 @@ export class BaseSessionController extends BaseHttpController {
}
const useCaseResponse = await this.deleteSessionForUser.execute({
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
sessionUuid: request.body.uuid,
})
@@ -74,7 +77,7 @@ export class BaseSessionController extends BaseHttpController {
)
}
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
response.setHeader('x-invalidate-cache', locals.user.uuid)
return this.statusCode(204)
}
@@ -83,7 +86,9 @@ export class BaseSessionController extends BaseHttpController {
_request: Request,
response: Response,
): Promise<results.JsonResult | results.StatusCodeResult> {
if (response.locals.readOnlyAccess) {
const locals = response.locals as ResponseLocals
if (locals.readOnlyAccess) {
return this.json(
{
error: {
@@ -95,7 +100,7 @@ export class BaseSessionController extends BaseHttpController {
)
}
if (!response.locals.user) {
if (!locals.user || !locals.session) {
return this.json(
{
error: {
@@ -107,12 +112,12 @@ export class BaseSessionController extends BaseHttpController {
}
await this.deleteOtherSessionsForUser.execute({
userUuid: response.locals.user.uuid,
currentSessionUuid: response.locals.session.uuid,
userUuid: locals.user.uuid,
currentSessionUuid: locals.session.uuid,
markAsRevoked: true,
})
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
response.setHeader('x-invalidate-cache', locals.user.uuid)
return this.statusCode(204)
}

View File

@@ -9,6 +9,7 @@ import { Session } from '../../../Domain/Session/Session'
import { BaseHttpController, results } from 'inversify-express-utils'
import { User } from '../../../Domain/User/User'
import { SessionProjector } from '../../../Projection/SessionProjector'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSessionsController extends BaseHttpController {
constructor(
@@ -67,12 +68,14 @@ export class BaseSessionsController extends BaseHttpController {
}
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
const locals = response.locals as ResponseLocals
if (locals.readOnlyAccess) {
return this.json([])
}
const useCaseResponse = await this.getActiveSessionsForUser.execute({
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
})
return this.json(
@@ -80,7 +83,7 @@ export class BaseSessionsController extends BaseHttpController {
this.sessionProjector.projectCustom(
SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
session,
response.locals.session,
locals.session,
),
),
)

View File

@@ -13,6 +13,7 @@ import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting
import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation'
import { SettingHttpRepresentation } from '../../../Mapping/Http/SettingHttpRepresentation'
import { TriggerPostSettingUpdateActions } from '../../../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSettingsController extends BaseHttpController {
constructor(
@@ -40,7 +41,9 @@ export class BaseSettingsController extends BaseHttpController {
}
async getSettings(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
const locals = response.locals as ResponseLocals
if (request.params.userUuid !== locals.user.uuid) {
return this.json(
{
error: {
@@ -86,7 +89,9 @@ export class BaseSettingsController extends BaseHttpController {
}
async getSetting(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
const locals = response.locals as ResponseLocals
if (request.params.userUuid !== locals.user.uuid) {
return this.json(
{
error: {
@@ -135,7 +140,9 @@ export class BaseSettingsController extends BaseHttpController {
}
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
if (response.locals.readOnlyAccess) {
const locals = response.locals as ResponseLocals
if (locals.readOnlyAccess) {
return this.json(
{
error: {
@@ -147,7 +154,7 @@ export class BaseSettingsController extends BaseHttpController {
)
}
if (request.params.userUuid !== response.locals.user.uuid) {
if (request.params.userUuid !== locals.user.uuid) {
return this.json(
{
error: {
@@ -163,7 +170,7 @@ export class BaseSettingsController extends BaseHttpController {
const result = await this.setSettingValue.execute({
settingName: name,
value,
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
checkUserPermissions: true,
})
@@ -181,8 +188,8 @@ export class BaseSettingsController extends BaseHttpController {
const triggerResult = await this.triggerPostSettingUpdateActions.execute({
updatedSettingName: setting.props.name,
userUuid: response.locals.user.uuid,
userEmail: response.locals.user.email,
userUuid: locals.user.uuid,
userEmail: locals.user.email,
unencryptedValue: value,
})
if (triggerResult.isFailed()) {
@@ -196,7 +203,9 @@ export class BaseSettingsController extends BaseHttpController {
}
async deleteSetting(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
const locals = response.locals as ResponseLocals
if (locals.readOnlyAccess) {
return this.json(
{
error: {
@@ -208,7 +217,7 @@ export class BaseSettingsController extends BaseHttpController {
)
}
if (request.params.userUuid !== response.locals.user.uuid) {
if (request.params.userUuid !== locals.user.uuid) {
return this.json(
{
error: {

View File

@@ -4,7 +4,8 @@ import { BaseHttpController, results } from 'inversify-express-utils'
import { ApiVersion } from '@standardnotes/api'
import { SubscriptionInvitesController } from '../../../Controller/SubscriptionInvitesController'
import { Role } from '../../../Domain/Role/Role'
import { ResponseLocals } from '../ResponseLocals'
import { Role } from '@standardnotes/security'
export class BaseSubscriptionInvitesController extends BaseHttpController {
constructor(
@@ -23,12 +24,14 @@ export class BaseSubscriptionInvitesController extends BaseHttpController {
}
async acceptInvite(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.subscriptionInvitesController.acceptInvite({
api: request.query.api as ApiVersion,
inviteUuid: request.params.inviteUuid,
})
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
response.setHeader('x-invalidate-cache', locals.user.uuid)
return this.json(result.data, result.status)
}
@@ -43,30 +46,36 @@ export class BaseSubscriptionInvitesController extends BaseHttpController {
}
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.subscriptionInvitesController.invite({
...request.body,
inviterEmail: response.locals.user.email,
inviterUuid: response.locals.user.uuid,
inviterRoles: response.locals.roles.map((role: Role) => role.name),
inviterEmail: locals.user.email,
inviterUuid: locals.user.uuid,
inviterRoles: locals.roles.map((role: Role) => role.name),
})
return this.json(result.data, result.status)
}
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.subscriptionInvitesController.cancelInvite({
...request.body,
inviteUuid: request.params.inviteUuid,
inviterEmail: response.locals.user.email,
inviterEmail: locals.user.email,
})
return this.json(result.data, result.status)
}
async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.subscriptionInvitesController.listInvites({
...request.body,
inviterEmail: response.locals.user.email,
inviterEmail: locals.user.email,
})
return this.json(result.data, result.status)

View File

@@ -6,6 +6,7 @@ import { GetSubscriptionSetting } from '../../../Domain/UseCase/GetSubscriptionS
import { GetSharedOrRegularSubscriptionForUser } from '../../../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser'
import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting'
import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSubscriptionSettingsController extends BaseHttpController {
constructor(
@@ -22,8 +23,10 @@ export class BaseSubscriptionSettingsController extends BaseHttpController {
}
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const subscriptionOrError = await this.getSharedOrRegularSubscription.execute({
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
})
if (subscriptionOrError.isFailed()) {
return this.json(

View File

@@ -9,6 +9,7 @@ import { CreateSubscriptionToken } from '../../../Domain/UseCase/CreateSubscript
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
import { User } from '../../../Domain/User/User'
import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSubscriptionTokensController extends BaseHttpController {
constructor(
@@ -29,7 +30,9 @@ export class BaseSubscriptionTokensController extends BaseHttpController {
}
async createToken(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
const locals = response.locals as ResponseLocals
if (locals.readOnlyAccess) {
return this.json(
{
error: {
@@ -42,7 +45,7 @@ export class BaseSubscriptionTokensController extends BaseHttpController {
}
const result = await this.createSubscriptionToken.execute({
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
})
return this.json({

View File

@@ -3,6 +3,7 @@ import { BaseHttpController, results } from 'inversify-express-utils'
import { Request, Response } from 'express'
import { UserRequestsController } from '../../../Controller/UserRequestsController'
import { ResponseLocals } from '../ResponseLocals'
export class BaseUserRequestsController extends BaseHttpController {
constructor(
@@ -17,10 +18,12 @@ export class BaseUserRequestsController extends BaseHttpController {
}
async submitRequest(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.userRequestsController.submitUserRequest({
requestType: request.body.requestType,
userUuid: response.locals.user.uuid,
userEmail: response.locals.user.email,
userUuid: locals.user.uuid,
userEmail: locals.user.email,
})
return this.json(result.data, result.status)

View File

@@ -7,12 +7,11 @@ import { ClearLoginAttempts } from '../../../Domain/UseCase/ClearLoginAttempts'
import { DeleteAccount } from '../../../Domain/UseCase/DeleteAccount/DeleteAccount'
import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription/GetUserSubscription'
import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
import { ErrorTag } from '@standardnotes/responses'
import { ResponseLocals } from '../ResponseLocals'
export class BaseUsersController extends BaseHttpController {
constructor(
protected updateUser: UpdateUser,
protected doDeleteAccount: DeleteAccount,
protected doGetUserSubscription: GetUserSubscription,
protected clearLoginAttempts: ClearLoginAttempts,
@@ -23,61 +22,16 @@ export class BaseUsersController extends BaseHttpController {
super()
if (this.controllerContainer !== undefined) {
this.controllerContainer.register('auth.users.update', this.update.bind(this))
this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this))
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
this.controllerContainer.register('auth.users.delete', this.deleteAccount.bind(this))
}
}
async update(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(
{
error: {
tag: ErrorTag.ReadOnlyAccess,
message: 'Session has read-only access.',
},
},
401,
)
}
if (request.params.userId !== response.locals.user.uuid) {
return this.json(
{
error: {
message: 'Operation not allowed.',
},
},
401,
)
}
const updateResult = await this.updateUser.execute({
user: response.locals.user,
updatedWithUserAgent: <string>request.headers['user-agent'],
apiVersion: request.body.api,
})
if (updateResult.success) {
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
return this.json(updateResult.authResponse)
}
return this.json(
{
error: {
message: 'Could not update user.',
},
},
400,
)
}
async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
const locals = response.locals as ResponseLocals
if (request.params.userUuid !== locals.user.uuid) {
return this.json(
{
error: {
@@ -107,7 +61,9 @@ export class BaseUsersController extends BaseHttpController {
}
async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
const locals = response.locals as ResponseLocals
if (request.params.userUuid !== locals.user.uuid) {
return this.json(
{
error: {
@@ -130,7 +86,9 @@ export class BaseUsersController extends BaseHttpController {
}
async changeCredentials(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
const locals = response.locals as ResponseLocals
if (locals.readOnlyAccess) {
return this.json(
{
error: {
@@ -175,7 +133,7 @@ export class BaseUsersController extends BaseHttpController {
400,
)
}
const usernameOrError = Username.create(response.locals.user.email)
const usernameOrError = Username.create(locals.user.email)
if (usernameOrError.isFailed()) {
return this.json(
{
@@ -202,7 +160,7 @@ export class BaseUsersController extends BaseHttpController {
})
if (changeCredentialsResult.isFailed()) {
await this.increaseLoginAttempts.execute({ email: response.locals.user.email })
await this.increaseLoginAttempts.execute({ email: locals.user.email })
return this.json(
{
@@ -214,9 +172,9 @@ export class BaseUsersController extends BaseHttpController {
)
}
await this.clearLoginAttempts.execute({ email: response.locals.user.email })
await this.clearLoginAttempts.execute({ email: locals.user.email })
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
response.setHeader('x-invalidate-cache', locals.user.uuid)
return this.json(changeCredentialsResult.getValue())
}

View File

@@ -6,6 +6,7 @@ import { ValetTokenOperation } from '@standardnotes/security'
import { CreateValetToken } from '../../../Domain/UseCase/CreateValetToken/CreateValetToken'
import { CreateValetTokenPayload } from '../../../Domain/ValetToken/CreateValetTokenPayload'
import { ResponseLocals } from '../ResponseLocals'
export class BaseValetTokenController extends BaseHttpController {
constructor(
@@ -20,9 +21,11 @@ export class BaseValetTokenController extends BaseHttpController {
}
public async create(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const payload: CreateValetTokenPayload = request.body
if (response.locals.readOnlyAccess && payload.operation !== 'read') {
if (locals.readOnlyAccess && payload.operation !== 'read') {
return this.json(
{
error: {
@@ -50,7 +53,7 @@ export class BaseValetTokenController extends BaseHttpController {
}
const createValetKeyResponse = await this.createValetKey.execute({
userUuid: response.locals.user.uuid,
userUuid: locals.user.uuid,
operation: payload.operation as ValetTokenOperation,
resources: payload.resources,
})

View File

@@ -2,6 +2,7 @@ import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/sec
import { NextFunction, Request, Response } from 'express'
import { BaseMiddleware } from 'inversify-express-utils'
import { Logger } from 'winston'
import { ResponseLocals } from '../ResponseLocals'
export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware {
constructor(
@@ -34,10 +35,12 @@ export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware {
return
}
response.locals.user = token.user
response.locals.roles = token.roles
response.locals.session = token.session
response.locals.readOnlyAccess = token.session?.readonly_access ?? false
Object.assign(response.locals, {
user: token.user,
roles: token.roles,
session: token.session,
readOnlyAccess: token.session?.readonly_access ?? false,
} as ResponseLocals)
return next()
} catch (error) {

View File

@@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { OfflineResponseLocals } from '../OfflineResponseLocals'
@injectable()
export class ApiGatewayOfflineAuthMiddleware extends BaseMiddleware {
@@ -48,8 +49,10 @@ export class ApiGatewayOfflineAuthMiddleware extends BaseMiddleware {
return
}
response.locals.featuresToken = token.featuresToken
response.locals.userEmail = token.userEmail
Object.assign(response.locals, {
featuresToken: token.featuresToken,
userEmail: token.userEmail,
} as OfflineResponseLocals)
return next()
} catch (error) {

View File

@@ -44,8 +44,8 @@ describe('OfflineUserAuthMiddleware', () => {
await createMiddleware().handler(request, response, next)
expect(response.locals.offlineUserEmail).toEqual('test@test.com')
expect(response.locals.offlineFeaturesToken).toEqual('offline-features-token')
expect(response.locals.userEmail).toEqual('test@test.com')
expect(response.locals.featuresToken).toEqual('offline-features-token')
expect(next).toHaveBeenCalled()
})

View File

@@ -5,6 +5,7 @@ import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { OfflineSettingName } from '../../../Domain/Setting/OfflineSettingName'
import { OfflineSettingRepositoryInterface } from '../../../Domain/Setting/OfflineSettingRepositoryInterface'
import { OfflineResponseLocals } from '../OfflineResponseLocals'
@injectable()
export class OfflineUserAuthMiddleware extends BaseMiddleware {
@@ -47,8 +48,10 @@ export class OfflineUserAuthMiddleware extends BaseMiddleware {
return
}
response.locals.offlineUserEmail = offlineFeaturesTokenSetting.email
response.locals.offlineFeaturesToken = offlineFeaturesTokenSetting.value
Object.assign(response.locals, {
featuresToken: offlineFeaturesTokenSetting.value,
userEmail: offlineFeaturesTokenSetting.email,
} as OfflineResponseLocals)
return next()
} catch (error) {

View File

@@ -0,0 +1,4 @@
export interface OfflineResponseLocals {
userEmail: string
featuresToken: string
}

View File

@@ -0,0 +1,20 @@
import { Role } from '@standardnotes/security'
export interface ResponseLocals {
user: {
uuid: string
email: string
}
roles: Array<Role>
session?: {
uuid: string
api_version: string
created_at: string
updated_at: string
device_info: string
readonly_access: boolean
access_expiration: string
refresh_expiration: string
}
readOnlyAccess: boolean
}

View File

@@ -84,6 +84,7 @@ export class TypeORMUserSubscriptionRepository implements UserSubscriptionReposi
userUuid,
subscriptionId,
})
.orderBy('ends_at', 'DESC')
.getOne()
}

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.9](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.37.8...@standardnotes/files-server@1.37.9) (2024-01-10)
### Bug Fixes
* add dedicated http code response upon a request with too large payload ([#1019](https://github.com/standardnotes/server/issues/1019)) ([6062f85](https://github.com/standardnotes/server/commit/6062f850000477983315d2d9b7c913956f755ebb))
## [1.37.8](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.37.7...@standardnotes/files-server@1.37.8) (2024-01-04)
**Note:** Version bump only for package @standardnotes/files-server
## [1.37.7](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.37.6...@standardnotes/files-server@1.37.7) (2024-01-03)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -24,6 +24,10 @@ void container.load().then((container) => {
const env: Env = new Env()
env.load()
const requestPayloadLimit = env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)
? `${+env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)}mb`
: '50mb'
const server = new InversifyExpressServer(container)
server.setConfig((app) => {
@@ -58,9 +62,9 @@ void container.load().then((container) => {
}
}))
/* eslint-enable */
app.use(json({ limit: '50mb' }))
app.use(raw({ limit: '50mb', type: 'application/octet-stream' }))
app.use(urlencoded({ extended: true, limit: '50mb' }))
app.use(json({ limit: requestPayloadLimit }))
app.use(raw({ limit: requestPayloadLimit, type: 'application/octet-stream' }))
app.use(urlencoded({ extended: true, limit: requestPayloadLimit }))
app.use(
cors({
exposedHeaders: ['Content-Range', 'Accept-Ranges'],

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.37.7",
"version": "1.37.9",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,88 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.22.57](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.56...@standardnotes/home-server@1.22.57) (2024-01-15)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.56](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.55...@standardnotes/home-server@1.22.56) (2024-01-15)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.55](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.54...@standardnotes/home-server@1.22.55) (2024-01-15)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.54](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.53...@standardnotes/home-server@1.22.54) (2024-01-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.53](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.52...@standardnotes/home-server@1.22.53) (2024-01-10)
### Bug Fixes
* add dedicated http code response upon a request with too large payload ([#1019](https://github.com/standardnotes/server/issues/1019)) ([6062f85](https://github.com/standardnotes/server/commit/6062f850000477983315d2d9b7c913956f755ebb))
## [1.22.52](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.51...@standardnotes/home-server@1.22.52) (2024-01-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.51](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.50...@standardnotes/home-server@1.22.51) (2024-01-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.50](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.49...@standardnotes/home-server@1.22.50) (2024-01-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.49](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.48...@standardnotes/home-server@1.22.49) (2024-01-05)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.48](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.47...@standardnotes/home-server@1.22.48) (2024-01-05)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.47](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.46...@standardnotes/home-server@1.22.47) (2024-01-05)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.46](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.45...@standardnotes/home-server@1.22.46) (2024-01-05)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.45](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.44...@standardnotes/home-server@1.22.45) (2024-01-04)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.44](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.43...@standardnotes/home-server@1.22.44) (2024-01-04)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.43](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.42...@standardnotes/home-server@1.22.43) (2024-01-04)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.42](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.41...@standardnotes/home-server@1.22.42) (2024-01-04)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.41](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.40...@standardnotes/home-server@1.22.41) (2024-01-04)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.40](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.39...@standardnotes/home-server@1.22.40) (2024-01-04)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.39](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.38...@standardnotes/home-server@1.22.39) (2024-01-04)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.38](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.37...@standardnotes/home-server@1.22.38) (2024-01-04)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.37](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.36...@standardnotes/home-server@1.22.37) (2024-01-03)
**Note:** Version bump only for package @standardnotes/home-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.22.37",
"version": "1.22.57",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -53,6 +53,10 @@ export class HomeServer implements HomeServerInterface {
const env: Env = new Env(environmentOverrides)
env.load()
const requestPayloadLimit = env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)
? `${+env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)}mb`
: '50mb'
this.configureLoggers(env, configuration)
const apiGatewayService = new ApiGatewayService(serviceContainer)
@@ -114,8 +118,8 @@ export class HomeServer implements HomeServerInterface {
}
}))
/* eslint-enable */
app.use(json({ limit: '50mb' }))
app.use(raw({ limit: '50mb', type: 'application/octet-stream' }))
app.use(json({ limit: requestPayloadLimit }))
app.use(raw({ limit: requestPayloadLimit, type: 'application/octet-stream' }))
app.use(
text({
type: [
@@ -160,8 +164,24 @@ export class HomeServer implements HomeServerInterface {
const logger: winston.Logger = winston.loggers.get('home-server')
server.setErrorConfig((app) => {
app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {
logger.error(error.stack)
app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
logger.error(`${error.stack}`, {
method: request.method,
url: request.url,
snjs: request.headers['x-snjs-version'],
application: request.headers['x-application-version'],
userId: response.locals.user ? response.locals.user.uuid : undefined,
})
if ('type' in error && error.type === 'entity.too.large') {
response.status(413).send({
error: {
message: 'The request payload is too large.',
},
})
return
}
response.status(500).send({
error: {

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.51.14](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.13...@standardnotes/revisions-server@1.51.14) (2024-01-04)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.51.13](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.12...@standardnotes/revisions-server@1.51.13) (2024-01-03)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.51.13",
"version": "1.51.14",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.27.19](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.18...@standardnotes/scheduler-server@1.27.19) (2024-01-04)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.27.18](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.17...@standardnotes/scheduler-server@1.27.18) (2024-01-03)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.27.18",
"version": "1.27.19",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -41,3 +41,6 @@ FILE_UPLOAD_PATH=
VALET_TOKEN_SECRET=change-me-!
VALET_TOKEN_TTL=7200
REDIS_URL=redis://cache
CACHE_TYPE=redis

View File

@@ -3,6 +3,92 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.6...@standardnotes/syncing-server@1.134.0) (2024-01-12)
### Features
* **syncing-server:** reduced abuse thresholds for free users ([#1021](https://github.com/standardnotes/server/issues/1021)) ([0443de8](https://github.com/standardnotes/server/commit/0443de88ceae3cb7c0793a3457753806b51db6e2))
## [1.133.6](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.5...@standardnotes/syncing-server@1.133.6) (2024-01-10)
### Bug Fixes
* add dedicated http code response upon a request with too large payload ([#1019](https://github.com/standardnotes/server/issues/1019)) ([6062f85](https://github.com/standardnotes/server/commit/6062f850000477983315d2d9b7c913956f755ebb))
## [1.133.5](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.4...@standardnotes/syncing-server@1.133.5) (2024-01-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.133.4](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.3...@standardnotes/syncing-server@1.133.4) (2024-01-08)
### Bug Fixes
* **syncing-server:** disable ot tracing ([a881dd2](https://github.com/standardnotes/server/commit/a881dd2d79d0e394b0b4f5d07e63821c93d45219))
## [1.133.3](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.2...@standardnotes/syncing-server@1.133.3) (2024-01-05)
### Bug Fixes
* **syncing-server:** add traffic abuse check in gRPC coms ([e3cb1fa](https://github.com/standardnotes/server/commit/e3cb1faba46bbfd8741f6c827daa9438934dd710))
* **syncing-server:** remove excessive debug logs ([5c5f988](https://github.com/standardnotes/server/commit/5c5f9880556f14f5cbe4599ac0d639c970495056))
## [1.133.2](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.1...@standardnotes/syncing-server@1.133.2) (2024-01-05)
### Bug Fixes
* **syncing-server:** add debug logs to redis metrics store ([a4ad37f](https://github.com/standardnotes/server/commit/a4ad37f30948ba4292f367240c3dbfca916282ac))
* **syncing-server:** add metadata to transfer breach logs ([73c2cc1](https://github.com/standardnotes/server/commit/73c2cc1222b647e82de3755c7e28d283cd9f872f))
## [1.133.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.0...@standardnotes/syncing-server@1.133.1) (2024-01-05)
### Bug Fixes
* **syncing-server:** add debug logs for checking traffic abuse ([1f4b26d](https://github.com/standardnotes/server/commit/1f4b26d269a92f5b43455ce3a3cf3d4f15f0d099))
* **syncing-server:** error message ([e253825](https://github.com/standardnotes/server/commit/e253825da69d1be6d7bb4d0360f8c3add73516ef))
* **syncing-server:** metadata in logs ([02f4d5c](https://github.com/standardnotes/server/commit/02f4d5c717cef1014930f11b2792868967812042))
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.132.0...@standardnotes/syncing-server@1.133.0) (2024-01-05)
### Features
* **syncing-server:** add traffic abuse checks ([#1014](https://github.com/standardnotes/server/issues/1014)) ([b717334](https://github.com/standardnotes/server/commit/b7173346d2949269b762b023da9ea67b7f327c35))
# [1.132.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.131.0...@standardnotes/syncing-server@1.132.0) (2024-01-04)
### Features
* **syncing-server:** send user based metrics to cloudwatch ([0c3bc0c](https://github.com/standardnotes/server/commit/0c3bc0cae654a6783f85e86995f978cc458d8b5c))
# [1.131.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.130.3...@standardnotes/syncing-server@1.131.0) (2024-01-04)
### Features
* **syncing-server:** store per user content size utilization and item operations metrics ([4dd2eb9](https://github.com/standardnotes/server/commit/4dd2eb9349eb16006d1ebba99c848b8f6c51baf9))
## [1.130.3](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.130.2...@standardnotes/syncing-server@1.130.3) (2024-01-04)
### Bug Fixes
* **syncing-server:** decrease metric expiration time ([f1aa431](https://github.com/standardnotes/server/commit/f1aa431c223714e56942a32ab75e380baf4f84aa))
## [1.130.2](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.130.1...@standardnotes/syncing-server@1.130.2) (2024-01-04)
### Bug Fixes
* **syncing-server:** amount of minutes to process for metrics ([92bb447](https://github.com/standardnotes/server/commit/92bb447cacd0a40f963c2bfa5e61cb0f451959e6))
## [1.130.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.130.0...@standardnotes/syncing-server@1.130.1) (2024-01-04)
### Bug Fixes
* **syncing-server:** skip sending empty metrics ([2c732ff](https://github.com/standardnotes/server/commit/2c732ff713736b845691e5e195f6a99086b6f2d7))
# [1.130.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.129.11...@standardnotes/syncing-server@1.130.0) (2024-01-04)
### Features
* add storing item saving and modifying metrics ([#1013](https://github.com/standardnotes/server/issues/1013)) ([efd816a](https://github.com/standardnotes/server/commit/efd816a627d59f4baaa69aa081c2ad9c88af971c))
## [1.129.11](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.129.10...@standardnotes/syncing-server@1.129.11) (2024-01-03)
### Bug Fixes

View File

@@ -1,10 +1,6 @@
import 'reflect-metadata'
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
import { MapperInterface, ServiceIdentifier } from '@standardnotes/domain-core'
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.SyncingServer })
sdk.start()
import { MapperInterface } from '@standardnotes/domain-core'
import '../src/Infra/InversifyExpressUtils/AnnotatedFallbackController'
import '../src/Infra/InversifyExpressUtils/AnnotatedHealthCheckController'
@@ -29,12 +25,17 @@ import { SyncingServer } from '../src/Infra/gRPC/SyncingServer'
import { SyncItems } from '../src/Domain/UseCase/Syncing/SyncItems/SyncItems'
import { SyncResponseFactoryResolverInterface } from '../src/Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
import { SyncResponse20200115 } from '../src/Domain/Item/SyncResponse/SyncResponse20200115'
import { CheckForTrafficAbuse } from '../src/Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
const container = new ContainerConfigLoader()
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const requestPayloadLimit = env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)
? `${+env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)}mb`
: '50mb'
const server = new InversifyExpressServer(container)
server.setConfig((app) => {
@@ -64,8 +65,8 @@ void container.load().then((container) => {
}
}))
/* eslint-enable */
app.use(json({ limit: '50mb' }))
app.use(urlencoded({ extended: true, limit: '50mb', parameterLimit: 5000 }))
app.use(json({ limit: requestPayloadLimit }))
app.use(urlencoded({ extended: true, limit: requestPayloadLimit, parameterLimit: 5000 }))
app.use(cors())
})
@@ -114,6 +115,14 @@ void container.load().then((container) => {
container.get<SyncItems>(TYPES.Sync_SyncItems),
container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
container.get<MapperInterface<SyncResponse20200115, SyncResponse>>(TYPES.Sync_SyncResponseGRPCMapper),
container.get<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse),
container.get<boolean>(TYPES.Sync_STRICT_ABUSE_PROTECTION),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<winston.Logger>(TYPES.Sync_Logger),
)
@@ -146,14 +155,6 @@ void container.load().then((container) => {
logger.info('gRPC server closed')
}
})
sdk
.shutdown()
.then(() => {
logger.info('OpenTelemetry SDK shut down')
})
.catch((error) => {
logger.error(`Failed to shut down OpenTelemetry SDK: ${error.message}`)
})
})
logger.info(`Server started on port ${process.env.PORT}`)

View File

@@ -0,0 +1,118 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { MetricsStoreInterface } from '../src/Domain/Metrics/MetricsStoreInterface'
import { Metric } from '../src/Domain/Metrics/Metric'
import { Time, TimerInterface } from '@standardnotes/time'
const sendStatistics = async (
metricsStore: MetricsStoreInterface,
timer: TimerInterface,
awsRegion: string,
): Promise<void> => {
const cloudwatchClient = new CloudWatchClient({
region: awsRegion,
})
const minutesToProcess = 30
const metricsToProcess = [Metric.NAMES.ItemCreated, Metric.NAMES.ItemUpdated]
for (const metricToProcess of metricsToProcess) {
for (let i = 0; i <= minutesToProcess; i++) {
const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
const statistics = await metricsStore.getMetricsSummary(
metricToProcess,
timestamp,
timestamp + Time.MicrosecondsInAMinute,
)
if (statistics.sampleCount === 0) {
continue
}
await cloudwatchClient.send(
new PutMetricDataCommand({
Namespace: 'SyncingServer',
MetricData: [
{
MetricName: metricToProcess,
Timestamp: dateNMinutesAgo,
StatisticValues: {
Maximum: statistics.max,
Minimum: statistics.min,
SampleCount: statistics.sampleCount,
Sum: statistics.sum,
},
},
],
}),
)
}
}
const userMetricsToProcess = [Metric.NAMES.ItemOperation, Metric.NAMES.ContentSizeUtilized]
for (const metricToProcess of userMetricsToProcess) {
for (let i = 0; i <= minutesToProcess; i++) {
const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
const statistics = await metricsStore.getUserBasedMetricsSummary(metricToProcess, timestamp)
if (statistics.sampleCount === 0) {
continue
}
await cloudwatchClient.send(
new PutMetricDataCommand({
Namespace: 'SyncingServer',
MetricData: [
{
MetricName: metricToProcess,
Timestamp: dateNMinutesAgo,
StatisticValues: {
Maximum: statistics.max,
Minimum: statistics.min,
SampleCount: statistics.sampleCount,
Sum: statistics.sum,
},
},
],
}),
)
}
}
}
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Sync_Logger)
logger.info('Starting statistics sending')
const metricsStore = container.get<MetricsStoreInterface>(TYPES.Sync_MetricsStore)
const timer = container.get<TimerInterface>(TYPES.Sync_Timer)
const awsRegion = env.get('SNS_AWS_REGION', true)
Promise.resolve(sendStatistics(metricsStore, timer, awsRegion))
.then(() => {
logger.info('Finished statistics sending')
process.exit(0)
})
.catch((error) => {
logger.error('Error while sending statistics', error)
process.exit(1)
})
})

View File

@@ -0,0 +1,11 @@
'use strict'
const path = require('path')
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/statistics.js')))
Object.defineProperty(exports, '__esModule', { value: true })
exports.default = index

View File

@@ -5,15 +5,17 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-web' )
echo "[Docker] Starting Web..."
exec node docker/entrypoint-server.js
;;
'start-worker' )
echo "[Docker] Starting Worker..."
exec node docker/entrypoint-worker.js
;;
'statistics' )
exec node docker/entrypoint-statistics.js
;;
* )
echo "[Docker] Unknown command"
;;

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.129.11",
"version": "1.134.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -32,6 +32,7 @@
"migrate": "yarn clean && yarn build && yarn typeorm migration:run -d dist/src/Bootstrap/DataSource.js"
},
"dependencies": {
"@aws-sdk/client-cloudwatch": "^3.485.0",
"@aws-sdk/client-s3": "^3.484.0",
"@aws-sdk/client-sns": "^3.484.0",
"@aws-sdk/client-sqs": "^3.484.0",

View File

@@ -1,4 +1,5 @@
import * as winston from 'winston'
import Redis from 'ioredis'
import { Container, interfaces } from 'inversify'
import { Env } from './Env'
@@ -162,6 +163,10 @@ import { SyncResponse } from '@standardnotes/grpc'
import { SyncResponseGRPCMapper } from '../Mapping/gRPC/SyncResponseGRPCMapper'
import { AccountDeletionVerificationRequestedEventHandler } from '../Domain/Handler/AccountDeletionVerificationRequestedEventHandler'
import { SendEventToClients } from '../Domain/UseCase/Syncing/SendEventToClients/SendEventToClients'
import { MetricsStoreInterface } from '../Domain/Metrics/MetricsStoreInterface'
import { RedisMetricStore } from '../Infra/Redis/RedisMetricStore'
import { DummyMetricStore } from '../Infra/Dummy/DummyMetricStore'
import { CheckForTrafficAbuse } from '../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -211,6 +216,20 @@ export class ContainerConfigLoader {
const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
const isConfiguredForSelfHosting = env.get('MODE', true) === 'self-hosted'
const isConfiguredForHomeServerOrSelfHosting = isConfiguredForHomeServer || isConfiguredForSelfHosting
const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
if (!isConfiguredForInMemoryCache) {
const redisUrl = env.get('REDIS_URL')
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
let redis
if (isRedisInClusterMode) {
redis = new Redis.Cluster(redisUrl.split(','))
} else {
redis = new Redis(redisUrl)
}
container.bind(TYPES.Sync_Redis).toConstantValue(redis)
}
container
.bind<boolean>(TYPES.Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING)
@@ -454,6 +473,49 @@ export class ContainerConfigLoader {
})
// env vars
container
.bind(TYPES.Sync_STRICT_ABUSE_PROTECTION)
.toConstantValue(env.get('STRICT_ABUSE_PROTECTION', true) === 'true')
container
.bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD)
.toConstantValue(
env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) ? +env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) : 1000,
)
container
.bind(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD)
.toConstantValue(
env.get('FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD', true)
? +env.get('FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD', true)
: 500,
)
container
.bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
.toConstantValue(
env.get('ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
? +env.get('ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
: 5,
)
container
.bind(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD)
.toConstantValue(
env.get('UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
? +env.get('UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
: 100_000_000,
)
container
.bind(TYPES.Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD)
.toConstantValue(
env.get('FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
? +env.get('FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
: 50_000_000,
)
container
.bind(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
.toConstantValue(
env.get('UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
? +env.get('UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
: 5,
)
container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
container
.bind(TYPES.Sync_REVISIONS_FREQUENCY)
@@ -533,6 +595,20 @@ export class ContainerConfigLoader {
),
)
if (isConfiguredForInMemoryCache) {
container.bind<MetricsStoreInterface>(TYPES.Sync_MetricsStore).toConstantValue(new DummyMetricStore())
} else {
container
.bind<MetricsStoreInterface>(TYPES.Sync_MetricsStore)
.toConstantValue(
new RedisMetricStore(
container.get<Redis>(TYPES.Sync_Redis),
container.get<TimerInterface>(TYPES.Sync_Timer),
container.get<Logger>(TYPES.Sync_Logger),
),
)
}
// use cases
container
.bind<GetItems>(TYPES.Sync_GetItems)
@@ -554,6 +630,7 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_Timer),
container.get(TYPES.Sync_DomainEventPublisher),
container.get(TYPES.Sync_DomainEventFactory),
container.get<MetricsStoreInterface>(TYPES.Sync_MetricsStore),
),
)
container
@@ -609,6 +686,7 @@ export class ContainerConfigLoader {
container.get<DetermineSharedVaultOperationOnItem>(TYPES.Sync_DetermineSharedVaultOperationOnItem),
container.get<AddNotificationsForUsers>(TYPES.Sync_AddNotificationsForUsers),
container.get<RemoveNotificationsForUser>(TYPES.Sync_RemoveNotificationsForUser),
container.get<MetricsStoreInterface>(TYPES.Sync_MetricsStore),
),
)
container
@@ -903,6 +981,15 @@ export class ContainerConfigLoader {
context.container.get(TYPES.Sync_SyncResponseFactory20200115),
)
})
container
.bind<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse)
.toConstantValue(
new CheckForTrafficAbuse(
container.get<MetricsStoreInterface>(TYPES.Sync_MetricsStore),
container.get<TimerInterface>(TYPES.Sync_Timer),
container.get<Logger>(TYPES.Sync_Logger),
),
)
// Handlers
container
@@ -1064,11 +1151,20 @@ export class ContainerConfigLoader {
.bind<BaseItemsController>(TYPES.Sync_BaseItemsController)
.toConstantValue(
new BaseItemsController(
container.get<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse),
container.get<SyncItems>(TYPES.Sync_SyncItems),
container.get<CheckIntegrity>(TYPES.Sync_CheckIntegrity),
container.get<GetItem>(TYPES.Sync_GetItem),
container.get<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper),
container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
container.get<Logger>(TYPES.Sync_Logger),
container.get<boolean>(TYPES.Sync_STRICT_ABUSE_PROTECTION),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer),
),
)

View File

@@ -42,6 +42,17 @@ const TYPES = {
Sync_VALET_TOKEN_SECRET: Symbol.for('Sync_VALET_TOKEN_SECRET'),
Sync_VALET_TOKEN_TTL: Symbol.for('Sync_VALET_TOKEN_TTL'),
Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING: Symbol.for('Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING'),
Sync_STRICT_ABUSE_PROTECTION: Symbol.for('Sync_STRICT_ABUSE_PROTECTION'),
Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES: Symbol.for(
'Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES',
),
Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD: Symbol.for('Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD'),
Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD: Symbol.for('Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD'),
Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD: Symbol.for('Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD'),
Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD: Symbol.for('Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD'),
Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES: Symbol.for(
'Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES',
),
// use cases
Sync_SyncItems: Symbol.for('Sync_SyncItems'),
Sync_CheckIntegrity: Symbol.for('Sync_CheckIntegrity'),
@@ -85,6 +96,7 @@ const TYPES = {
Sync_TransferSharedVault: Symbol.for('Sync_TransferSharedVault'),
Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
Sync_DumpItem: Symbol.for('Sync_DumpItem'),
Sync_CheckForTrafficAbuse: Symbol.for('Sync_CheckForTrafficAbuse'),
// Handlers
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for(
@@ -98,6 +110,7 @@ const TYPES = {
Sync_SharedVaultFileMovedEventHandler: Symbol.for('Sync_SharedVaultFileMovedEventHandler'),
Sync_SharedVaultRemovedEventHandler: Symbol.for('Sync_SharedVaultRemovedEventHandler'),
// Services
Sync_MetricsStore: Symbol.for('Sync_MetricsStore'),
Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),
Sync_DomainEventSubscriber: Symbol.for('Sync_DomainEventSubscriber'),

View File

@@ -3,7 +3,7 @@ import {
DomainEventPublisherInterface,
EmailBackupRequestedEvent,
} from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import { EmailLevel, Uuid } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
@@ -32,6 +32,17 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
event: EmailBackupRequestedEvent,
itemRepository: ItemRepositoryInterface,
): Promise<void> {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.error('User uuid is invalid', {
userId: event.payload.userUuid,
codeTag: 'EmailBackupRequestedEventHandler',
})
return
}
const userUuid = userUuidOrError.getValue()
const itemQuery: ItemQuery = {
userUuid: event.payload.userUuid,
sortBy: 'updated_at_timestamp',
@@ -42,6 +53,7 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
const itemUuidBundles = await this.itemTransferCalculator.computeItemUuidBundlesToFetch(
itemContentSizeDescriptors,
this.emailAttachmentMaxByteSize,
userUuid,
)
const backupFileNames: string[] = []

View File

@@ -4,13 +4,18 @@ import { Logger } from 'winston'
import { ItemTransferCalculator } from './ItemTransferCalculator'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
import { Uuid } from '@standardnotes/domain-core'
describe('ItemTransferCalculator', () => {
let logger: Logger
const createCalculator = () => new ItemTransferCalculator(logger)
let userUuid: Uuid
beforeEach(() => {
userUuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
})
@@ -23,7 +28,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50, userUuid)
expect(result).toEqual({
uuids: [
@@ -42,7 +47,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40, userUuid)
expect(result).toEqual({
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
@@ -57,7 +62,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50, userUuid)
expect(result).toEqual({
uuids: [
@@ -76,7 +81,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40, userUuid)
expect(result).toEqual({
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
@@ -93,7 +98,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50, userUuid)
expect(result).toEqual([
[
@@ -111,7 +116,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40, userUuid)
expect(result).toEqual([
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
@@ -126,7 +131,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50, userUuid)
expect(result).toEqual([
[
@@ -144,7 +149,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40, userUuid)
expect(result).toEqual([
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],

View File

@@ -2,6 +2,7 @@ import { Logger } from 'winston'
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
import { Uuid } from '@standardnotes/domain-core'
export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
constructor(private logger: Logger) {}
@@ -9,6 +10,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
async computeItemUuidsToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
userUuid: Uuid,
): Promise<{ uuids: Array<string>; transferLimitBreachedBeforeEndOfItems: boolean }> {
const itemUuidsToFetch = []
let totalContentSizeInBytes = 0
@@ -24,6 +26,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
bytesTransferLimit,
itemUuidsToFetch,
itemContentSizeDescriptors,
userUuid,
})
if (transferLimitBreached) {
@@ -41,6 +44,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
async computeItemUuidBundlesToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
userUuid: Uuid,
): Promise<Array<Array<string>>> {
let itemUuidsToFetch = []
let totalContentSizeInBytes = 0
@@ -56,6 +60,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
bytesTransferLimit,
itemUuidsToFetch,
itemContentSizeDescriptors,
userUuid,
})
if (transferLimitBreached) {
@@ -77,15 +82,20 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
bytesTransferLimit: number
itemUuidsToFetch: Array<string>
itemContentSizeDescriptors: ItemContentSizeDescriptor[]
userUuid: Uuid
}): boolean {
const transferLimitBreached = dto.totalContentSizeInBytes >= dto.bytesTransferLimit
const transferLimitBreachedAtFirstItem =
transferLimitBreached && dto.itemUuidsToFetch.length === 1 && dto.itemContentSizeDescriptors.length > 1
if (transferLimitBreachedAtFirstItem) {
this.logger.warn(
`Item ${dto.itemUuidsToFetch[0]} is breaching the content size transfer limit: ${dto.bytesTransferLimit}`,
)
this.logger.warn('Item is breaching the content size transfer limit at first item in the bundle to fetch.', {
codeTag: 'ItemTransferCalculator',
itemUuid: dto.itemUuidsToFetch[0],
totalContentSizeInBytes: dto.totalContentSizeInBytes,
bytesTransferLimit: dto.bytesTransferLimit,
userId: dto.userUuid.value,
})
}
return transferLimitBreached && !transferLimitBreachedAtFirstItem

View File

@@ -1,12 +1,16 @@
import { Uuid } from '@standardnotes/domain-core'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
export interface ItemTransferCalculatorInterface {
computeItemUuidsToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
userUuid: Uuid,
): Promise<{ uuids: Array<string>; transferLimitBreachedBeforeEndOfItems: boolean }>
computeItemUuidBundlesToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
userUuid: Uuid,
): Promise<Array<Array<string>>>
}

View File

@@ -0,0 +1,16 @@
import { Metric } from './Metric'
describe('Metric', () => {
it('should create a value object', () => {
const valueOrError = Metric.create({ name: 'ItemCreated', timestamp: 0 })
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().props.name).toEqual('ItemCreated')
})
it('should not create an invalid value object', () => {
const valueOrError = Metric.create({ name: 'InvalidMetricName', timestamp: 0 })
expect(valueOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,21 @@
import { Result, ValueObject } from '@standardnotes/domain-core'
import { MetricProps } from './MetricProps'
export class Metric extends ValueObject<MetricProps> {
static readonly NAMES = {
ItemCreated: 'ItemCreated',
ItemUpdated: 'ItemUpdated',
ContentSizeUtilized: 'ContentSizeUtilized',
ItemOperation: 'ItemOperation',
}
static create(props: MetricProps): Result<Metric> {
const isValidName = Object.values(this.NAMES).includes(props.name)
if (!isValidName) {
return Result.fail<Metric>(`Invalid metric name: ${props.name}`)
} else {
return Result.ok<Metric>(new Metric(props))
}
}
}

View File

@@ -0,0 +1,4 @@
export interface MetricProps {
name: string
timestamp: number
}

View File

@@ -0,0 +1,17 @@
import { Uuid } from '@standardnotes/domain-core'
import { Metric } from './Metric'
import { MetricsSummary } from './MetricsSummary'
export interface MetricsStoreInterface {
storeMetric(metric: Metric): Promise<void>
storeUserBasedMetric(metric: Metric, value: number, userUuid: Uuid): Promise<void>
getUserBasedMetricsSummaryWithinTimeRange(dto: {
metricName: string
userUuid: Uuid
from: Date
to: Date
}): Promise<MetricsSummary>
getUserBasedMetricsSummary(name: string, timestamp: number): Promise<MetricsSummary>
getMetricsSummary(name: string, from: number, to: number): Promise<MetricsSummary>
}

View File

@@ -0,0 +1,6 @@
export interface MetricsSummary {
sum: number
max: number
min: number
sampleCount: number
}

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