mirror of
https://github.com/standardnotes/server
synced 2026-01-26 14:01:08 -05:00
Compare commits
84 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
162a63ae2b | ||
|
|
0d82819cba | ||
|
|
578ce0e74e | ||
|
|
532be7c358 | ||
|
|
d406272f07 | ||
|
|
9de3352885 | ||
|
|
8575d20f7b | ||
|
|
102d4b1e8a | ||
|
|
1a57c247b2 | ||
|
|
dbb0e4a974 | ||
|
|
5c02435ee4 | ||
|
|
0a1e555b13 | ||
|
|
be668d7d7a | ||
|
|
87e50ec941 | ||
|
|
6d7ca1b926 | ||
|
|
00bfaaa53d | ||
|
|
f939caf2d9 | ||
|
|
0f3615ee65 | ||
|
|
567bcf26b5 | ||
|
|
9d49764b84 | ||
|
|
5c9f493b67 | ||
|
|
4fe8e9a79f | ||
|
|
f975dd9697 | ||
|
|
10832f7001 | ||
|
|
86b050865f | ||
|
|
6f07aaf87a | ||
|
|
634e8bd2d0 | ||
|
|
6853dfbf66 | ||
|
|
136cf252a1 | ||
|
|
cad28ebba5 | ||
|
|
460fdf9eaf | ||
|
|
bec1b502ad | ||
|
|
70bbf11db5 | ||
|
|
c00c7becae | ||
|
|
89dc6c19bf | ||
|
|
972a91d59f | ||
|
|
045358ddbf | ||
|
|
c7217a92ba | ||
|
|
3da7a21cde | ||
|
|
351e18f638 | ||
|
|
4f2129c4e0 | ||
|
|
d7a1c667dd | ||
|
|
4de0bfa36d | ||
|
|
0443de88ce | ||
|
|
f830bac873 | ||
|
|
517ae5ded9 | ||
|
|
6062f85000 | ||
|
|
e2205c3849 | ||
|
|
0b46eff16e | ||
|
|
df67982bca | ||
|
|
d44866b3c0 | ||
|
|
6ee18bffe6 | ||
|
|
a881dd2d79 | ||
|
|
b767e1f072 | ||
|
|
e3cb1faba4 | ||
|
|
5c5f988055 | ||
|
|
c7d2adf091 | ||
|
|
a4ad37f309 | ||
|
|
73c2cc1222 | ||
|
|
9380900aaf | ||
|
|
02f4d5c717 | ||
|
|
1f4b26d269 | ||
|
|
e253825da6 | ||
|
|
2bddd947ba | ||
|
|
b7173346d2 | ||
|
|
01641975c0 | ||
|
|
7abd80cdba | ||
|
|
aeb5ea1874 | ||
|
|
d2a371b92c | ||
|
|
3ea3b24bb6 | ||
|
|
0c3bc0cae6 | ||
|
|
d56410984a | ||
|
|
4dd2eb9349 | ||
|
|
709aec5eeb | ||
|
|
f1aa431c22 | ||
|
|
86d0e565ed | ||
|
|
92bb447cac | ||
|
|
08966e7af7 | ||
|
|
2c732ff713 | ||
|
|
1493b7c478 | ||
|
|
efd816a627 | ||
|
|
f24c4c27fc | ||
|
|
88a5de766c | ||
|
|
a0e523e64c |
7
.github/ci.env
vendored
7
.github/ci.env
vendored
@@ -17,6 +17,9 @@ SYNCING_SERVER_LOG_LEVEL=debug
|
||||
FILES_SERVER_LOG_LEVEL=debug
|
||||
REVISIONS_SERVER_LOG_LEVEL=debug
|
||||
API_GATEWAY_LOG_LEVEL=debug
|
||||
COOKIE_DOMAIN=localhost
|
||||
COOKIE_SECURE=false
|
||||
COOKIE_PARTITIONED=false
|
||||
|
||||
MYSQL_DATABASE=standard_notes_db
|
||||
MYSQL_USER=std_notes_user
|
||||
@@ -27,4 +30,6 @@ 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
|
||||
|
||||
HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES=1
|
||||
|
||||
36
.github/workflows/common-server-application.yml
vendored
36
.github/workflows/common-server-application.yml
vendored
@@ -42,26 +42,26 @@ jobs:
|
||||
workspace_name: ${{ inputs.workspace_name }}
|
||||
secrets: inherit
|
||||
|
||||
deploy-web:
|
||||
if: ${{ inputs.deploy_web }}
|
||||
# deploy-web:
|
||||
# if: ${{ inputs.deploy_web }}
|
||||
|
||||
needs: publish
|
||||
# needs: publish
|
||||
|
||||
name: Deploy Web
|
||||
uses: standardnotes/server/.github/workflows/common-deploy.yml@main
|
||||
with:
|
||||
service_name: ${{ inputs.service_name }}
|
||||
docker_image: ${{ inputs.service_name }}:${{ github.sha }}
|
||||
secrets: inherit
|
||||
# name: Deploy Web
|
||||
# uses: standardnotes/server/.github/workflows/common-deploy.yml@main
|
||||
# with:
|
||||
# service_name: ${{ inputs.service_name }}
|
||||
# docker_image: ${{ inputs.service_name }}:${{ github.sha }}
|
||||
# secrets: inherit
|
||||
|
||||
deploy-worker:
|
||||
if: ${{ inputs.deploy_worker }}
|
||||
# deploy-worker:
|
||||
# if: ${{ inputs.deploy_worker }}
|
||||
|
||||
needs: publish
|
||||
# needs: publish
|
||||
|
||||
name: Deploy Worker
|
||||
uses: standardnotes/server/.github/workflows/common-deploy.yml@main
|
||||
with:
|
||||
service_name: ${{ inputs.service_name }}-worker
|
||||
docker_image: ${{ inputs.service_name }}:${{ github.sha }}
|
||||
secrets: inherit
|
||||
# name: Deploy Worker
|
||||
# uses: standardnotes/server/.github/workflows/common-deploy.yml@main
|
||||
# with:
|
||||
# service_name: ${{ inputs.service_name }}-worker
|
||||
# docker_image: ${{ inputs.service_name }}:${{ github.sha }}
|
||||
# secrets: inherit
|
||||
|
||||
9
.github/workflows/e2e-home-server.yml
vendored
9
.github/workflows/e2e-home-server.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -70,7 +70,8 @@ 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 "HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES=1" >> 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
|
||||
@@ -93,11 +94,11 @@ jobs:
|
||||
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 3600000 -f http://localhost:9001/mocha/test.html?suite=${{ inputs.suite }}
|
||||
run: yarn dlx mocha-headless-chrome --timeout 3600000 -a no-sandbox -a disable-setuid-sandbox -f http://localhost:9001/mocha/test.html?suite=${{ inputs.suite }}
|
||||
|
||||
- name: Archive failed run logs
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: home-server-failure-logs-${{ inputs.suite }}-${{ matrix.db_type }}-${{ matrix.cache_type }}
|
||||
retention-days: 5
|
||||
|
||||
6
.github/workflows/e2e-self-hosted.yml
vendored
6
.github/workflows/e2e-self-hosted.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -57,11 +57,11 @@ jobs:
|
||||
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 3600000 -f http://localhost:9001/mocha/test.html?suite=${{ inputs.suite }}
|
||||
run: yarn dlx mocha-headless-chrome --timeout 3600000 -a no-sandbox -a disable-setuid-sandbox -f http://localhost:9001/mocha/test.html?suite=${{ inputs.suite }}
|
||||
|
||||
- name: Archive failed run logs
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: self-hosted-failure-logs-${{ inputs.suite }}
|
||||
retention-days: 5
|
||||
|
||||
12
.github/workflows/pr.yml
vendored
12
.github/workflows/pr.yml
vendored
@@ -13,14 +13,14 @@ jobs:
|
||||
|
||||
- name: Cache build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/**/dist
|
||||
key: ${{ runner.os }}-build-${{ github.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
- name: Cache build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/**/dist
|
||||
key: ${{ runner.os }}-build-${{ github.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -73,14 +73,14 @@ jobs:
|
||||
|
||||
- name: Cache build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/**/dist
|
||||
key: ${{ runner.os }}-build-${{ github.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
10
.github/workflows/publish.yml
vendored
10
.github/workflows/publish.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
- name: Cache build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/**/dist
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Cache build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/**/dist
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Cache build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/**/dist
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
- name: Cache build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/**/dist
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
BIN
.yarn/cache/@aws-sdk-client-cloudwatch-npm-3.485.0-afe4ac001f-0e02739ef1.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-client-cloudwatch-npm-3.485.0-afe4ac001f-0e02739ef1.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-client-sns-npm-3.490.0-2cd839225d-4d8875521c.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-client-sns-npm-3.490.0-2cd839225d-4d8875521c.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-client-sso-npm-3.485.0-5f6733bd23-635de0e310.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-client-sso-npm-3.485.0-5f6733bd23-635de0e310.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-client-sso-npm-3.490.0-26ec4a8978-785147e3c2.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-client-sso-npm-3.490.0-26ec4a8978-785147e3c2.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-client-sts-npm-3.485.0-cc69ab3505-98c7f4d722.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-client-sts-npm-3.485.0-cc69ab3505-98c7f4d722.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-client-sts-npm-3.490.0-f3cd7f7c70-19d1b98694.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-client-sts-npm-3.490.0-f3cd7f7c70-19d1b98694.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-core-npm-3.485.0-77ed30ee18-b84dafb213.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-core-npm-3.485.0-77ed30ee18-b84dafb213.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-core-npm-3.490.0-3725a806be-3e81f37825.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-core-npm-3.490.0-3725a806be-3e81f37825.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-env-npm-3.485.0-0fda7f74e0-b8346ea6f5.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-env-npm-3.485.0-0fda7f74e0-b8346ea6f5.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-env-npm-3.489.0-e86b20f5e4-95ab96ee49.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-env-npm-3.489.0-e86b20f5e4-95ab96ee49.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.485.0-bec3aaa989-3176b03ee1.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.485.0-bec3aaa989-3176b03ee1.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.490.0-51f9d0faff-4e4cd2633a.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.490.0-51f9d0faff-4e4cd2633a.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-node-npm-3.485.0-9f40e4a3cf-d31e5a95ea.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-node-npm-3.485.0-9f40e4a3cf-d31e5a95ea.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-node-npm-3.490.0-3cbe0ec5e6-2f8141c3e1.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-node-npm-3.490.0-3cbe0ec5e6-2f8141c3e1.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-process-npm-3.485.0-62d3460338-e740fb949e.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-process-npm-3.485.0-62d3460338-e740fb949e.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-process-npm-3.489.0-9370bfd061-42f4f5f21d.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-process-npm-3.489.0-9370bfd061-42f4f5f21d.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.485.0-42db25db09-7269315797.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.485.0-42db25db09-7269315797.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.490.0-9acb6828c0-ef2eff8fbc.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.490.0-9acb6828c0-ef2eff8fbc.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.485.0-420b04bcce-33125ce0b7.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.485.0-420b04bcce-33125ce0b7.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.489.0-002a8c8ade-911bc3fd28.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.489.0-002a8c8ade-911bc3fd28.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-host-header-npm-3.485.0-2e625f9614-9ca3da2a26.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-host-header-npm-3.485.0-2e625f9614-9ca3da2a26.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-host-header-npm-3.489.0-10c65ea2e3-3f80f71691.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-host-header-npm-3.489.0-10c65ea2e3-3f80f71691.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-logger-npm-3.485.0-3ff7eeabbb-2fcb731794.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-logger-npm-3.485.0-3ff7eeabbb-2fcb731794.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-logger-npm-3.489.0-ba04fd0161-0bbf9d08c7.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-logger-npm-3.489.0-ba04fd0161-0bbf9d08c7.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-recursion-detection-npm-3.485.0-af05ed4810-afdea18930.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-recursion-detection-npm-3.485.0-af05ed4810-afdea18930.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-recursion-detection-npm-3.489.0-2cefe5fc6b-91eb0b3b46.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-recursion-detection-npm-3.489.0-2cefe5fc6b-91eb0b3b46.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-signing-npm-3.485.0-3117db6053-f9dbb39d8d.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-signing-npm-3.485.0-3117db6053-f9dbb39d8d.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-signing-npm-3.489.0-722d97a2fd-6fedba4569.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-signing-npm-3.489.0-722d97a2fd-6fedba4569.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-user-agent-npm-3.485.0-983204fccf-a8fc812aff.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-user-agent-npm-3.485.0-983204fccf-a8fc812aff.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-middleware-user-agent-npm-3.489.0-4a9e57c5ff-51fc7a8a03.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-middleware-user-agent-npm-3.489.0-4a9e57c5ff-51fc7a8a03.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-region-config-resolver-npm-3.485.0-1a69e46754-55bc5128b8.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-region-config-resolver-npm-3.485.0-1a69e46754-55bc5128b8.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-region-config-resolver-npm-3.489.0-0721047a4a-045a630c94.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-region-config-resolver-npm-3.489.0-0721047a4a-045a630c94.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-token-providers-npm-3.485.0-1fb5ab9dfb-aed270b625.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-token-providers-npm-3.485.0-1fb5ab9dfb-aed270b625.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-token-providers-npm-3.489.0-05002406d6-ee7a20eff7.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-token-providers-npm-3.489.0-05002406d6-ee7a20eff7.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-types-npm-3.485.0-6aa8cab069-588aae4b49.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-types-npm-3.485.0-6aa8cab069-588aae4b49.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-types-npm-3.489.0-f0f748fbaa-48778dad14.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-types-npm-3.489.0-f0f748fbaa-48778dad14.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-util-endpoints-npm-3.485.0-5e0fad395e-c1844fed8b.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-util-endpoints-npm-3.485.0-5e0fad395e-c1844fed8b.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-util-endpoints-npm-3.489.0-1f2dd7e944-68f921982f.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-util-endpoints-npm-3.489.0-1f2dd7e944-68f921982f.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-util-user-agent-browser-npm-3.485.0-23925a5581-d1e4d4c635.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-util-user-agent-browser-npm-3.485.0-23925a5581-d1e4d4c635.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-util-user-agent-browser-npm-3.489.0-ffb94f7b1d-2bb414b8d8.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-util-user-agent-browser-npm-3.489.0-ffb94f7b1d-2bb414b8d8.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-util-user-agent-node-npm-3.485.0-7991a74cb3-e2805ef37b.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-util-user-agent-node-npm-3.485.0-7991a74cb3-e2805ef37b.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@aws-sdk-util-user-agent-node-npm-3.489.0-082349e8a9-95dc1e07b6.zip
vendored
Normal file
BIN
.yarn/cache/@aws-sdk-util-user-agent-node-npm-3.489.0-082349e8a9-95dc1e07b6.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@types-cookie-parser-npm-1.4.6-27287e1e43-b1bbb17bc4.zip
vendored
Normal file
BIN
.yarn/cache/@types-cookie-parser-npm-1.4.6-27287e1e43-b1bbb17bc4.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/axios-npm-1.6.7-d7b9974d1b-a1932b089e.zip
vendored
Normal file
BIN
.yarn/cache/axios-npm-1.6.7-d7b9974d1b-a1932b089e.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/cookie-npm-0.4.1-cc5e2ebb42-0f2defd60a.zip
vendored
Normal file
BIN
.yarn/cache/cookie-npm-0.4.1-cc5e2ebb42-0f2defd60a.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/cookie-parser-npm-1.4.6-a68f84d02a-1e5a63aa82.zip
vendored
Normal file
BIN
.yarn/cache/cookie-parser-npm-1.4.6-a68f84d02a-1e5a63aa82.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/follow-redirects-npm-1.15.5-9d14db76ca-d467f13c1c.zip
vendored
Normal file
BIN
.yarn/cache/follow-redirects-npm-1.15.5-9d14db76ca-d467f13c1c.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/fsevents-patch-19706e7e35-10.zip
vendored
BIN
.yarn/cache/fsevents-patch-19706e7e35-10.zip
vendored
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/mysql2-npm-3.9.7-8fe89e50ac-7f43b17cc0.zip
vendored
Normal file
BIN
.yarn/cache/mysql2-npm-3.9.7-8fe89e50ac-7f43b17cc0.zip
vendored
Normal file
Binary file not shown.
@@ -54,7 +54,6 @@ services:
|
||||
ports:
|
||||
- 3306
|
||||
restart: unless-stopped
|
||||
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
|
||||
volumes:
|
||||
- ./data/mysql:/var/lib/mysql
|
||||
- ./data/import:/docker-entrypoint-initdb.d
|
||||
|
||||
@@ -39,7 +39,6 @@ services:
|
||||
expose:
|
||||
- 3306
|
||||
restart: unless-stopped
|
||||
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
|
||||
volumes:
|
||||
- ./data/mysql:/var/lib/mysql
|
||||
- ./data/import:/docker-entrypoint-initdb.d
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
|
||||
"postversion": "./scripts/push-tags-one-by-one.sh",
|
||||
"e2e": "yarn build && PORT=3123 yarn workspace @standardnotes/home-server start",
|
||||
"start": "yarn workspace @standardnotes/home-server run build && yarn workspace @standardnotes/home-server start"
|
||||
"start": "yarn build && yarn workspace @standardnotes/home-server start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.2",
|
||||
@@ -39,7 +39,7 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"packageManager": "yarn@4.0.2",
|
||||
"packageManager": "yarn@4.1.0",
|
||||
"dependenciesMeta": {
|
||||
"grpc-tools@1.12.4": {
|
||||
"unplugged": true
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.34.18](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.17...@standardnotes/analytics@2.34.18) (2025-04-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.34.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.16...@standardnotes/analytics@2.34.17) (2024-06-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.34.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.15...@standardnotes/analytics@2.34.16) (2024-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.34.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.14...@standardnotes/analytics@2.34.15) (2024-01-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [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
|
||||
|
||||
## [2.34.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.11...@standardnotes/analytics@2.34.12) (2024-01-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -10,6 +10,12 @@ RUN corepack enable
|
||||
|
||||
COPY ./ /workspace
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN yarn install --immutable
|
||||
|
||||
RUN yarn build
|
||||
|
||||
WORKDIR /workspace/packages/analytics
|
||||
|
||||
ENTRYPOINT [ "/workspace/packages/analytics/docker/entrypoint.sh" ]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.34.12",
|
||||
"version": "2.34.18",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -24,7 +24,7 @@
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=2",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"report": "yarn node dist/bin/report.js",
|
||||
"setup:env": "cp .env.sample .env",
|
||||
@@ -57,7 +57,7 @@
|
||||
"inversify": "^6.0.1",
|
||||
"ioredis": "^5.2.4",
|
||||
"mixpanel": "^0.17.0",
|
||||
"mysql2": "^3.0.1",
|
||||
"mysql2": "^3.9.7",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"typeorm": "^0.3.17",
|
||||
"winston": "^3.8.1"
|
||||
|
||||
@@ -5,6 +5,8 @@ import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
import { safeHtml } from '@standardnotes/common'
|
||||
|
||||
const countActiveUsers = (measureName: string, data: any): { yesterday: number; last30Days: number } => {
|
||||
const totalActiveUsersLast30DaysIncludingToday = data.statisticsOverTime.find(
|
||||
(a: { name: string; period: number }) => a.name === measureName && a.period === 27,
|
||||
@@ -567,7 +569,7 @@ export const html = (data: any, timer: TimerInterface) => {
|
||||
const totalActivePlusUsers = countActiveUsers(StatisticMeasureName.NAMES.ActivePlusUsers, data)
|
||||
const totalActiveProUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveProUsers, data)
|
||||
|
||||
return ` <div>
|
||||
return safeHtml` <div>
|
||||
<p>Hello,</p>
|
||||
<p>
|
||||
<strong>Here are some statistics from yesterday:</strong>
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.92.2](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.92.1...@standardnotes/api-gateway@1.92.2) (2025-04-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.92.1](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.91.0...@standardnotes/api-gateway@1.92.1) (2024-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** bump version ([102d4b1](https://github.com/standardnotes/server/commit/102d4b1e8ab000fc97d01c621654b6fc65e37d32))
|
||||
|
||||
## [1.90.1](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.90.0...@standardnotes/api-gateway@1.90.1) (2024-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.90.0](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.20...@standardnotes/api-gateway@1.90.0) (2024-01-18)
|
||||
|
||||
### Features
|
||||
|
||||
* add content sizes fixing upon grpc resource exhausted error ([#1029](https://github.com/standardnotes/server/issues/1029)) ([634e8bd](https://github.com/standardnotes/server/commit/634e8bd2d0f055abbda1150587ab9a444281e600))
|
||||
|
||||
## [1.89.20](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.19...@standardnotes/api-gateway@1.89.20) (2024-01-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** add codetag metadata to error logs ([136cf25](https://github.com/standardnotes/server/commit/136cf252a134efe7d99f79d8622c43dbebbb5ac8))
|
||||
|
||||
## [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
|
||||
|
||||
## [1.89.13](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.12...@standardnotes/api-gateway@1.89.13) (2024-01-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -10,6 +10,12 @@ RUN corepack enable
|
||||
|
||||
COPY ./ /workspace
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN yarn install --immutable
|
||||
|
||||
RUN yarn build
|
||||
|
||||
WORKDIR /workspace/packages/api-gateway
|
||||
|
||||
ENTRYPOINT [ "/workspace/packages/api-gateway/docker/entrypoint.sh" ]
|
||||
|
||||
@@ -27,6 +27,7 @@ import '../src/Controller/v2/RevisionsControllerV2'
|
||||
|
||||
import helmet from 'helmet'
|
||||
import * as cors from 'cors'
|
||||
import * as cookieParser from 'cookie-parser'
|
||||
import { text, json, Request, Response, NextFunction } from 'express'
|
||||
import * as winston from 'winston'
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@@ -36,15 +37,35 @@ 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 logger: winston.Logger = container.get(TYPES.ApiGateway_Logger)
|
||||
|
||||
const server = new InversifyExpressServer(container)
|
||||
|
||||
server.setConfig((app) => {
|
||||
app.use((request: Request, _response: Response, next: NextFunction) => {
|
||||
if (request.hostname.includes('standardnotes.org')) {
|
||||
logger.warn('Request is using deprecated domain', {
|
||||
origin: request.headers.origin,
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
snjs: request.headers['x-snjs-version'],
|
||||
application: request.headers['x-application-version'],
|
||||
})
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
app.use((_request: Request, response: Response, next: NextFunction) => {
|
||||
response.setHeader('X-API-Gateway-Version', container.get(TYPES.ApiGateway_VERSION))
|
||||
next()
|
||||
@@ -72,13 +93,57 @@ void container.load().then((container) => {
|
||||
}),
|
||||
)
|
||||
|
||||
app.use(json({ limit: '50mb' }))
|
||||
app.use(cookieParser())
|
||||
|
||||
app.use(json({ limit: requestPayloadLimit }))
|
||||
app.use(
|
||||
text({
|
||||
type: ['text/plain', 'application/x-www-form-urlencoded', 'application/x-www-form-urlencoded; charset=utf-8'],
|
||||
}),
|
||||
)
|
||||
app.use(cors())
|
||||
const corsAllowedOrigins = container.get<string[]>(TYPES.ApiGateway_CORS_ALLOWED_ORIGINS)
|
||||
app.use(
|
||||
cors({
|
||||
credentials: true,
|
||||
exposedHeaders: ['x-captcha-required'],
|
||||
origin: (requestOrigin: string | undefined, callback: (err: Error | null, origin?: string[]) => void) => {
|
||||
const originStrictModeEnabled = env.get('CORS_ORIGIN_STRICT_MODE_ENABLED', true)
|
||||
? env.get('CORS_ORIGIN_STRICT_MODE_ENABLED', true) === 'true'
|
||||
: false
|
||||
|
||||
if (!originStrictModeEnabled) {
|
||||
callback(null, [requestOrigin as string])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const requstOriginIsNotFilled = !requestOrigin || requestOrigin === 'null'
|
||||
const requestOriginatesFromTheDesktopApp = requestOrigin?.startsWith('file://')
|
||||
const requestOriginatesFromClipperForFirefox = requestOrigin?.startsWith('moz-extension://')
|
||||
const requestOriginatesFromSelfHostedAppOnHttpPort = requestOrigin === 'http://localhost'
|
||||
const requestOriginatesFromSelfHostedAppOnCustomPort = requestOrigin?.match(/http:\/\/localhost:\d+/) !== null
|
||||
const requestOriginatesFromSelfHostedApp =
|
||||
requestOriginatesFromSelfHostedAppOnHttpPort || requestOriginatesFromSelfHostedAppOnCustomPort
|
||||
|
||||
const requestIsWhitelisted =
|
||||
corsAllowedOrigins.length === 0 ||
|
||||
requstOriginIsNotFilled ||
|
||||
requestOriginatesFromTheDesktopApp ||
|
||||
requestOriginatesFromClipperForFirefox ||
|
||||
requestOriginatesFromSelfHostedApp
|
||||
|
||||
if (requestIsWhitelisted) {
|
||||
callback(null, [requestOrigin as string])
|
||||
} else {
|
||||
if (corsAllowedOrigins.includes(requestOrigin)) {
|
||||
callback(null, [requestOrigin])
|
||||
} else {
|
||||
callback(new Error('Not allowed by CORS', { cause: 'origin not allowed' }))
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
app.use(
|
||||
robots({
|
||||
UserAgent: '*',
|
||||
@@ -87,16 +152,18 @@ void container.load().then((container) => {
|
||||
)
|
||||
})
|
||||
|
||||
const logger: winston.Logger = container.get(TYPES.ApiGateway_Logger)
|
||||
|
||||
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}`, {
|
||||
origin: request.headers.origin,
|
||||
codeTag: 'server.ts',
|
||||
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 +171,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:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.89.13",
|
||||
"version": "1.92.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -31,6 +31,7 @@
|
||||
"start": "yarn node dist/bin/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sns": "^3.490.0",
|
||||
"@grpc/grpc-js": "^1.9.13",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
@@ -40,6 +41,7 @@
|
||||
"@standardnotes/time": "workspace:*",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"axios": "^1.6.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.2",
|
||||
@@ -54,6 +56,7 @@
|
||||
"winston": "^3.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1",
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/ioredis": "^5.0.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as winston from 'winston'
|
||||
import * as AgentKeepAlive from 'agentkeepalive'
|
||||
import * as grpc from '@grpc/grpc-js'
|
||||
import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import Redis from 'ioredis'
|
||||
import { Container } from 'inversify'
|
||||
@@ -29,6 +30,10 @@ import { SyncResponseHttpRepresentation } from '../Mapping/Sync/Http/SyncRespons
|
||||
import { SyncRequestGRPCMapper } from '../Mapping/Sync/GRPC/SyncRequestGRPCMapper'
|
||||
import { SyncResponseGRPCMapper } from '../Mapping/Sync/GRPC/SyncResponseGRPCMapper'
|
||||
import { GRPCWebSocketAuthMiddleware } from '../Controller/GRPCWebSocketAuthMiddleware'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { SNSDomainEventPublisher } from '@standardnotes/domain-events-infra'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { DomainEventFactory } from '../Event/DomainEventFactory'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: {
|
||||
@@ -51,6 +56,34 @@ export class ContainerConfigLoader {
|
||||
.bind<boolean>(TYPES.ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING)
|
||||
.toConstantValue(isConfiguredForHomeServerOrSelfHosting)
|
||||
|
||||
if (!isConfiguredForHomeServerOrSelfHosting) {
|
||||
const snsConfig: SNSClientConfig = {
|
||||
region: env.get('SNS_AWS_REGION', true),
|
||||
}
|
||||
if (env.get('SNS_ENDPOINT', true)) {
|
||||
snsConfig.endpoint = env.get('SNS_ENDPOINT', true)
|
||||
}
|
||||
if (env.get('SNS_ACCESS_KEY_ID', true) && env.get('SNS_SECRET_ACCESS_KEY', true)) {
|
||||
snsConfig.credentials = {
|
||||
accessKeyId: env.get('SNS_ACCESS_KEY_ID', true),
|
||||
secretAccessKey: env.get('SNS_SECRET_ACCESS_KEY', true),
|
||||
}
|
||||
}
|
||||
const snsClient = new SNSClient(snsConfig)
|
||||
container.bind<SNSClient>(TYPES.ApiGateway_SNS).toConstantValue(snsClient)
|
||||
|
||||
container.bind(TYPES.ApiGateway_SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
|
||||
|
||||
container
|
||||
.bind<DomainEventPublisherInterface>(TYPES.ApiGateway_DomainEventPublisher)
|
||||
.toConstantValue(
|
||||
new SNSDomainEventPublisher(
|
||||
container.get(TYPES.ApiGateway_SNS),
|
||||
container.get(TYPES.ApiGateway_SNS_TOPIC_ARN),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const winstonFormatters = [winston.format.splat(), winston.format.json()]
|
||||
|
||||
let logger: winston.Logger
|
||||
@@ -109,6 +142,10 @@ export class ContainerConfigLoader {
|
||||
.bind(TYPES.ApiGateway_CROSS_SERVICE_TOKEN_CACHE_TTL)
|
||||
.toConstantValue(+env.get('CROSS_SERVICE_TOKEN_CACHE_TTL', true))
|
||||
container.bind(TYPES.ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER).toConstantValue(isConfiguredForHomeServer)
|
||||
container
|
||||
.bind<string[]>(TYPES.ApiGateway_CORS_ALLOWED_ORIGINS)
|
||||
.toConstantValue(env.get('CORS_ALLOWED_ORIGINS', true) ? env.get('CORS_ALLOWED_ORIGINS', true).split(',') : [])
|
||||
container.bind<string>(TYPES.ApiGateway_CAPTCHA_UI_URL).toConstantValue(env.get('CAPTCHA_UI_URL', true))
|
||||
|
||||
// Middleware
|
||||
container
|
||||
@@ -124,14 +161,14 @@ export class ContainerConfigLoader {
|
||||
// Services
|
||||
container.bind<TimerInterface>(TYPES.ApiGateway_Timer).toConstantValue(new Timer())
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
.bind<CrossServiceTokenCacheInterface>(TYPES.ApiGateway_CrossServiceTokenCache)
|
||||
.toConstantValue(new InMemoryCrossServiceTokenCache(container.get(TYPES.ApiGateway_Timer)))
|
||||
} else {
|
||||
container
|
||||
.bind<CrossServiceTokenCacheInterface>(TYPES.ApiGateway_CrossServiceTokenCache)
|
||||
.to(RedisCrossServiceTokenCache)
|
||||
.toConstantValue(new RedisCrossServiceTokenCache(container.get(TYPES.ApiGateway_Redis)))
|
||||
}
|
||||
container
|
||||
.bind<EndpointResolverInterface>(TYPES.ApiGateway_EndpointResolver)
|
||||
@@ -192,6 +229,10 @@ export class ContainerConfigLoader {
|
||||
.bind<MapperInterface<SyncResponse, SyncResponseHttpRepresentation>>(TYPES.Mapper_SyncResponseGRPCMapper)
|
||||
.toConstantValue(new SyncResponseGRPCMapper())
|
||||
|
||||
container
|
||||
.bind<DomainEventFactoryInterface>(TYPES.ApiGateway_DomainEventFactory)
|
||||
.toConstantValue(new DomainEventFactory(container.get<TimerInterface>(TYPES.ApiGateway_Timer)))
|
||||
|
||||
container
|
||||
.bind<GRPCSyncingServerServiceProxy>(TYPES.ApiGateway_GRPCSyncingServerServiceProxy)
|
||||
.toConstantValue(
|
||||
@@ -202,6 +243,10 @@ export class ContainerConfigLoader {
|
||||
TYPES.Mapper_SyncResponseGRPCMapper,
|
||||
),
|
||||
container.get<winston.Logger>(TYPES.ApiGateway_Logger),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.ApiGateway_DomainEventFactory),
|
||||
isConfiguredForHomeServerOrSelfHosting
|
||||
? undefined
|
||||
: container.get<DomainEventPublisherInterface>(TYPES.ApiGateway_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
container
|
||||
|
||||
@@ -2,7 +2,12 @@ export const TYPES = {
|
||||
ApiGateway_Logger: Symbol.for('ApiGateway_Logger'),
|
||||
ApiGateway_Redis: Symbol.for('ApiGateway_Redis'),
|
||||
ApiGateway_HTTPClient: Symbol.for('ApiGateway_HTTPClient'),
|
||||
ApiGateway_SNS: Symbol.for('ApiGateway_SNS'),
|
||||
ApiGateway_DomainEventPublisher: Symbol.for('ApiGateway_DomainEventPublisher'),
|
||||
// env vars
|
||||
ApiGateway_CORS_ALLOWED_ORIGINS: Symbol.for('ApiGateway_CORS_ALLOWED_ORIGINS'),
|
||||
ApiGateway_SNS_TOPIC_ARN: Symbol.for('ApiGateway_SNS_TOPIC_ARN'),
|
||||
ApiGateway_SNS_AWS_REGION: Symbol.for('ApiGateway_SNS_AWS_REGION'),
|
||||
ApiGateway_SYNCING_SERVER_JS_URL: Symbol.for('ApiGateway_SYNCING_SERVER_JS_URL'),
|
||||
ApiGateway_AUTH_SERVER_URL: Symbol.for('ApiGateway_AUTH_SERVER_URL'),
|
||||
ApiGateway_AUTH_SERVER_GRPC_URL: Symbol.for('ApiGateway_AUTH_SERVER_GRPC_URL'),
|
||||
@@ -20,6 +25,7 @@ export const TYPES = {
|
||||
ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING: Symbol.for(
|
||||
'ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING',
|
||||
),
|
||||
ApiGateway_CAPTCHA_UI_URL: Symbol.for('ApiGateway_CAPTCHA_UI_URL'),
|
||||
// Middleware
|
||||
ApiGateway_RequiredCrossServiceTokenMiddleware: Symbol.for('ApiGateway_RequiredCrossServiceTokenMiddleware'),
|
||||
ApiGateway_OptionalCrossServiceTokenMiddleware: Symbol.for('ApiGateway_OptionalCrossServiceTokenMiddleware'),
|
||||
@@ -29,6 +35,7 @@ export const TYPES = {
|
||||
Mapper_SyncRequestGRPCMapper: Symbol.for('Mapper_SyncRequestGRPCMapper'),
|
||||
Mapper_SyncResponseGRPCMapper: Symbol.for('Mapper_SyncResponseGRPCMapper'),
|
||||
// Services
|
||||
ApiGateway_DomainEventFactory: Symbol.for('ApiGateway_DomainEventFactory'),
|
||||
ApiGateway_GRPCSyncingServerServiceProxy: Symbol.for('ApiGateway_GRPCSyncingServerServiceProxy'),
|
||||
ApiGateway_ServiceProxy: Symbol.for('ApiGateway_ServiceProxy'),
|
||||
ApiGateway_CrossServiceTokenCache: Symbol.for('ApiGateway_CrossServiceTokenCache'),
|
||||
|
||||
@@ -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(
|
||||
@@ -40,9 +42,33 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
}
|
||||
|
||||
if (crossServiceToken === null) {
|
||||
const cookiesFromHeaders = new Map<string, string[]>()
|
||||
request.headers.cookie?.split(';').forEach((cookie) => {
|
||||
const parts = cookie.split('=')
|
||||
if (parts.length === 2) {
|
||||
const existingCookies = cookiesFromHeaders.get(parts[0].trim())
|
||||
if (existingCookies) {
|
||||
existingCookies.push(parts[1].trim())
|
||||
cookiesFromHeaders.set(parts[0].trim(), existingCookies)
|
||||
} else {
|
||||
cookiesFromHeaders.set(parts[0].trim(), [parts[1].trim()])
|
||||
}
|
||||
}
|
||||
})
|
||||
const authResponse = await this.serviceProxy.validateSession({
|
||||
authorization: authHeaderValue,
|
||||
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
|
||||
headers: {
|
||||
authorization: authHeaderValue.replace('Bearer ', ''),
|
||||
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
|
||||
},
|
||||
requestMetadata: {
|
||||
snjs: request.headers['x-snjs-version'] as string,
|
||||
application: request.headers['x-application-version'] as string,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
userAgent: request.headers['user-agent'],
|
||||
secChUa: request.headers['sec-ch-ua'] as string,
|
||||
},
|
||||
cookies: cookiesFromHeaders,
|
||||
})
|
||||
|
||||
if (!this.handleSessionValidationResponse(authResponse, response, next)) {
|
||||
@@ -55,33 +81,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) {
|
||||
|
||||
@@ -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,17 @@ 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,
|
||||
hasContentLimit: decodedToken.hasContentLimit,
|
||||
} as ResponseLocals)
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Could not pass the request to websocket connection validation on underlying service: ${
|
||||
|
||||
@@ -20,8 +20,6 @@ export class LegacyController extends BaseHttpController {
|
||||
['DELETE:/session', 'DELETE:session'],
|
||||
['DELETE:/session/all', 'DELETE:session/all'],
|
||||
['POST:/session/refresh', 'POST:session/refresh'],
|
||||
['POST:/auth/sign_in', 'POST:auth/sign_in'],
|
||||
['GET:/auth/params', 'GET:auth/params'],
|
||||
])
|
||||
|
||||
this.PARAMETRIZED_AUTH_ROUTES = new Map([
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface OfflineResponseLocals {
|
||||
offlineAuthToken: string
|
||||
userEmail: string
|
||||
featuresToken: string
|
||||
}
|
||||
30
packages/api-gateway/src/Controller/ResponseLocals.ts
Normal file
30
packages/api-gateway/src/Controller/ResponseLocals.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
}
|
||||
hasContentLimit: boolean
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
|
||||
|
||||
export interface SubscriptionResponseLocals {
|
||||
tokenAuthenticationMethod: TokenAuthenticationMethod
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,12 +4,14 @@ import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-exp
|
||||
import { TYPES } from '../../Bootstrap/Types'
|
||||
import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
|
||||
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
|
||||
import { JsonResult } from 'inversify-express-utils/lib/results'
|
||||
|
||||
@controller('/v1')
|
||||
export class ActionsController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.ApiGateway_ServiceProxy) private serviceProxy: ServiceProxyInterface,
|
||||
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
|
||||
@inject(TYPES.ApiGateway_CAPTCHA_UI_URL) private captchaUIUrl: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -19,7 +21,7 @@ export class ActionsController extends BaseHttpController {
|
||||
await this.serviceProxy.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'auth/sign_in'),
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'auth/pkce_sign_in'),
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
@@ -29,7 +31,7 @@ export class ActionsController extends BaseHttpController {
|
||||
await this.serviceProxy.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'auth/params'),
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'auth/pkce_params'),
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
@@ -83,4 +85,11 @@ export class ActionsController extends BaseHttpController {
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/meta')
|
||||
async serverMetadata(): Promise<JsonResult> {
|
||||
return this.json({
|
||||
captchaUIUrl: this.captchaUIUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
controller,
|
||||
httpDelete,
|
||||
httpGet,
|
||||
httpPatch,
|
||||
httpPost,
|
||||
httpPut,
|
||||
results,
|
||||
@@ -16,6 +15,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 {
|
||||
@@ -37,16 +38,6 @@ export class UsersController extends BaseHttpController {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/send-activation-code', request.body)
|
||||
}
|
||||
|
||||
@httpPatch('/:userId', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
async updateUser(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('PATCH', 'users/:userId', request.params.userId),
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPut('/:userUuid/password', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
async changePassword(request: Request, response: Response): Promise<void> {
|
||||
this.logger.debug(
|
||||
@@ -84,7 +75,7 @@ export class UsersController extends BaseHttpController {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'auth/params'),
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'auth/pkce_params'),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,6 +131,20 @@ export class UsersController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
@httpPut('/:userUuid/subscription-settings', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
async putSubscriptionSetting(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier(
|
||||
'PUT',
|
||||
'users/:userUuid/subscription-settings',
|
||||
request.params.userUuid,
|
||||
),
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/:userUuid/settings/:settingName', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
async getSetting(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
@@ -214,7 +219,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 +234,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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
23
packages/api-gateway/src/Event/DomainEventFactory.ts
Normal file
23
packages/api-gateway/src/Event/DomainEventFactory.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
import { ContentSizesFixRequestedEvent, DomainEventService } from '@standardnotes/domain-events'
|
||||
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
createContentSizesFixRequestedEvent(dto: { userUuid: string }): ContentSizesFixRequestedEvent {
|
||||
return {
|
||||
type: 'CONTENT_SIZES_FIX_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ContentSizesFixRequestedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createContentSizesFixRequestedEvent(dto: { userUuid: string }): ContentSizesFixRequestedEvent
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import * as IORedis from 'ioredis'
|
||||
import { TYPES } from '../../Bootstrap/Types'
|
||||
|
||||
import { CrossServiceTokenCacheInterface } from '../../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
|
||||
@injectable()
|
||||
export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterface {
|
||||
private readonly PREFIX = 'cst'
|
||||
private readonly USER_CST_PREFIX = 'user-cst'
|
||||
|
||||
constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
|
||||
constructor(private redisClient: IORedis.Redis) {}
|
||||
|
||||
async set(dto: {
|
||||
key: string
|
||||
|
||||
@@ -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(
|
||||
@@ -9,23 +10,44 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
private filesServerUrl: string,
|
||||
) {}
|
||||
|
||||
async validateSession(
|
||||
async validateSession(dto: {
|
||||
headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
_retryAttempt?: number,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
}
|
||||
cookies?: Map<string, string[]>
|
||||
snjs?: string
|
||||
application?: string
|
||||
retryAttempt?: number
|
||||
}): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
contentType: string
|
||||
}
|
||||
}> {
|
||||
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
|
||||
if (!authService) {
|
||||
throw new Error('Auth service not found')
|
||||
}
|
||||
|
||||
let stringOfCookies = ''
|
||||
for (const cookieName of dto.cookies?.keys() ?? []) {
|
||||
for (const cookieValue of dto.cookies?.get(cookieName) as string[]) {
|
||||
stringOfCookies += `${cookieName}=${cookieValue}; `
|
||||
}
|
||||
}
|
||||
|
||||
const serviceResponse = (await authService.handleRequest(
|
||||
{
|
||||
body: {
|
||||
authTokenFromHeaders: dto.headers.authorization,
|
||||
sharedVaultOwnerContext: dto.headers.sharedVaultOwnerContext,
|
||||
},
|
||||
headers: {
|
||||
authorization: headers.authorization,
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
'x-snjs-version': dto.snjs,
|
||||
'x-application-version': dto.application,
|
||||
cookie: stringOfCookies.trim(),
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
@@ -134,11 +156,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,
|
||||
|
||||
@@ -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 {
|
||||
@@ -26,20 +28,51 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
@inject(TYPES.ApiGateway_Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async validateSession(
|
||||
async validateSession(dto: {
|
||||
headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
retryAttempt?: number,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
}
|
||||
requestMetadata: {
|
||||
url: string
|
||||
method: string
|
||||
snjs?: string
|
||||
application?: string
|
||||
userAgent?: string
|
||||
secChUa?: string
|
||||
}
|
||||
cookies?: Map<string, string[]>
|
||||
retryAttempt?: number
|
||||
}): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
contentType: string
|
||||
}
|
||||
}> {
|
||||
try {
|
||||
let stringOfCookies = ''
|
||||
for (const cookieName of dto.cookies?.keys() ?? []) {
|
||||
for (const cookieValue of dto.cookies?.get(cookieName) as string[]) {
|
||||
stringOfCookies += `${cookieName}=${cookieValue}; `
|
||||
}
|
||||
}
|
||||
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: headers.authorization,
|
||||
Accept: 'application/json',
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
Cookie: stringOfCookies.trim(),
|
||||
'x-snjs-version': dto.requestMetadata.snjs,
|
||||
'x-application-version': dto.requestMetadata.application,
|
||||
'x-origin-user-agent': dto.requestMetadata.userAgent,
|
||||
'x-origin-sec-ch-ua': dto.requestMetadata.secChUa,
|
||||
'x-origin-url': dto.requestMetadata.url,
|
||||
'x-origin-method': dto.requestMetadata.method,
|
||||
},
|
||||
data: {
|
||||
authTokenFromHeaders: dto.headers.authorization,
|
||||
sharedVaultOwnerContext: dto.headers.sharedVaultOwnerContext,
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
@@ -56,13 +89,18 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
}
|
||||
} catch (error) {
|
||||
const requestDidNotMakeIt = this.requestTimedOutOrDidNotReachDestination(error as Record<string, unknown>)
|
||||
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
|
||||
const tooManyRetryAttempts = dto.retryAttempt && dto.retryAttempt > 2
|
||||
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
|
||||
await this.timer.sleep(50)
|
||||
|
||||
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
|
||||
const nextRetryAttempt = dto.retryAttempt ? dto.retryAttempt + 1 : 1
|
||||
|
||||
return this.validateSession(headers, nextRetryAttempt)
|
||||
return this.validateSession({
|
||||
headers: dto.headers,
|
||||
cookies: dto.cookies,
|
||||
requestMetadata: dto.requestMetadata,
|
||||
retryAttempt: nextRetryAttempt,
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
@@ -175,23 +213,33 @@ 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)) {
|
||||
headers[headerName] = request.headers[headerName] as string
|
||||
}
|
||||
|
||||
headers['x-origin-url'] = request.url
|
||||
headers['x-origin-method'] = request.method
|
||||
headers['x-snjs-version'] = request.headers['x-snjs-version'] as string
|
||||
headers['x-application-version'] = request.headers['x-application-version'] as string
|
||||
headers['x-origin-user-agent'] = request.headers['user-agent'] as string
|
||||
headers['x-origin-sec-ch-ua'] = request.headers['sec-ch-ua'] as string
|
||||
|
||||
delete headers.host
|
||||
delete headers['content-length']
|
||||
|
||||
if (response.locals.authToken) {
|
||||
headers['X-Auth-Token'] = response.locals.authToken
|
||||
headers.cookie = request.headers.cookie as string
|
||||
|
||||
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 +261,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 +306,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 +324,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,
|
||||
@@ -354,13 +385,11 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
|
||||
private applyResponseHeaders(serviceResponse: AxiosResponse, response: Response): void {
|
||||
const returnedHeadersFromUnderlyingService = [
|
||||
'access-control-allow-methods',
|
||||
'access-control-allow-origin',
|
||||
'access-control-expose-headers',
|
||||
'authorization',
|
||||
'content-type',
|
||||
'x-ssjs-version',
|
||||
'x-auth-version',
|
||||
'authorization',
|
||||
'set-cookie',
|
||||
'access-control-expose-headers',
|
||||
'x-captcha-required',
|
||||
]
|
||||
|
||||
returnedHeadersFromUnderlyingService.map((headerName) => {
|
||||
|
||||
@@ -49,13 +49,22 @@ export interface ServiceProxyInterface {
|
||||
endpointOrMethodIdentifier: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
validateSession(
|
||||
validateSession(dto: {
|
||||
headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
retryAttempt?: number,
|
||||
): Promise<{
|
||||
}
|
||||
requestMetadata: {
|
||||
url: string
|
||||
method: string
|
||||
snjs?: string
|
||||
application?: string
|
||||
userAgent?: string
|
||||
secChUa?: string
|
||||
}
|
||||
cookies?: Map<string, string[]>
|
||||
retryAttempt?: number
|
||||
}): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
|
||||
@@ -7,8 +7,6 @@ export class EndpointResolver implements EndpointResolverInterface {
|
||||
// Auth Middleware
|
||||
['[POST]:sessions/validate', 'auth.sessions.validate'],
|
||||
// Actions Controller
|
||||
['[POST]:auth/sign_in', 'auth.signIn'],
|
||||
['[GET]:auth/params', 'auth.params'],
|
||||
['[POST]:auth/sign_out', 'auth.signOut'],
|
||||
['[POST]:auth/recovery/codes', 'auth.generateRecoveryCodes'],
|
||||
['[POST]:auth/recovery/login', 'auth.signInWithRecoveryCodes'],
|
||||
@@ -40,7 +38,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'],
|
||||
@@ -49,6 +46,7 @@ export class EndpointResolver implements EndpointResolverInterface {
|
||||
['[PUT]:users/:userUuid/settings', 'auth.users.updateSetting'],
|
||||
['[GET]:users/:userUuid/settings/:settingName', 'auth.users.getSetting'],
|
||||
['[DELETE]:users/:userUuid/settings/:settingName', 'auth.users.deleteSetting'],
|
||||
['[PUT]:users/:userUuid/subscription-settings', 'auth.users.updateSubscriptionSetting'],
|
||||
['[GET]:users/:userUuid/subscription-settings/:subscriptionSettingName', 'auth.users.getSubscriptionSetting'],
|
||||
['[GET]:users/:userUuid/features', 'auth.users.getFeatures'],
|
||||
['[GET]:users/:userUuid/subscription', 'auth.users.getSubscription'],
|
||||
|
||||
@@ -2,13 +2,15 @@ import { AxiosInstance, AxiosError, AxiosResponse, Method } from 'axios'
|
||||
import { Request, Response } from 'express'
|
||||
import { Logger } from 'winston'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { IAuthClient, AuthorizationHeader, SessionValidationResponse } from '@standardnotes/grpc'
|
||||
import { Cookie, IAuthClient, RequestValidationOptions, SessionValidationResponse } from '@standardnotes/grpc'
|
||||
import * as grpc from '@grpc/grpc-js'
|
||||
|
||||
import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
|
||||
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(
|
||||
@@ -28,23 +30,56 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
private gRPCSyncingServerServiceProxy: GRPCSyncingServerServiceProxy,
|
||||
) {}
|
||||
|
||||
async validateSession(
|
||||
async validateSession(dto: {
|
||||
headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
retryAttempt?: number,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
}
|
||||
requestMetadata: {
|
||||
url: string
|
||||
method: string
|
||||
snjs?: string
|
||||
application?: string
|
||||
userAgent?: string
|
||||
secChUa?: string
|
||||
}
|
||||
cookies?: Map<string, string[]>
|
||||
retryAttempt?: number
|
||||
}): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
contentType: string
|
||||
}
|
||||
}> {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
try {
|
||||
const request = new AuthorizationHeader()
|
||||
request.setBearerToken(headers.authorization)
|
||||
const request = new RequestValidationOptions()
|
||||
request.setBearerToken(dto.headers.authorization)
|
||||
|
||||
const metadata = new grpc.Metadata()
|
||||
metadata.set('x-shared-vault-owner-context', headers.sharedVaultOwnerContext ?? '')
|
||||
for (const cookieName of dto.cookies?.keys() ?? []) {
|
||||
for (const cookieValue of dto.cookies?.get(cookieName) as string[]) {
|
||||
const cookie = new Cookie()
|
||||
cookie.setName(cookieName)
|
||||
cookie.setValue(cookieValue)
|
||||
|
||||
request.addCookie(cookie)
|
||||
}
|
||||
}
|
||||
if (dto.headers.sharedVaultOwnerContext) {
|
||||
request.setSharedVaultOwnerContext(dto.headers.sharedVaultOwnerContext)
|
||||
}
|
||||
|
||||
this.logger.debug('[GRPCServiceProxy] Validating session via gRPC')
|
||||
|
||||
const metadata = new grpc.Metadata()
|
||||
metadata.set('x-snjs-version', dto.requestMetadata.snjs as string)
|
||||
metadata.set('x-application-version', dto.requestMetadata.application as string)
|
||||
metadata.set('x-origin-user-agent', dto.requestMetadata.userAgent as string)
|
||||
metadata.set('x-origin-sec-ch-ua', dto.requestMetadata.secChUa as string)
|
||||
metadata.set('x-origin-url', dto.requestMetadata.url)
|
||||
metadata.set('x-origin-method', dto.requestMetadata.method)
|
||||
|
||||
this.authClient.validate(
|
||||
request,
|
||||
metadata,
|
||||
@@ -88,8 +123,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
try {
|
||||
const result = await promise
|
||||
|
||||
if (retryAttempt) {
|
||||
this.logger.debug(`Request to Auth Server succeeded after ${retryAttempt} retries`)
|
||||
if (dto.retryAttempt) {
|
||||
this.logger.info(`Request to Auth Server succeeded after ${dto.retryAttempt} retries`)
|
||||
}
|
||||
|
||||
return result as { status: number; data: unknown; headers: { contentType: string } }
|
||||
@@ -97,15 +132,20 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
const requestDidNotMakeIt =
|
||||
'code' in (error as Record<string, unknown>) && (error as Record<string, unknown>).code === Status.UNAVAILABLE
|
||||
|
||||
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
|
||||
const tooManyRetryAttempts = dto.retryAttempt && dto.retryAttempt > 2
|
||||
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
|
||||
await this.timer.sleep(50)
|
||||
|
||||
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
|
||||
const nextRetryAttempt = dto.retryAttempt ? dto.retryAttempt + 1 : 1
|
||||
|
||||
this.logger.debug(`Retrying request to Auth Server for the ${nextRetryAttempt} time`)
|
||||
this.logger.warn(`Retrying request to Auth Server for the ${nextRetryAttempt} time`)
|
||||
|
||||
return this.validateSession(headers, nextRetryAttempt)
|
||||
return this.validateSession({
|
||||
headers: dto.headers,
|
||||
cookies: dto.cookies,
|
||||
requestMetadata: dto.requestMetadata,
|
||||
retryAttempt: nextRetryAttempt,
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
@@ -134,48 +174,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 +292,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 +303,14 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
delete headers.host
|
||||
delete headers['content-length']
|
||||
|
||||
if (response.locals.authToken) {
|
||||
headers['X-Auth-Token'] = response.locals.authToken
|
||||
headers.cookie = request.headers.cookie as string
|
||||
|
||||
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 +360,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 +395,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 +414,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,
|
||||
@@ -454,13 +475,11 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
|
||||
private applyResponseHeaders(serviceResponse: AxiosResponse, response: Response): void {
|
||||
const returnedHeadersFromUnderlyingService = [
|
||||
'access-control-allow-methods',
|
||||
'access-control-allow-origin',
|
||||
'access-control-expose-headers',
|
||||
'authorization',
|
||||
'content-type',
|
||||
'x-ssjs-version',
|
||||
'x-auth-version',
|
||||
'authorization',
|
||||
'set-cookie',
|
||||
'access-control-expose-headers',
|
||||
'x-captcha-required',
|
||||
]
|
||||
|
||||
returnedHeadersFromUnderlyingService.map((headerName) => {
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { ISyncingClient, SyncRequest, SyncResponse } from '@standardnotes/grpc'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
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 { SyncResponseHttpRepresentation } from '../../Mapping/Sync/Http/SyncResponseHttpRepresentation'
|
||||
import { ResponseLocals } from '../../Controller/ResponseLocals'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
|
||||
export class GRPCSyncingServerServiceProxy {
|
||||
constructor(
|
||||
private syncingClient: ISyncingClient,
|
||||
private syncRequestGRPCMapper: MapperInterface<Record<string, unknown>, SyncRequest>,
|
||||
private syncResponseGRPCMapper: MapperInterface<SyncResponse, SyncResponseHttpRepresentation>,
|
||||
private logger: Logger,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher?: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async sync(
|
||||
@@ -20,24 +25,27 @@ 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')
|
||||
metadata.set('x-has-content-limit', locals.hasContentLimit ? 'true' : 'false')
|
||||
|
||||
this.syncingClient.syncItems(syncRequest, metadata, (error, syncResponse) => {
|
||||
if (error) {
|
||||
@@ -52,10 +60,16 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
if (error.code === Status.RESOURCE_EXHAUSTED && this.domainEventPublisher !== undefined) {
|
||||
void this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createContentSizesFixRequestedEvent({ userUuid: locals.user.uuid }),
|
||||
)
|
||||
}
|
||||
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
@@ -68,7 +82,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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,11 @@ CACHE_TYPE=redis
|
||||
|
||||
DISABLE_USER_REGISTRATION=false
|
||||
|
||||
COOKIE_DOMAIN=
|
||||
COOKIE_SAME_SITE=
|
||||
COOKIE_SECURE=
|
||||
COOKIE_PARTITIONED=
|
||||
|
||||
ACCESS_TOKEN_AGE=5184000
|
||||
REFRESH_TOKEN_AGE=31556926
|
||||
|
||||
@@ -49,6 +54,10 @@ VALET_TOKEN_TTL=
|
||||
|
||||
WEB_SOCKET_CONNECTION_TOKEN_SECRET=
|
||||
|
||||
# Human verfication
|
||||
CAPTCHA_SERVER_URL=
|
||||
CAPTCHA_UI_URL=
|
||||
|
||||
# (Optional) U2F Setup
|
||||
U2F_RELYING_PARTY_ID=
|
||||
U2F_RELYING_PARTY_NAME=
|
||||
|
||||
@@ -3,6 +3,69 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.178.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.178.5...@standardnotes/auth-server@1.178.6) (2025-04-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.178.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.178.3...@standardnotes/auth-server@1.178.5) (2024-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump versions on packages ([8575d20](https://github.com/standardnotes/server/commit/8575d20f7b79f5220da7cced0041ae12b72e1e49))
|
||||
|
||||
# [1.178.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.20...@standardnotes/auth-server@1.178.0) (2024-01-19)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add script for fixing subscriptions with missing id state ([#1030](https://github.com/standardnotes/server/issues/1030)) ([86b0508](https://github.com/standardnotes/server/commit/86b050865f8090ed33d5ce05528ff0e1e23657ef))
|
||||
|
||||
## [1.177.20](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.19...@standardnotes/auth-server@1.177.20) (2024-01-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.177.19](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.18...@standardnotes/auth-server@1.177.19) (2024-01-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add server daily email backup permission for all versions of core user role ([#1028](https://github.com/standardnotes/server/issues/1028)) ([460fdf9](https://github.com/standardnotes/server/commit/460fdf9eafe2db629637ba481f2b135ed21560b9))
|
||||
|
||||
## [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
|
||||
|
||||
## [1.177.11](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.10...@standardnotes/auth-server@1.177.11) (2024-01-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -10,6 +10,12 @@ RUN corepack enable
|
||||
|
||||
COPY ./ /workspace
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN yarn install --immutable
|
||||
|
||||
RUN yarn build
|
||||
|
||||
WORKDIR /workspace/packages/auth
|
||||
|
||||
ENTRYPOINT [ "/workspace/packages/auth/docker/entrypoint.sh" ]
|
||||
|
||||
74
packages/auth/bin/fix_subscriptions.ts
Normal file
74
packages/auth/bin/fix_subscriptions.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import { UserSubscriptionRepositoryInterface } from '../src/Domain/Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
const fixSubscriptions = async (
|
||||
userRepository: UserRepositoryInterface,
|
||||
userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
domainEventFactory: DomainEventFactoryInterface,
|
||||
domainEventPublisher: DomainEventPublisherInterface,
|
||||
): Promise<void> => {
|
||||
const subscriptions = await userSubscriptionRepository.findBySubscriptionId(0)
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
const userUuidOrError = Uuid.create(subscription.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
continue
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await userRepository.findOneByUuid(userUuid)
|
||||
if (!user) {
|
||||
continue
|
||||
}
|
||||
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createSubscriptionStateRequestedEvent({
|
||||
userEmail: user.email,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const container = new ContainerConfigLoader('worker')
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Auth_Logger)
|
||||
|
||||
logger.info('Starting to fix subscriptions with missing subscriptionId ...')
|
||||
|
||||
const userRepository = container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository)
|
||||
const userSubscriptionRepository = container.get<UserSubscriptionRepositoryInterface>(
|
||||
TYPES.Auth_UserSubscriptionRepository,
|
||||
)
|
||||
const domainEventFactory = container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory)
|
||||
const domainEventPublisher = container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher)
|
||||
|
||||
Promise.resolve(
|
||||
fixSubscriptions(userRepository, userSubscriptionRepository, domainEventFactory, domainEventPublisher),
|
||||
)
|
||||
.then(() => {
|
||||
logger.info('Finished fixing subscriptions with missing subscriptionId.')
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Failed to fix subscriptions with missing subscriptionId.', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
})
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
@@ -20,6 +20,7 @@ import '../src/Infra/InversifyExpressUtils/AnnotatedHealthCheckController'
|
||||
import '../src/Infra/InversifyExpressUtils/AnnotatedFeaturesController'
|
||||
|
||||
import * as cors from 'cors'
|
||||
import * as cookieParser from 'cookie-parser'
|
||||
import * as grpc from '@grpc/grpc-js'
|
||||
import { urlencoded, json, Request, Response, NextFunction } from 'express'
|
||||
import * as winston from 'winston'
|
||||
@@ -35,6 +36,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) => {
|
||||
@@ -52,6 +54,7 @@ void container.load().then((container) => {
|
||||
})
|
||||
app.use(json())
|
||||
app.use(urlencoded({ extended: true }))
|
||||
app.use(cookieParser())
|
||||
app.use(cors())
|
||||
})
|
||||
|
||||
@@ -59,12 +62,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({
|
||||
|
||||
@@ -9,28 +9,23 @@ import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
|
||||
import { MuteFailedBackupsEmailsOption } from '@standardnotes/settings'
|
||||
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
|
||||
import { GetUserKeyParams } from '../src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
|
||||
import { Email, SettingName } from '@standardnotes/domain-core'
|
||||
import { Email } from '@standardnotes/domain-core'
|
||||
|
||||
const inputArgs = process.argv.slice(2)
|
||||
const backupEmail = inputArgs[0]
|
||||
|
||||
const requestBackups = async (
|
||||
userRepository: UserRepositoryInterface,
|
||||
settingRepository: SettingRepositoryInterface,
|
||||
roleService: RoleServiceInterface,
|
||||
domainEventFactory: DomainEventFactoryInterface,
|
||||
domainEventPublisher: DomainEventPublisherInterface,
|
||||
getUserKeyParamsUseCase: GetUserKeyParams,
|
||||
): Promise<void> => {
|
||||
const permissionName = PermissionName.DailyEmailBackup
|
||||
const muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
|
||||
const muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted
|
||||
|
||||
const emailOrError = Email.create(backupEmail)
|
||||
if (emailOrError.isFailed()) {
|
||||
@@ -48,24 +43,13 @@ const requestBackups = async (
|
||||
throw new Error(`User ${backupEmail} is not permitted for email backups`)
|
||||
}
|
||||
|
||||
let userHasEmailsMuted = false
|
||||
const emailsMutedSetting = await settingRepository.findOneByNameAndUserUuid(muteEmailsSettingName, user.uuid)
|
||||
if (emailsMutedSetting !== null && emailsMutedSetting.props.value !== null) {
|
||||
userHasEmailsMuted = emailsMutedSetting.props.value === muteEmailsSettingValue
|
||||
}
|
||||
|
||||
const keyParamsResponse = await getUserKeyParamsUseCase.execute({
|
||||
userUuid: user.uuid,
|
||||
authenticated: false,
|
||||
})
|
||||
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createEmailBackupRequestedEvent(
|
||||
user.uuid,
|
||||
emailsMutedSetting?.id.toString() as string,
|
||||
userHasEmailsMuted,
|
||||
keyParamsResponse.keyParams,
|
||||
),
|
||||
domainEventFactory.createEmailBackupRequestedEvent(user.uuid, keyParamsResponse.keyParams),
|
||||
)
|
||||
|
||||
return
|
||||
@@ -82,7 +66,6 @@ void container.load().then((container) => {
|
||||
|
||||
logger.info(`Starting email backup requesting for ${backupEmail} ...`)
|
||||
|
||||
const settingRepository: SettingRepositoryInterface = container.get(TYPES.Auth_SettingRepository)
|
||||
const userRepository: UserRepositoryInterface = container.get(TYPES.Auth_UserRepository)
|
||||
const roleService: RoleServiceInterface = container.get(TYPES.Auth_RoleService)
|
||||
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.Auth_DomainEventFactory)
|
||||
@@ -90,14 +73,7 @@ void container.load().then((container) => {
|
||||
const getUserKeyParamsUseCase: GetUserKeyParams = container.get(TYPES.Auth_GetUserKeyParams)
|
||||
|
||||
Promise.resolve(
|
||||
requestBackups(
|
||||
userRepository,
|
||||
settingRepository,
|
||||
roleService,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
getUserKeyParamsUseCase,
|
||||
),
|
||||
requestBackups(userRepository, roleService, domainEventFactory, domainEventPublisher, getUserKeyParamsUseCase),
|
||||
)
|
||||
.then(() => {
|
||||
logger.info(`Email backup requesting complete for ${backupEmail}`)
|
||||
|
||||
11
packages/auth/docker/entrypoint-fix-subscriptions.js
Normal file
11
packages/auth/docker/entrypoint-fix-subscriptions.js
Normal 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/fix_subscriptions.js')))
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true })
|
||||
|
||||
exports.default = index
|
||||
@@ -42,6 +42,10 @@ case "$COMMAND" in
|
||||
exec node docker/entrypoint-fix-roles.js
|
||||
;;
|
||||
|
||||
'fix-subscriptions' )
|
||||
exec node docker/entrypoint-fix-subscriptions.js
|
||||
;;
|
||||
|
||||
'delete-accounts' )
|
||||
FILE_NAME=$1 && shift 1
|
||||
MODE=$1 && shift 1
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class EnableEmailBackupsForAll1705493201352 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Core User v1 Permissions
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES \
|
||||
("bde42e26-628c-44e6-9d76-21b08954b0bf", "eb0575a2-6e26-49e3-9501-f2e75d7dbda3") \
|
||||
',
|
||||
)
|
||||
// Core User v2 Permissions
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES \
|
||||
("23bf88ca-bee1-4a4c-adf0-b7a48749eea7", "eb0575a2-6e26-49e3-9501-f2e75d7dbda3") \
|
||||
',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class UserRolesContentLimit1707759514236 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `permissions` (uuid, name) VALUES ("f8b4ced2-6a59-49f8-9ade-416a5f5ffc61", "server:content-limit")',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `roles` (uuid, name, version) VALUES ("ab2e15c9-9252-43f3-829c-6f0af3315791", "CORE_USER", 4)',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `role_permissions` (permission_uuid, role_uuid) VALUES \
|
||||
("b04a7670-934e-4ab1-b8a3-0f27ff159511", "ab2e15c9-9252-43f3-829c-6f0af3315791"), \
|
||||
("eb0575a2-6e26-49e3-9501-f2e75d7dbda3", "ab2e15c9-9252-43f3-829c-6f0af3315791"), \
|
||||
("f8b4ced2-6a59-49f8-9ade-416a5f5ffc61", "ab2e15c9-9252-43f3-829c-6f0af3315791") \
|
||||
',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DELETE FROM `role_permissions` WHERE role_uuid="ab2e15c9-9252-43f3-829c-6f0af3315791"')
|
||||
await queryRunner.query('DELETE FROM `permissions` WHERE uuid="f8b4ced2-6a59-49f8-9ade-416a5f5ffc61"')
|
||||
await queryRunner.query('DELETE FROM `roles` WHERE uuid="ab2e15c9-9252-43f3-829c-6f0af3315791"')
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user