mirror of
https://github.com/standardnotes/server
synced 2026-02-01 05:01:11 -05:00
Compare commits
25 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 |
5
.github/ci.env
vendored
5
.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
|
||||
@@ -28,3 +31,5 @@ AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d
|
||||
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
|
||||
|
||||
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
|
||||
|
||||
7
.github/workflows/e2e-home-server.yml
vendored
7
.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'
|
||||
@@ -71,6 +71,7 @@ jobs:
|
||||
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
|
||||
echo "REVISIONS_FREQUENCY=2" >> 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'
|
||||
|
||||
89
.pnp.cjs
generated
89
.pnp.cjs
generated
@@ -6356,7 +6356,7 @@ const RAW_RUNTIME_STATE =
|
||||
["ioredis", "npm:5.3.2"],\
|
||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
|
||||
["mixpanel", "npm:0.17.0"],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["mysql2", "npm:3.9.7"],\
|
||||
["prettier", "npm:3.0.3"],\
|
||||
["reflect-metadata", "npm:0.2.1"],\
|
||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
|
||||
@@ -6396,6 +6396,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/grpc", "workspace:packages/grpc"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
["@types/cookie-parser", "npm:1.4.6"],\
|
||||
["@types/cors", "npm:2.8.13"],\
|
||||
["@types/express", "npm:4.17.17"],\
|
||||
["@types/ioredis", "npm:5.0.0"],\
|
||||
@@ -6407,6 +6408,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["agentkeepalive", "npm:4.5.0"],\
|
||||
["axios", "npm:1.6.1"],\
|
||||
["cookie-parser", "npm:1.4.6"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
["dotenv", "npm:16.1.3"],\
|
||||
["eslint", "npm:8.41.0"],\
|
||||
@@ -6457,6 +6459,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/sncrypto-node", "workspace:packages/sncrypto-node"],\
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
["@types/bcryptjs", "npm:2.4.2"],\
|
||||
["@types/cookie-parser", "npm:1.4.6"],\
|
||||
["@types/cors", "npm:2.8.13"],\
|
||||
["@types/express", "npm:4.17.17"],\
|
||||
["@types/ioredis", "npm:5.0.0"],\
|
||||
@@ -6468,7 +6471,10 @@ const RAW_RUNTIME_STATE =
|
||||
["@types/uuid", "npm:9.0.3"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["agentkeepalive", "npm:4.5.0"],\
|
||||
["axios", "npm:1.6.7"],\
|
||||
["bcryptjs", "npm:2.4.3"],\
|
||||
["cookie-parser", "npm:1.4.6"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
["dayjs", "npm:1.11.7"],\
|
||||
["dotenv", "npm:16.1.3"],\
|
||||
@@ -6479,7 +6485,7 @@ const RAW_RUNTIME_STATE =
|
||||
["inversify-express-utils", "npm:6.4.3"],\
|
||||
["ioredis", "npm:5.3.2"],\
|
||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["mysql2", "npm:3.9.7"],\
|
||||
["otplib", "npm:12.0.1"],\
|
||||
["prettier", "npm:3.0.3"],\
|
||||
["prettyjson", "npm:1.2.5"],\
|
||||
@@ -6689,10 +6695,12 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/files-server", "workspace:packages/files"],\
|
||||
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
|
||||
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
|
||||
["@types/cookie-parser", "npm:1.4.6"],\
|
||||
["@types/cors", "npm:2.8.13"],\
|
||||
["@types/express", "npm:4.17.17"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
|
||||
["cookie-parser", "npm:1.4.6"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
["dotenv", "npm:16.1.3"],\
|
||||
["eslint", "npm:8.41.0"],\
|
||||
@@ -6790,7 +6798,7 @@ const RAW_RUNTIME_STATE =
|
||||
["inversify-express-utils", "npm:6.4.3"],\
|
||||
["ioredis", "npm:5.3.2"],\
|
||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["mysql2", "npm:3.9.7"],\
|
||||
["prettier", "npm:3.0.3"],\
|
||||
["reflect-metadata", "npm:0.2.1"],\
|
||||
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
|
||||
@@ -6809,6 +6817,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
|
||||
["@aws-sdk/client-sns", "npm:3.484.0"],\
|
||||
["@aws-sdk/client-sqs", "npm:3.484.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
@@ -6826,7 +6835,7 @@ const RAW_RUNTIME_STATE =
|
||||
["inversify", "npm:6.0.1"],\
|
||||
["ioredis", "npm:5.3.2"],\
|
||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["mysql2", "npm:3.9.7"],\
|
||||
["prettier", "npm:3.0.3"],\
|
||||
["reflect-metadata", "npm:0.2.1"],\
|
||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
|
||||
@@ -6977,7 +6986,7 @@ const RAW_RUNTIME_STATE =
|
||||
["ioredis", "npm:5.3.2"],\
|
||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
|
||||
["jsonwebtoken", "npm:9.0.0"],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["mysql2", "npm:3.9.7"],\
|
||||
["prettier", "npm:3.0.3"],\
|
||||
["prettyjson", "npm:1.2.5"],\
|
||||
["reflect-metadata", "npm:0.2.1"],\
|
||||
@@ -7057,7 +7066,7 @@ const RAW_RUNTIME_STATE =
|
||||
["inversify-express-utils", "npm:6.4.3"],\
|
||||
["ioredis", "npm:5.3.2"],\
|
||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["mysql2", "npm:3.9.7"],\
|
||||
["prettier", "npm:3.0.3"],\
|
||||
["reflect-metadata", "npm:0.2.1"],\
|
||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
|
||||
@@ -7237,6 +7246,16 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/cookie-parser", [\
|
||||
["npm:1.4.6", {\
|
||||
"packageLocation": "./.yarn/cache/@types-cookie-parser-npm-1.4.6-27287e1e43-b1bbb17bc4.zip/node_modules/@types/cookie-parser/",\
|
||||
"packageDependencies": [\
|
||||
["@types/cookie-parser", "npm:1.4.6"],\
|
||||
["@types/express", "npm:4.17.17"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/cors", [\
|
||||
["npm:2.8.13", {\
|
||||
"packageLocation": "./.yarn/cache/@types-cors-npm-2.8.13-4b8ac1068f-7ef197ea19.zip/node_modules/@types/cors/",\
|
||||
@@ -8497,6 +8516,16 @@ const RAW_RUNTIME_STATE =
|
||||
["proxy-from-env", "npm:1.1.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.6.7", {\
|
||||
"packageLocation": "./.yarn/cache/axios-npm-1.6.7-d7b9974d1b-a1932b089e.zip/node_modules/axios/",\
|
||||
"packageDependencies": [\
|
||||
["axios", "npm:1.6.7"],\
|
||||
["follow-redirects", "virtual:d7b9974d1bba76881cc57a280a16dd4914416a6fc4923c2efbb6328057412974da1e719cef1530b7a62b97d85d828f7e1d49b5f6de3b5b0854d49902ec87827c#npm:1.15.5"],\
|
||||
["form-data", "npm:4.0.0"],\
|
||||
["proxy-from-env", "npm:1.1.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["babel-jest", [\
|
||||
@@ -9608,6 +9637,13 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["cookie", [\
|
||||
["npm:0.4.1", {\
|
||||
"packageLocation": "./.yarn/cache/cookie-npm-0.4.1-cc5e2ebb42-0f2defd60a.zip/node_modules/cookie/",\
|
||||
"packageDependencies": [\
|
||||
["cookie", "npm:0.4.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:0.5.0", {\
|
||||
"packageLocation": "./.yarn/cache/cookie-npm-0.5.0-e2d58a161a-aae7911ddc.zip/node_modules/cookie/",\
|
||||
"packageDependencies": [\
|
||||
@@ -9616,6 +9652,17 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["cookie-parser", [\
|
||||
["npm:1.4.6", {\
|
||||
"packageLocation": "./.yarn/cache/cookie-parser-npm-1.4.6-a68f84d02a-1e5a63aa82.zip/node_modules/cookie-parser/",\
|
||||
"packageDependencies": [\
|
||||
["cookie-parser", "npm:1.4.6"],\
|
||||
["cookie", "npm:0.4.1"],\
|
||||
["cookie-signature", "npm:1.0.6"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["cookie-signature", [\
|
||||
["npm:1.0.6", {\
|
||||
"packageLocation": "./.yarn/cache/cookie-signature-npm-1.0.6-93f325f7f0-f4e1b0a98a.zip/node_modules/cookie-signature/",\
|
||||
@@ -10864,6 +10911,26 @@ const RAW_RUNTIME_STATE =
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["npm:1.15.5", {\
|
||||
"packageLocation": "./.yarn/cache/follow-redirects-npm-1.15.5-9d14db76ca-d467f13c1c.zip/node_modules/follow-redirects/",\
|
||||
"packageDependencies": [\
|
||||
["follow-redirects", "npm:1.15.5"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:d7b9974d1bba76881cc57a280a16dd4914416a6fc4923c2efbb6328057412974da1e719cef1530b7a62b97d85d828f7e1d49b5f6de3b5b0854d49902ec87827c#npm:1.15.5", {\
|
||||
"packageLocation": "./.yarn/__virtual__/follow-redirects-virtual-393395f3f6/0/cache/follow-redirects-npm-1.15.5-9d14db76ca-d467f13c1c.zip/node_modules/follow-redirects/",\
|
||||
"packageDependencies": [\
|
||||
["follow-redirects", "virtual:d7b9974d1bba76881cc57a280a16dd4914416a6fc4923c2efbb6328057412974da1e719cef1530b7a62b97d85d828f7e1d49b5f6de3b5b0854d49902ec87827c#npm:1.15.5"],\
|
||||
["@types/debug", null],\
|
||||
["debug", null]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/debug",\
|
||||
"debug"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:ffaff76449f02e83712a7d24e03c564489516739c78ebeffb0fbcdb3893ad9a0e48504f9acfa70fe6f16debe9c8dabde3679d63bf648278ea98a5ff38cf77a9e#npm:1.15.2", {\
|
||||
"packageLocation": "./.yarn/__virtual__/follow-redirects-virtual-c2d5794c26/0/cache/follow-redirects-npm-1.15.2-1ec1dd82be-8be0d39919.zip/node_modules/follow-redirects/",\
|
||||
"packageDependencies": [\
|
||||
@@ -13783,10 +13850,10 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["mysql2", [\
|
||||
["npm:3.3.3", {\
|
||||
"packageLocation": "./.yarn/cache/mysql2-npm-3.3.3-d2fe8cf512-4bf7ace8f1.zip/node_modules/mysql2/",\
|
||||
["npm:3.9.7", {\
|
||||
"packageLocation": "./.yarn/cache/mysql2-npm-3.9.7-8fe89e50ac-7f43b17cc0.zip/node_modules/mysql2/",\
|
||||
"packageDependencies": [\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["mysql2", "npm:3.9.7"],\
|
||||
["denque", "npm:2.1.0"],\
|
||||
["generate-function", "npm:2.3.1"],\
|
||||
["iconv-lite", "npm:0.6.3"],\
|
||||
@@ -16836,7 +16903,7 @@ const RAW_RUNTIME_STATE =
|
||||
["mkdirp", "npm:2.1.6"],\
|
||||
["mongodb", null],\
|
||||
["mssql", null],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["mysql2", "npm:3.9.7"],\
|
||||
["oracledb", null],\
|
||||
["pg", null],\
|
||||
["pg-native", null],\
|
||||
@@ -16928,7 +16995,7 @@ const RAW_RUNTIME_STATE =
|
||||
["mkdirp", "npm:2.1.6"],\
|
||||
["mongodb", null],\
|
||||
["mssql", null],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["mysql2", "npm:3.9.7"],\
|
||||
["oracledb", null],\
|
||||
["pg", null],\
|
||||
["pg-native", null],\
|
||||
|
||||
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,18 @@
|
||||
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
|
||||
|
||||
@@ -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.15",
|
||||
"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,20 @@
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -47,9 +48,24 @@ void container.load().then((container) => {
|
||||
? `${+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()
|
||||
@@ -77,13 +93,57 @@ void container.load().then((container) => {
|
||||
}),
|
||||
)
|
||||
|
||||
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: '*',
|
||||
@@ -92,13 +152,12 @@ 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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.90.0",
|
||||
"version": "1.92.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -41,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",
|
||||
@@ -55,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",
|
||||
|
||||
@@ -142,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
|
||||
@@ -157,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)
|
||||
|
||||
@@ -5,6 +5,7 @@ export const TYPES = {
|
||||
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'),
|
||||
@@ -24,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'),
|
||||
|
||||
@@ -42,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)) {
|
||||
|
||||
@@ -100,6 +100,7 @@ export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
|
||||
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(
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -26,4 +26,5 @@ export interface ResponseLocals {
|
||||
sharedVaultOwnerContext?: {
|
||||
upload_bytes_limit: number
|
||||
}
|
||||
hasContentLimit: boolean
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -39,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(
|
||||
@@ -86,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'),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -142,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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,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,
|
||||
|
||||
@@ -28,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
|
||||
@@ -58,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
|
||||
@@ -186,9 +222,18 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
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']
|
||||
|
||||
headers.cookie = request.headers.cookie as string
|
||||
|
||||
if ('authToken' in locals && locals.authToken) {
|
||||
headers['X-Auth-Token'] = locals.authToken
|
||||
}
|
||||
@@ -340,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'],
|
||||
@@ -48,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,7 +2,7 @@ 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'
|
||||
@@ -30,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,
|
||||
@@ -90,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 } }
|
||||
@@ -99,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
|
||||
@@ -265,6 +303,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
delete headers.host
|
||||
delete headers['content-length']
|
||||
|
||||
headers.cookie = request.headers.cookie as string
|
||||
|
||||
if ('authToken' in locals && locals.authToken) {
|
||||
headers['X-Auth-Token'] = locals.authToken
|
||||
}
|
||||
@@ -435,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) => {
|
||||
|
||||
@@ -45,6 +45,7 @@ export class GRPCSyncingServerServiceProxy {
|
||||
metadata.set('x-session-uuid', locals.session.uuid)
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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,22 @@
|
||||
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
|
||||
|
||||
@@ -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'
|
||||
@@ -53,6 +54,7 @@ void container.load().then((container) => {
|
||||
})
|
||||
app.use(json())
|
||||
app.use(urlencoded({ extended: true }))
|
||||
app.use(cookieParser())
|
||||
app.use(cors())
|
||||
})
|
||||
|
||||
|
||||
@@ -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,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"')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddSessionVersion1707813542369 implements MigrationInterface {
|
||||
name = 'AddSessionVersion1707813542369'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `sessions` ADD `version` smallint NULL DEFAULT 1')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `sessions` DROP COLUMN `version`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddSessionPrivateIdentifier1709133001993 implements MigrationInterface {
|
||||
name = 'AddSessionPrivateIdentifier1709133001993'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
"ALTER TABLE `sessions` ADD `private_identifier` varchar(36) NULL COMMENT 'Used to identify a session without exposing the UUID in client-side cookies.'",
|
||||
)
|
||||
await queryRunner.query('CREATE INDEX `index_sessions_on_private_identifier` ON `sessions` (`private_identifier`)')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `index_sessions_on_private_identifier` ON `sessions`')
|
||||
await queryRunner.query('ALTER TABLE `sessions` DROP COLUMN `private_identifier`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddRevokedSessionPrivateIdentifier1709206805226 implements MigrationInterface {
|
||||
name = 'AddRevokedSessionPrivateIdentifier1709206805226'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
"ALTER TABLE `revoked_sessions` ADD `private_identifier` varchar(36) NULL COMMENT 'Used to identify a session without exposing the UUID in client-side cookies.'",
|
||||
)
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX `index_revoked_sessions_on_private_identifier` ON `revoked_sessions` (`private_identifier`)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `index_revoked_sessions_on_private_identifier` ON `revoked_sessions`')
|
||||
await queryRunner.query('ALTER TABLE `revoked_sessions` DROP COLUMN `private_identifier`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddApplicationAndSnjsToSessions1710236132439 implements MigrationInterface {
|
||||
name = 'AddApplicationAndSnjsToSessions1710236132439'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `sessions` ADD `application` varchar(255) NULL')
|
||||
await queryRunner.query('ALTER TABLE `sessions` ADD `snjs` varchar(255) NULL')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `sessions` DROP COLUMN `snjs`')
|
||||
await queryRunner.query('ALTER TABLE `sessions` DROP COLUMN `application`')
|
||||
}
|
||||
}
|
||||
@@ -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"')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddSessionVersion1707813542369 implements MigrationInterface {
|
||||
name = 'AddSessionVersion1707813542369'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `sessions` ADD `version` smallint NULL DEFAULT 1')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `sessions` DROP COLUMN `version`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddSessionPrivateIdentifier1709133169237 implements MigrationInterface {
|
||||
name = 'AddSessionPrivateIdentifier1709133169237'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "index_sessions_on_updated_at"')
|
||||
await queryRunner.query('DROP INDEX "index_sessions_on_user_uuid"')
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "temporary_sessions" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(255), "hashed_access_token" varchar(255) NOT NULL, "hashed_refresh_token" varchar(255) NOT NULL, "access_expiration" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "refresh_expiration" datetime NOT NULL, "api_version" varchar(255), "user_agent" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL, "readonly_access" tinyint NOT NULL DEFAULT (0), "version" smallint DEFAULT (1), "private_identifier" varchar(36))',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO "temporary_sessions"("uuid", "user_uuid", "hashed_access_token", "hashed_refresh_token", "access_expiration", "refresh_expiration", "api_version", "user_agent", "created_at", "updated_at", "readonly_access", "version") SELECT "uuid", "user_uuid", "hashed_access_token", "hashed_refresh_token", "access_expiration", "refresh_expiration", "api_version", "user_agent", "created_at", "updated_at", "readonly_access", "version" FROM "sessions"',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE "sessions"')
|
||||
await queryRunner.query('ALTER TABLE "temporary_sessions" RENAME TO "sessions"')
|
||||
await queryRunner.query('CREATE INDEX "index_sessions_on_updated_at" ON "sessions" ("updated_at") ')
|
||||
await queryRunner.query('CREATE INDEX "index_sessions_on_user_uuid" ON "sessions" ("user_uuid") ')
|
||||
await queryRunner.query('CREATE INDEX "index_sessions_on_private_identifier" ON "sessions" ("private_identifier") ')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "index_sessions_on_private_identifier"')
|
||||
await queryRunner.query('DROP INDEX "index_sessions_on_user_uuid"')
|
||||
await queryRunner.query('DROP INDEX "index_sessions_on_updated_at"')
|
||||
await queryRunner.query('ALTER TABLE "sessions" RENAME TO "temporary_sessions"')
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "sessions" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(255), "hashed_access_token" varchar(255) NOT NULL, "hashed_refresh_token" varchar(255) NOT NULL, "access_expiration" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "refresh_expiration" datetime NOT NULL, "api_version" varchar(255), "user_agent" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL, "readonly_access" tinyint NOT NULL DEFAULT (0), "version" smallint DEFAULT (1))',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO "sessions"("uuid", "user_uuid", "hashed_access_token", "hashed_refresh_token", "access_expiration", "refresh_expiration", "api_version", "user_agent", "created_at", "updated_at", "readonly_access", "version") SELECT "uuid", "user_uuid", "hashed_access_token", "hashed_refresh_token", "access_expiration", "refresh_expiration", "api_version", "user_agent", "created_at", "updated_at", "readonly_access", "version" FROM "temporary_sessions"',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE "temporary_sessions"')
|
||||
await queryRunner.query('CREATE INDEX "index_sessions_on_user_uuid" ON "sessions" ("user_uuid") ')
|
||||
await queryRunner.query('CREATE INDEX "index_sessions_on_updated_at" ON "sessions" ("updated_at") ')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddRevokedSessionPrivateIdentifier1709208455658 implements MigrationInterface {
|
||||
name = 'AddRevokedSessionPrivateIdentifier1709208455658'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "index_revoked_sessions_on_user_uuid"')
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "temporary_revoked_sessions" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "received" tinyint NOT NULL DEFAULT (0), "created_at" datetime NOT NULL, "received_at" datetime, "user_agent" text, "api_version" varchar(255), "private_identifier" varchar(36), CONSTRAINT "FK_edaf18faca67e682be39b5ecae5" FOREIGN KEY ("user_uuid") REFERENCES "users" ("uuid") ON DELETE CASCADE ON UPDATE NO ACTION)',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO "temporary_revoked_sessions"("uuid", "user_uuid", "received", "created_at", "received_at", "user_agent", "api_version") SELECT "uuid", "user_uuid", "received", "created_at", "received_at", "user_agent", "api_version" FROM "revoked_sessions"',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE "revoked_sessions"')
|
||||
await queryRunner.query('ALTER TABLE "temporary_revoked_sessions" RENAME TO "revoked_sessions"')
|
||||
await queryRunner.query('CREATE INDEX "index_revoked_sessions_on_user_uuid" ON "revoked_sessions" ("user_uuid") ')
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX "index_revoked_sessions_on_private_identifier" ON "revoked_sessions" ("private_identifier") ',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "index_revoked_sessions_on_user_uuid"')
|
||||
await queryRunner.query('ALTER TABLE "revoked_sessions" RENAME TO "temporary_revoked_sessions"')
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "revoked_sessions" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "received" tinyint NOT NULL DEFAULT (0), "created_at" datetime NOT NULL, "received_at" datetime, "user_agent" text, "api_version" varchar(255), CONSTRAINT "FK_edaf18faca67e682be39b5ecae5" FOREIGN KEY ("user_uuid") REFERENCES "users" ("uuid") ON DELETE CASCADE ON UPDATE NO ACTION)',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO "revoked_sessions"("uuid", "user_uuid", "received", "created_at", "received_at", "user_agent", "api_version") SELECT "uuid", "user_uuid", "received", "created_at", "received_at", "user_agent", "api_version" FROM "temporary_revoked_sessions"',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE "temporary_revoked_sessions"')
|
||||
await queryRunner.query('CREATE INDEX "index_revoked_sessions_on_user_uuid" ON "revoked_sessions" ("user_uuid") ')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddApplicationAndSnjsToSessions1710236132439 implements MigrationInterface {
|
||||
name = 'AddApplicationAndSnjsToSessions1710236132439'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `sessions` ADD `application` varchar(255) NULL')
|
||||
await queryRunner.query('ALTER TABLE `sessions` ADD `snjs` varchar(255) NULL')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `sessions` DROP COLUMN `snjs`')
|
||||
await queryRunner.query('ALTER TABLE `sessions` DROP COLUMN `application`')
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.177.20",
|
||||
"version": "1.178.6",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
"description": "Auth Server",
|
||||
"description": "Auth Server for SN",
|
||||
"main": "dist/src/index.js",
|
||||
"typings": "dist/src/index.d.ts",
|
||||
"author": "Karol Sójko <karol@standardnotes.com>",
|
||||
@@ -24,8 +24,7 @@
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --fix --ext .ts",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=2",
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"cleanup": "yarn node dist/bin/cleanup.js",
|
||||
@@ -60,7 +59,10 @@
|
||||
"@standardnotes/sncrypto-common": "^1.13.4",
|
||||
"@standardnotes/sncrypto-node": "workspace:*",
|
||||
"@standardnotes/time": "workspace:*",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"axios": "^1.6.7",
|
||||
"bcryptjs": "2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "2.8.5",
|
||||
"dayjs": "^1.11.6",
|
||||
"dotenv": "^16.0.1",
|
||||
@@ -68,7 +70,7 @@
|
||||
"inversify": "^6.0.1",
|
||||
"inversify-express-utils": "^6.4.3",
|
||||
"ioredis": "^5.2.4",
|
||||
"mysql2": "^3.0.1",
|
||||
"mysql2": "^3.9.7",
|
||||
"otplib": "12.0.1",
|
||||
"prettyjson": "^1.2.5",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
@@ -80,6 +82,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1",
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/ioredis": "^5.0.0",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as winston from 'winston'
|
||||
import * as AgentKeepAlive from 'agentkeepalive'
|
||||
import Redis from 'ioredis'
|
||||
import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
|
||||
import { S3Client } from '@aws-sdk/client-s3'
|
||||
import { Container } from 'inversify'
|
||||
@@ -36,13 +38,11 @@ import { AuthResponseFactoryResolver } from '../Domain/Auth/AuthResponseFactoryR
|
||||
import { ClearLoginAttempts } from '../Domain/UseCase/ClearLoginAttempts'
|
||||
import { IncreaseLoginAttempts } from '../Domain/UseCase/IncreaseLoginAttempts'
|
||||
import { GetUserKeyParams } from '../Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
|
||||
import { UpdateUser } from '../Domain/UseCase/UpdateUser'
|
||||
import { RedisEphemeralSessionRepository } from '../Infra/Redis/RedisEphemeralSessionRepository'
|
||||
import { GetActiveSessionsForUser } from '../Domain/UseCase/GetActiveSessionsForUser'
|
||||
import { DeleteOtherSessionsForUser } from '../Domain/UseCase/DeleteOtherSessionsForUser'
|
||||
import { DeleteSessionForUser } from '../Domain/UseCase/DeleteSessionForUser'
|
||||
import { Register } from '../Domain/UseCase/Register'
|
||||
import { LockRepository } from '../Infra/Redis/LockRepository'
|
||||
import { TypeORMRevokedSessionRepository } from '../Infra/TypeORM/TypeORMRevokedSessionRepository'
|
||||
import { AuthenticationMethodResolver } from '../Domain/Auth/AuthenticationMethodResolver'
|
||||
import { RevokedSession } from '../Domain/Session/RevokedSession'
|
||||
@@ -285,6 +285,20 @@ import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscript
|
||||
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
|
||||
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
|
||||
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
|
||||
import { SubscriptionStateFetchedEventHandler } from '../Domain/Handler/SubscriptionStateFetchedEventHandler'
|
||||
import { CaptchaServerInterface } from '../Domain/HumanVerification/CaptchaServerInterface'
|
||||
import { VerifyHumanInteraction } from '../Domain/UseCase/VerifyHumanInteraction/VerifyHumanInteraction'
|
||||
import { HttpCaptchaServer } from '../Infra/Http/HumanVerification/HttpCaptchaServer'
|
||||
import { CookieFactoryInterface } from '../Domain/Auth/Cookies/CookieFactoryInterface'
|
||||
import { CookieFactory } from '../Domain/Auth/Cookies/CookieFactory'
|
||||
import { RedisLockRepository } from '../Infra/Redis/RedisLockRepository'
|
||||
import { DeleteSessionByToken } from '../Domain/UseCase/DeleteSessionByToken/DeleteSessionByToken'
|
||||
import { GetSessionFromToken } from '../Domain/UseCase/GetSessionFromToken/GetSessionFromToken'
|
||||
import { CooldownSessionTokens } from '../Domain/UseCase/CooldownSessionTokens/CooldownSessionTokens'
|
||||
import { SessionTokensCooldownRepositoryInterface } from '../Domain/Session/SessionTokensCooldownRepositoryInterface'
|
||||
import { RedisSessionTokensCooldownRepository } from '../Infra/Redis/RedisSessionTokensCooldownRepository'
|
||||
import { InMemorySessionTokensCooldownRepository } from '../Infra/InMemory/InMemorySessionTokensCooldownRepository'
|
||||
import { GetCooldownSessionTokens } from '../Domain/UseCase/GetCooldownSessionTokens/GetCooldownSessionTokens'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
@@ -329,6 +343,8 @@ export class ContainerConfigLoader {
|
||||
const isConfiguredForSelfHosting = env.get('MODE', true) === 'self-hosted'
|
||||
const isConfiguredForHomeServerOrSelfHosting = isConfiguredForHomeServer || isConfiguredForSelfHosting
|
||||
const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
|
||||
const captchaServerUrl = env.get('CAPTCHA_SERVER_URL', true)
|
||||
const captchaUIUrl = env.get('CAPTCHA_UI_URL', true)
|
||||
|
||||
container
|
||||
.bind<boolean>(TYPES.Auth_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING)
|
||||
@@ -596,9 +612,17 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind(TYPES.Auth_MAX_LOGIN_ATTEMPTS)
|
||||
.toConstantValue(env.get('MAX_LOGIN_ATTEMPTS', true) ? +env.get('MAX_LOGIN_ATTEMPTS', true) : 6)
|
||||
container
|
||||
.bind(TYPES.Auth_MAX_CAPTCHA_LOGIN_ATTEMPTS)
|
||||
.toConstantValue(env.get('MAX_CAPTCHA_LOGIN_ATTEMPTS', true) ? +env.get('MAX_CAPTCHA_LOGIN_ATTEMPTS', true) : 6)
|
||||
container
|
||||
.bind(TYPES.Auth_FAILED_LOGIN_LOCKOUT)
|
||||
.toConstantValue(env.get('FAILED_LOGIN_LOCKOUT', true) ? +env.get('FAILED_LOGIN_LOCKOUT', true) : 3600)
|
||||
container
|
||||
.bind(TYPES.Auth_FAILED_LOGIN_CAPTCHA_LOCKOUT)
|
||||
.toConstantValue(
|
||||
env.get('FAILED_LOGIN_CAPTCHA_LOCKOUT', true) ? +env.get('FAILED_LOGIN_CAPTCHA_LOCKOUT', true) : 86400,
|
||||
)
|
||||
container.bind(TYPES.Auth_PSEUDO_KEY_PARAMS_KEY).toConstantValue(env.get('PSEUDO_KEY_PARAMS_KEY'))
|
||||
container
|
||||
.bind(TYPES.Auth_EPHEMERAL_SESSION_AGE)
|
||||
@@ -632,6 +656,10 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind(TYPES.Auth_READONLY_USERS)
|
||||
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
|
||||
container.bind(TYPES.Auth_CAPTCHA_SERVER_URL).toConstantValue(captchaServerUrl)
|
||||
container.bind(TYPES.Auth_CAPTCHA_UI_URL).toConstantValue(captchaUIUrl)
|
||||
container.bind<boolean>(TYPES.Auth_HUMAN_VERIFICATION_ENABLED).toConstantValue(!!captchaServerUrl && !!captchaUIUrl)
|
||||
container.bind<boolean>(TYPES.Auth_FORCE_LEGACY_SESSIONS).toConstantValue(env.get('E2E_TESTING', true) === 'true')
|
||||
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
@@ -651,6 +679,7 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_Timer),
|
||||
container.get(TYPES.Auth_MAX_LOGIN_ATTEMPTS),
|
||||
container.get(TYPES.Auth_FAILED_LOGIN_LOCKOUT),
|
||||
container.get(TYPES.Auth_FAILED_LOGIN_CAPTCHA_LOCKOUT),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -678,9 +707,21 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SessionTokensCooldownRepositoryInterface>(TYPES.Auth_SessionTokensCooldownRepository)
|
||||
.toConstantValue(new InMemorySessionTokensCooldownRepository())
|
||||
} else {
|
||||
container.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository).to(RedisPKCERepository)
|
||||
container.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository).to(LockRepository)
|
||||
container
|
||||
.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository)
|
||||
.toConstantValue(
|
||||
new RedisLockRepository(
|
||||
container.get<Redis>(TYPES.Auth_Redis),
|
||||
container.get<number>(TYPES.Auth_MAX_LOGIN_ATTEMPTS),
|
||||
container.get<number>(TYPES.Auth_FAILED_LOGIN_LOCKOUT),
|
||||
container.get<number>(TYPES.Auth_FAILED_LOGIN_CAPTCHA_LOCKOUT),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<EphemeralSessionRepositoryInterface>(TYPES.Auth_EphemeralSessionRepository)
|
||||
.to(RedisEphemeralSessionRepository)
|
||||
@@ -690,6 +731,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<SubscriptionTokenRepositoryInterface>(TYPES.Auth_SubscriptionTokenRepository)
|
||||
.to(RedisSubscriptionTokenRepository)
|
||||
container
|
||||
.bind<SessionTokensCooldownRepositoryInterface>(TYPES.Auth_SessionTokensCooldownRepository)
|
||||
.toConstantValue(new RedisSessionTokensCooldownRepository(container.get<Redis>(TYPES.Auth_Redis)))
|
||||
}
|
||||
|
||||
container
|
||||
@@ -739,6 +783,41 @@ export class ContainerConfigLoader {
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<string[]>(TYPES.Auth_READONLY_USERS),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<boolean>(TYPES.Auth_FORCE_LEGACY_SESSIONS),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetCooldownSessionTokens>(TYPES.Auth_GetCooldownSessionTokens)
|
||||
.toConstantValue(
|
||||
new GetCooldownSessionTokens(
|
||||
container.get<SessionTokensCooldownRepositoryInterface>(TYPES.Auth_SessionTokensCooldownRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetSessionFromToken>(TYPES.Auth_GetSessionFromToken)
|
||||
.toConstantValue(
|
||||
new GetSessionFromToken(
|
||||
container.get<SessionRepositoryInterface>(TYPES.Auth_SessionRepository),
|
||||
container.get<EphemeralSessionRepositoryInterface>(TYPES.Auth_EphemeralSessionRepository),
|
||||
container.get<GetCooldownSessionTokens>(TYPES.Auth_GetCooldownSessionTokens),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DeleteSessionByToken>(TYPES.Auth_DeleteSessionByToken)
|
||||
.toConstantValue(
|
||||
new DeleteSessionByToken(
|
||||
container.get<GetSessionFromToken>(TYPES.Auth_GetSessionFromToken),
|
||||
container.get<SessionRepositoryInterface>(TYPES.Auth_SessionRepository),
|
||||
container.get<EphemeralSessionRepositoryInterface>(TYPES.Auth_EphemeralSessionRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<CooldownSessionTokens>(TYPES.Auth_CooldownSessionTokens)
|
||||
.toConstantValue(
|
||||
new CooldownSessionTokens(
|
||||
env.get('COOLDOWN_SESSION_TOKENS_TTL', true) ? +env.get('COOLDOWN_SESSION_TOKENS_TTL', true) : 120,
|
||||
container.get<SessionTokensCooldownRepositoryInterface>(TYPES.Auth_SessionTokensCooldownRepository),
|
||||
),
|
||||
)
|
||||
container.bind<AuthResponseFactory20161215>(TYPES.Auth_AuthResponseFactory20161215).to(AuthResponseFactory20161215)
|
||||
@@ -779,7 +858,16 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(new TokenEncoder<ValetTokenData>(container.get(TYPES.Auth_VALET_TOKEN_SECRET)))
|
||||
container
|
||||
.bind<AuthenticationMethodResolver>(TYPES.Auth_AuthenticationMethodResolver)
|
||||
.to(AuthenticationMethodResolver)
|
||||
.toConstantValue(
|
||||
new AuthenticationMethodResolver(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<SessionServiceInterface>(TYPES.Auth_SessionService),
|
||||
container.get<TokenDecoderInterface<SessionTokenData>>(TYPES.Auth_SessionTokenDecoder),
|
||||
container.get<TokenDecoderInterface<SessionTokenData>>(TYPES.Auth_FallbackSessionTokenDecoder),
|
||||
container.get<GetSessionFromToken>(TYPES.Auth_GetSessionFromToken),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container.bind<DomainEventFactory>(TYPES.Auth_DomainEventFactory).to(DomainEventFactory)
|
||||
container
|
||||
.bind<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService)
|
||||
@@ -818,6 +906,43 @@ export class ContainerConfigLoader {
|
||||
.bind<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector)
|
||||
.toConstantValue(new DeterministicSelector<boolean>())
|
||||
|
||||
const httpAgentKeepAliveTimeout = env.get('HTTP_AGENT_KEEP_ALIVE_TIMEOUT', true)
|
||||
? +env.get('HTTP_AGENT_KEEP_ALIVE_TIMEOUT', true)
|
||||
: 4_000
|
||||
|
||||
container.bind<AxiosInstance>(TYPES.Auth_HTTPClient).toConstantValue(
|
||||
axios.create({
|
||||
httpAgent: new AgentKeepAlive({
|
||||
keepAlive: true,
|
||||
timeout: 2 * httpAgentKeepAliveTimeout,
|
||||
freeSocketTimeout: httpAgentKeepAliveTimeout,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
container
|
||||
.bind<CaptchaServerInterface>(TYPES.Auth_CaptchaServer)
|
||||
.toConstantValue(
|
||||
new HttpCaptchaServer(
|
||||
container.get(TYPES.Auth_Logger),
|
||||
container.get(TYPES.Auth_HTTPClient),
|
||||
container.get(TYPES.Auth_CAPTCHA_SERVER_URL),
|
||||
),
|
||||
)
|
||||
|
||||
container
|
||||
.bind<CookieFactoryInterface>(TYPES.Auth_CookieFactory)
|
||||
.toConstantValue(
|
||||
new CookieFactory(
|
||||
['None', 'Lax', 'Strict'].includes(env.get('COOKIE_SAME_SITE', true))
|
||||
? (env.get('COOKIE_SAME_SITE', true) as 'None' | 'Lax' | 'Strict')
|
||||
: 'None',
|
||||
env.get('COOKIE_DOMAIN', true) ?? 'standardnotes.com',
|
||||
env.get('COOKIE_SECURE', true) ? env.get('COOKIE_SECURE', true) === 'true' : true,
|
||||
env.get('COOKIE_PARTITIONED', true) ? env.get('COOKIE_PARTITIONED', true) === 'true' : true,
|
||||
),
|
||||
)
|
||||
|
||||
// Middleware
|
||||
container.bind<SessionMiddleware>(TYPES.Auth_SessionMiddleware).to(SessionMiddleware)
|
||||
container.bind<LockMiddleware>(TYPES.Auth_LockMiddleware).to(LockMiddleware)
|
||||
@@ -952,6 +1077,7 @@ export class ContainerConfigLoader {
|
||||
new SetSubscriptionSettingValue(
|
||||
container.get<SubscriptionSettingRepositoryInterface>(TYPES.Auth_SubscriptionSettingRepository),
|
||||
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
|
||||
container.get<SettingsAssociationServiceInterface>(TYPES.Auth_SettingsAssociationService),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
@@ -996,10 +1122,36 @@ export class ContainerConfigLoader {
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<CooldownSessionTokens>(TYPES.Auth_CooldownSessionTokens),
|
||||
container.get<GetSessionFromToken>(TYPES.Auth_GetSessionFromToken),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container.bind<SignIn>(TYPES.Auth_SignIn).to(SignIn)
|
||||
container
|
||||
.bind<VerifyHumanInteraction>(TYPES.Auth_VerifyHumanInteraction)
|
||||
.toConstantValue(
|
||||
new VerifyHumanInteraction(
|
||||
container.get(TYPES.Auth_HUMAN_VERIFICATION_ENABLED),
|
||||
container.get<CaptchaServerInterface>(TYPES.Auth_CaptchaServer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SignIn>(TYPES.Auth_SignIn)
|
||||
.toConstantValue(
|
||||
new SignIn(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<AuthResponseFactoryResolverInterface>(TYPES.Auth_AuthResponseFactoryResolver),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<SessionServiceInterface>(TYPES.Auth_SessionService),
|
||||
container.get<PKCERepositoryInterface>(TYPES.Auth_PKCERepository),
|
||||
container.get<CrypterInterface>(TYPES.Auth_Crypter),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
container.get<number>(TYPES.Auth_MAX_LOGIN_ATTEMPTS),
|
||||
container.get<LockRepositoryInterface>(TYPES.Auth_LockRepository),
|
||||
container.get<VerifyHumanInteraction>(TYPES.Auth_VerifyHumanInteraction),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<VerifyMFA>(TYPES.Auth_VerifyMFA)
|
||||
.toConstantValue(
|
||||
@@ -1016,8 +1168,24 @@ export class ContainerConfigLoader {
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container.bind<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts).to(ClearLoginAttempts)
|
||||
container.bind<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts).to(IncreaseLoginAttempts)
|
||||
container
|
||||
.bind<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts)
|
||||
.toConstantValue(
|
||||
new ClearLoginAttempts(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<LockRepositoryInterface>(TYPES.Auth_LockRepository),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts)
|
||||
.toConstantValue(
|
||||
new IncreaseLoginAttempts(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<LockRepositoryInterface>(TYPES.Auth_LockRepository),
|
||||
container.get<number>(TYPES.Auth_MAX_LOGIN_ATTEMPTS),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetUserKeyParamsRecovery>(TYPES.Auth_GetUserKeyParamsRecovery)
|
||||
.toConstantValue(
|
||||
@@ -1028,7 +1196,6 @@ export class ContainerConfigLoader {
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
),
|
||||
)
|
||||
container.bind<UpdateUser>(TYPES.Auth_UpdateUser).to(UpdateUser)
|
||||
container
|
||||
.bind<ApplyDefaultSettings>(TYPES.Auth_ApplyDefaultSettings)
|
||||
.toConstantValue(
|
||||
@@ -1129,6 +1296,9 @@ export class ContainerConfigLoader {
|
||||
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
|
||||
container.get<AuthenticatorRepositoryInterface>(TYPES.Auth_AuthenticatorRepository),
|
||||
container.get<number>(TYPES.Auth_MAX_LOGIN_ATTEMPTS),
|
||||
container.get<LockRepositoryInterface>(TYPES.Auth_LockRepository),
|
||||
container.get<VerifyHumanInteraction>(TYPES.Auth_VerifyHumanInteraction),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -1261,7 +1431,6 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
new TriggerEmailBackupForUser(
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
@@ -1336,15 +1505,9 @@ export class ContainerConfigLoader {
|
||||
.bind<AuthController>(TYPES.Auth_AuthController)
|
||||
.toConstantValue(
|
||||
new AuthController(
|
||||
container.get(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get(TYPES.Auth_Register),
|
||||
container.get(TYPES.Auth_DomainEventPublisher),
|
||||
container.get(TYPES.Auth_DomainEventFactory),
|
||||
container.get(TYPES.Auth_SignInWithRecoveryCodes),
|
||||
container.get(TYPES.Auth_GetUserKeyParamsRecovery),
|
||||
container.get(TYPES.Auth_GenerateRecoveryCodes),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
container.get(TYPES.Auth_SessionService),
|
||||
container.get<GetUserKeyParamsRecovery>(TYPES.Auth_GetUserKeyParamsRecovery),
|
||||
container.get<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -1579,6 +1742,16 @@ export class ContainerConfigLoader {
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionStateFetchedEventHandler>(TYPES.Auth_SubscriptionStateFetchedEventHandler)
|
||||
.toConstantValue(
|
||||
new SubscriptionStateFetchedEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
|
||||
@@ -1620,6 +1793,7 @@ export class ContainerConfigLoader {
|
||||
'FILE_QUOTA_RECALCULATED',
|
||||
container.get<FileQuotaRecalculatedEventHandler>(TYPES.Auth_FileQuotaRecalculatedEventHandler),
|
||||
],
|
||||
['SUBSCRIPTION_STATE_FETCHED', container.get(TYPES.Auth_SubscriptionStateFetchedEventHandler)],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
@@ -1652,14 +1826,23 @@ export class ContainerConfigLoader {
|
||||
.bind<BaseAuthController>(TYPES.Auth_BaseAuthController)
|
||||
.toConstantValue(
|
||||
new BaseAuthController(
|
||||
container.get(TYPES.Auth_VerifyMFA),
|
||||
container.get(TYPES.Auth_SignIn),
|
||||
container.get(TYPES.Auth_GetUserKeyParams),
|
||||
container.get(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get(TYPES.Auth_IncreaseLoginAttempts),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
container.get(TYPES.Auth_AuthController),
|
||||
container.get(TYPES.Auth_ControllerContainer),
|
||||
container.get<VerifyMFA>(TYPES.Auth_VerifyMFA),
|
||||
container.get<SignIn>(TYPES.Auth_SignIn),
|
||||
container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
|
||||
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
container.get<AuthController>(TYPES.Auth_AuthController),
|
||||
container.get<Register>(TYPES.Auth_Register),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<SessionServiceInterface>(TYPES.Auth_SessionService),
|
||||
container.get<VerifyHumanInteraction>(TYPES.Auth_VerifyHumanInteraction),
|
||||
container.get<CookieFactoryInterface>(TYPES.Auth_CookieFactory),
|
||||
container.get<SignInWithRecoveryCodes>(TYPES.Auth_SignInWithRecoveryCodes),
|
||||
container.get<DeleteSessionByToken>(TYPES.Auth_DeleteSessionByToken),
|
||||
container.get<string>(TYPES.Auth_CAPTCHA_UI_URL),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1726,6 +1909,7 @@ export class ContainerConfigLoader {
|
||||
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
|
||||
container.get<ChangeCredentials>(TYPES.Auth_ChangeCredentials),
|
||||
container.get<CookieFactoryInterface>(TYPES.Auth_CookieFactory),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
@@ -1733,11 +1917,12 @@ export class ContainerConfigLoader {
|
||||
.bind<BaseAdminController>(TYPES.Auth_BaseAdminController)
|
||||
.toConstantValue(
|
||||
new BaseAdminController(
|
||||
container.get(TYPES.Auth_DeleteSetting),
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_CreateSubscriptionToken),
|
||||
container.get(TYPES.Auth_CreateOfflineSubscriptionToken),
|
||||
container.get(TYPES.Auth_ControllerContainer),
|
||||
container.get<DeleteSetting>(TYPES.Auth_DeleteSetting),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<CreateSubscriptionToken>(TYPES.Auth_CreateSubscriptionToken),
|
||||
container.get<CreateOfflineSubscriptionToken>(TYPES.Auth_CreateOfflineSubscriptionToken),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -1760,9 +1945,12 @@ export class ContainerConfigLoader {
|
||||
new BaseSubscriptionSettingsController(
|
||||
container.get<GetSubscriptionSetting>(TYPES.Auth_GetSubscriptionSetting),
|
||||
container.get<GetSharedOrRegularSubscriptionForUser>(TYPES.Auth_GetSharedOrRegularSubscriptionForUser),
|
||||
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
|
||||
container.get<TriggerPostSettingUpdateActions>(TYPES.Auth_TriggerPostSettingUpdateActions),
|
||||
container.get<MapperInterface<SubscriptionSetting, SubscriptionSettingHttpRepresentation>>(
|
||||
TYPES.Auth_SubscriptionSettingHttpMapper,
|
||||
),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
@@ -1787,10 +1975,11 @@ export class ContainerConfigLoader {
|
||||
.bind<BaseSessionController>(TYPES.Auth_BaseSessionController)
|
||||
.toConstantValue(
|
||||
new BaseSessionController(
|
||||
container.get(TYPES.Auth_DeleteSessionForUser),
|
||||
container.get(TYPES.Auth_DeleteOtherSessionsForUser),
|
||||
container.get(TYPES.Auth_RefreshSessionToken),
|
||||
container.get(TYPES.Auth_ControllerContainer),
|
||||
container.get<DeleteSessionForUser>(TYPES.Auth_DeleteSessionForUser),
|
||||
container.get<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser),
|
||||
container.get<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken),
|
||||
container.get<CookieFactoryInterface>(TYPES.Auth_CookieFactory),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
container
|
||||
|
||||
@@ -34,6 +34,7 @@ const TYPES = {
|
||||
Auth_UserSubscriptionRepository: Symbol.for('Auth_UserSubscriptionRepository'),
|
||||
Auth_OfflineUserSubscriptionRepository: Symbol.for('Auth_OfflineUserSubscriptionRepository'),
|
||||
Auth_SubscriptionTokenRepository: Symbol.for('Auth_SubscriptionTokenRepository'),
|
||||
Auth_SessionTokensCooldownRepository: Symbol.for('Auth_SessionTokensCooldownRepository'),
|
||||
Auth_OfflineSubscriptionTokenRepository: Symbol.for('Auth_OfflineSubscriptionTokenRepository'),
|
||||
Auth_SharedSubscriptionInvitationRepository: Symbol.for('Auth_SharedSubscriptionInvitationRepository'),
|
||||
Auth_PKCERepository: Symbol.for('Auth_PKCERepository'),
|
||||
@@ -84,7 +85,9 @@ const TYPES = {
|
||||
Auth_REFRESH_TOKEN_AGE: Symbol.for('Auth_REFRESH_TOKEN_AGE'),
|
||||
Auth_EPHEMERAL_SESSION_AGE: Symbol.for('Auth_EPHEMERAL_SESSION_AGE'),
|
||||
Auth_MAX_LOGIN_ATTEMPTS: Symbol.for('Auth_MAX_LOGIN_ATTEMPTS'),
|
||||
Auth_MAX_CAPTCHA_LOGIN_ATTEMPTS: Symbol.for('Auth_MAX_CAPTCHA_LOGIN_ATTEMPTS'),
|
||||
Auth_FAILED_LOGIN_LOCKOUT: Symbol.for('Auth_FAILED_LOGIN_LOCKOUT'),
|
||||
Auth_FAILED_LOGIN_CAPTCHA_LOCKOUT: Symbol.for('Auth_FAILED_LOGIN_CAPTCHA_LOCKOUT'),
|
||||
Auth_PSEUDO_KEY_PARAMS_KEY: Symbol.for('Auth_PSEUDO_KEY_PARAMS_KEY'),
|
||||
Auth_REDIS_URL: Symbol.for('Auth_REDIS_URL'),
|
||||
Auth_DISABLE_USER_REGISTRATION: Symbol.for('Auth_DISABLE_USER_REGISTRATION'),
|
||||
@@ -100,6 +103,10 @@ const TYPES = {
|
||||
Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'),
|
||||
Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'),
|
||||
Auth_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING: Symbol.for('Auth_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING'),
|
||||
Auth_CAPTCHA_SERVER_URL: Symbol.for('Auth_CAPTCHA_SERVER_URL'),
|
||||
Auth_CAPTCHA_UI_URL: Symbol.for('Auth_CAPTCHA_UI_URL'),
|
||||
Auth_HUMAN_VERIFICATION_ENABLED: Symbol.for('Auth_HUMAN_VERIFICATION_ENABLED'),
|
||||
Auth_FORCE_LEGACY_SESSIONS: Symbol.for('Auth_FORCE_LEGACY_SESSIONS'),
|
||||
// use cases
|
||||
Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'),
|
||||
Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'),
|
||||
@@ -109,7 +116,6 @@ const TYPES = {
|
||||
Auth_ClearLoginAttempts: Symbol.for('Auth_ClearLoginAttempts'),
|
||||
Auth_IncreaseLoginAttempts: Symbol.for('Auth_IncreaseLoginAttempts'),
|
||||
Auth_GetUserKeyParams: Symbol.for('Auth_GetUserKeyParams'),
|
||||
Auth_UpdateUser: Symbol.for('Auth_UpdateUser'),
|
||||
Auth_Register: Symbol.for('Auth_Register'),
|
||||
Auth_GetActiveSessionsForUser: Symbol.for('Auth_GetActiveSessionsForUser'),
|
||||
Auth_DeleteOtherSessionsForUser: Symbol.for('Auth_DeleteOtherSessionsForUser'),
|
||||
@@ -158,6 +164,10 @@ const TYPES = {
|
||||
Auth_ApplyDefaultSettings: Symbol.for('Auth_ApplyDefaultSettings'),
|
||||
Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'),
|
||||
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
|
||||
Auth_GetSessionFromToken: Symbol.for('Auth_GetSessionFromToken'),
|
||||
Auth_DeleteSessionByToken: Symbol.for('Auth_DeleteSessionByToken'),
|
||||
Auth_CooldownSessionTokens: Symbol.for('Auth_CooldownSessionTokens'),
|
||||
Auth_GetCooldownSessionTokens: Symbol.for('Auth_GetCooldownSessionTokens'),
|
||||
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
|
||||
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
|
||||
Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'),
|
||||
@@ -171,6 +181,7 @@ const TYPES = {
|
||||
Auth_DeleteAccountsFromCSVFile: Symbol.for('Auth_DeleteAccountsFromCSVFile'),
|
||||
Auth_RenewSharedSubscriptions: Symbol.for('Auth_RenewSharedSubscriptions'),
|
||||
Auth_FixStorageQuotaForUser: Symbol.for('Auth_FixStorageQuotaForUser'),
|
||||
Auth_VerifyHumanInteraction: Symbol.for('Auth_VerifyHumanInteraction'),
|
||||
// Handlers
|
||||
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
|
||||
Auth_AccountDeletionVerificationPassedEventHandler: Symbol.for('Auth_AccountDeletionVerificationPassedEventHandler'),
|
||||
@@ -205,7 +216,9 @@ const TYPES = {
|
||||
),
|
||||
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
|
||||
Auth_FileQuotaRecalculatedEventHandler: Symbol.for('Auth_FileQuotaRecalculatedEventHandler'),
|
||||
Auth_SubscriptionStateFetchedEventHandler: Symbol.for('Auth_SubscriptionStateFetchedEventHandler'),
|
||||
// Services
|
||||
Auth_CookieFactory: Symbol.for('Auth_CookieFactory'),
|
||||
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
||||
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
||||
Auth_OfflineSettingService: Symbol.for('Auth_OfflineSettingService'),
|
||||
@@ -258,6 +271,8 @@ const TYPES = {
|
||||
Auth_BaseListedController: Symbol.for('Auth_BaseListedController'),
|
||||
Auth_BaseFeaturesController: Symbol.for('Auth_BaseFeaturesController'),
|
||||
Auth_CSVFileReader: Symbol.for('Auth_CSVFileReader'),
|
||||
Auth_CaptchaServer: Symbol.for('Auth_CaptchaServer'),
|
||||
Auth_HTTPClient: Symbol.for('Auth_HTTPClient'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { AuthController } from './AuthController'
|
||||
import { ClearLoginAttempts } from '../Domain/UseCase/ClearLoginAttempts'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { Register } from '../Domain/UseCase/Register'
|
||||
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
|
||||
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
||||
import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
|
||||
import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes'
|
||||
import { Logger } from 'winston'
|
||||
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
|
||||
import { ApiVersion } from '../Domain/Api/ApiVersion'
|
||||
|
||||
describe('AuthController', () => {
|
||||
let clearLoginAttempts: ClearLoginAttempts
|
||||
let register: Register
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let event: DomainEventInterface
|
||||
let user: User
|
||||
let doSignInWithRecoveryCodes: SignInWithRecoveryCodes
|
||||
let getUserKeyParamsRecovery: GetUserKeyParamsRecovery
|
||||
let doGenerateRecoveryCodes: GenerateRecoveryCodes
|
||||
let logger: Logger
|
||||
let sessionService: SessionServiceInterface
|
||||
|
||||
const createController = () =>
|
||||
new AuthController(
|
||||
clearLoginAttempts,
|
||||
register,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
doSignInWithRecoveryCodes,
|
||||
getUserKeyParamsRecovery,
|
||||
doGenerateRecoveryCodes,
|
||||
logger,
|
||||
sessionService,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
register = {} as jest.Mocked<Register>
|
||||
register.execute = jest.fn()
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
user.email = 'test@test.te'
|
||||
|
||||
clearLoginAttempts = {} as jest.Mocked<ClearLoginAttempts>
|
||||
clearLoginAttempts.execute = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<DomainEventInterface>
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createUserRegisteredEvent = jest.fn().mockReturnValue(event)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.deleteSessionByToken = jest.fn().mockReturnValue('1-2-3')
|
||||
})
|
||||
|
||||
it('should register a user', async () => {
|
||||
register.execute = jest.fn().mockReturnValue({ success: true, authResponse: { user } })
|
||||
|
||||
const response = await createController().register({
|
||||
email: 'test@test.te',
|
||||
password: 'asdzxc',
|
||||
version: ProtocolVersion.V004,
|
||||
api: ApiVersion.v20200115,
|
||||
origination: KeyParamsOrigination.Registration,
|
||||
userAgent: 'Google Chrome',
|
||||
identifier: 'test@test.te',
|
||||
pw_nonce: '11',
|
||||
ephemeral: false,
|
||||
})
|
||||
|
||||
expect(register.execute).toHaveBeenCalledWith({
|
||||
apiVersion: '20200115',
|
||||
kpOrigination: 'registration',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
ephemeralSession: false,
|
||||
version: '004',
|
||||
email: 'test@test.te',
|
||||
password: 'asdzxc',
|
||||
pwNonce: '11',
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledWith(event)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toEqual({ user: { email: 'test@test.te' } })
|
||||
})
|
||||
|
||||
it('should not register a user if request param is missing', async () => {
|
||||
const response = await createController().register({
|
||||
email: 'test@test.te',
|
||||
password: '',
|
||||
version: ProtocolVersion.V004,
|
||||
api: ApiVersion.v20200115,
|
||||
origination: KeyParamsOrigination.Registration,
|
||||
userAgent: 'Google Chrome',
|
||||
identifier: 'test@test.te',
|
||||
pw_nonce: '11',
|
||||
ephemeral: false,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
|
||||
expect(response.status).toEqual(400)
|
||||
})
|
||||
|
||||
it('should respond with error if registering a user fails', async () => {
|
||||
register.execute = jest.fn().mockReturnValue({ success: false, errorMessage: 'Something bad happened' })
|
||||
|
||||
const response = await createController().register({
|
||||
email: 'test@test.te',
|
||||
password: 'test',
|
||||
version: ProtocolVersion.V004,
|
||||
api: ApiVersion.v20200115,
|
||||
origination: KeyParamsOrigination.Registration,
|
||||
userAgent: 'Google Chrome',
|
||||
identifier: 'test@test.te',
|
||||
pw_nonce: '11',
|
||||
ephemeral: false,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
|
||||
expect(response.status).toEqual(400)
|
||||
})
|
||||
|
||||
it('should throw error on the delete user method as it is still a part of the payments server', async () => {
|
||||
let caughtError = null
|
||||
try {
|
||||
await createController().deleteAccount({} as never)
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,42 +1,23 @@
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import {
|
||||
UserRegistrationRequestParams,
|
||||
UserServerInterface,
|
||||
UserDeletionResponseBody,
|
||||
UserRegistrationResponseBody,
|
||||
UserUpdateRequestParams,
|
||||
} from '@standardnotes/api'
|
||||
import { ErrorTag, HttpResponse, HttpStatusCode } from '@standardnotes/responses'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { UserDeletionResponseBody, UserUpdateRequestParams } from '@standardnotes/api'
|
||||
import { HttpResponse, HttpStatusCode } from '@standardnotes/responses'
|
||||
|
||||
import { ClearLoginAttempts } from '../Domain/UseCase/ClearLoginAttempts'
|
||||
import { Register } from '../Domain/UseCase/Register'
|
||||
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
|
||||
import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
||||
import { SignInWithRecoveryCodesRequestParams } from '../Infra/Http/Request/SignInWithRecoveryCodesRequestParams'
|
||||
import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
|
||||
import { RecoveryKeyParamsRequestParams } from '../Infra/Http/Request/RecoveryKeyParamsRequestParams'
|
||||
import { SignInWithRecoveryCodesResponseBody } from '../Infra/Http/Response/SignInWithRecoveryCodesResponseBody'
|
||||
import { RecoveryKeyParamsResponseBody } from '../Infra/Http/Response/RecoveryKeyParamsResponseBody'
|
||||
import { GenerateRecoveryCodesResponseBody } from '../Infra/Http/Response/GenerateRecoveryCodesResponseBody'
|
||||
import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes'
|
||||
import { GenerateRecoveryCodesRequestParams } from '../Infra/Http/Request/GenerateRecoveryCodesRequestParams'
|
||||
import { Logger } from 'winston'
|
||||
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
|
||||
import { ApiVersion } from '../Domain/Api/ApiVersion'
|
||||
import { UserUpdateResponse } from '@standardnotes/api/dist/Domain/Response/User/UserUpdateResponse'
|
||||
|
||||
export class AuthController implements UserServerInterface {
|
||||
/**
|
||||
* DEPRECATED: This controller is deprecated and will be removed in the future.
|
||||
*/
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private clearLoginAttempts: ClearLoginAttempts,
|
||||
private registerUser: Register,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private doSignInWithRecoveryCodes: SignInWithRecoveryCodes,
|
||||
private getUserKeyParamsRecovery: GetUserKeyParamsRecovery,
|
||||
private doGenerateRecoveryCodes: GenerateRecoveryCodes,
|
||||
private logger: Logger,
|
||||
private sessionService: SessionServiceInterface,
|
||||
) {}
|
||||
|
||||
async update(_params: UserUpdateRequestParams): Promise<HttpResponse<UserUpdateResponse>> {
|
||||
@@ -47,57 +28,6 @@ export class AuthController implements UserServerInterface {
|
||||
throw new Error('This method is implemented on the payments server.')
|
||||
}
|
||||
|
||||
async register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>> {
|
||||
if (!params.email || !params.password) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: 'Please enter an email and a password to register.',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const registerResult = await this.registerUser.execute({
|
||||
email: params.email,
|
||||
password: params.password,
|
||||
updatedWithUserAgent: params.userAgent as string,
|
||||
apiVersion: params.api,
|
||||
ephemeralSession: params.ephemeral,
|
||||
pwNonce: params.pw_nonce,
|
||||
kpOrigination: params.origination,
|
||||
kpCreated: params.created,
|
||||
version: params.version,
|
||||
})
|
||||
|
||||
if (!registerResult.success) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: registerResult.errorMessage,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
await this.clearLoginAttempts.execute({ email: registerResult.authResponse.user.email as string })
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createUserRegisteredEvent({
|
||||
userUuid: <string>registerResult.authResponse.user.uuid,
|
||||
email: <string>registerResult.authResponse.user.email,
|
||||
protocolVersion: (<string>registerResult.authResponse.user.protocolVersion) as ProtocolVersion,
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: registerResult.authResponse,
|
||||
}
|
||||
}
|
||||
|
||||
async generateRecoveryCodes(
|
||||
params: GenerateRecoveryCodesRequestParams,
|
||||
): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
|
||||
@@ -124,62 +54,11 @@ export class AuthController implements UserServerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async signInWithRecoveryCodes(
|
||||
params: SignInWithRecoveryCodesRequestParams,
|
||||
): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
|
||||
if (params.apiVersion !== ApiVersion.v20200115) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: 'Invalid API version.',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.doSignInWithRecoveryCodes.execute({
|
||||
userAgent: params.userAgent,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
codeVerifier: params.codeVerifier,
|
||||
recoveryCodes: params.recoveryCodes,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.debug(`Failed to sign in with recovery codes: ${result.getError()}`)
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Unauthorized,
|
||||
data: {
|
||||
error: {
|
||||
message: 'Invalid login credentials.',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result.getValue(),
|
||||
}
|
||||
}
|
||||
|
||||
async recoveryKeyParams(
|
||||
params: RecoveryKeyParamsRequestParams,
|
||||
): Promise<HttpResponse<RecoveryKeyParamsResponseBody>> {
|
||||
if (params.apiVersion !== ApiVersion.v20200115) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: 'Invalid API version.',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.getUserKeyParamsRecovery.execute({
|
||||
apiVersion: params.apiVersion,
|
||||
username: params.username,
|
||||
codeChallenge: params.codeChallenge,
|
||||
recoveryCodes: params.recoveryCodes,
|
||||
@@ -205,33 +84,4 @@ export class AuthController implements UserServerInterface {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async signOut(params: Record<string, unknown>): Promise<HttpResponse> {
|
||||
if (params.readOnlyAccess) {
|
||||
return {
|
||||
status: HttpStatusCode.Unauthorized,
|
||||
data: {
|
||||
error: {
|
||||
tag: ErrorTag.ReadOnlyAccess,
|
||||
message: 'Session has read-only access.',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const userUuid = await this.sessionService.deleteSessionByToken(
|
||||
(params.authorizationHeader as string).replace('Bearer ', ''),
|
||||
)
|
||||
|
||||
let headers = undefined
|
||||
if (userUuid !== null) {
|
||||
headers = new Map([['x-invalidate-cache', userUuid]])
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.NoContent,
|
||||
data: {},
|
||||
headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,10 @@ describe('SubscriptionInvitesController', () => {
|
||||
invitations: [],
|
||||
})
|
||||
|
||||
const result = await createController().listInvites({ api: ApiVersion.v20200115, inviterEmail: 'test@test.te' })
|
||||
const result = await createController().listInvites({
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
inviterEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(listSharedSubscriptionInvitations.execute).toHaveBeenCalledWith({
|
||||
inviterEmail: 'test@test.te',
|
||||
@@ -68,7 +71,7 @@ describe('SubscriptionInvitesController', () => {
|
||||
})
|
||||
|
||||
const result = await createController().cancelInvite({
|
||||
api: ApiVersion.v20200115,
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
inviteUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
})
|
||||
@@ -87,7 +90,7 @@ describe('SubscriptionInvitesController', () => {
|
||||
})
|
||||
|
||||
const result = await createController().cancelInvite({
|
||||
api: ApiVersion.v20200115,
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
@@ -100,7 +103,7 @@ describe('SubscriptionInvitesController', () => {
|
||||
})
|
||||
|
||||
const result = await createController().declineInvite({
|
||||
api: ApiVersion.v20200115,
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
@@ -117,7 +120,7 @@ describe('SubscriptionInvitesController', () => {
|
||||
})
|
||||
|
||||
const result = await createController().declineInvite({
|
||||
api: ApiVersion.v20200115,
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
@@ -134,7 +137,7 @@ describe('SubscriptionInvitesController', () => {
|
||||
})
|
||||
|
||||
const result = await createController().acceptInvite({
|
||||
api: ApiVersion.v20200115,
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
@@ -151,7 +154,7 @@ describe('SubscriptionInvitesController', () => {
|
||||
})
|
||||
|
||||
const result = await createController().acceptInvite({
|
||||
api: ApiVersion.v20200115,
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
@@ -168,7 +171,7 @@ describe('SubscriptionInvitesController', () => {
|
||||
})
|
||||
|
||||
const result = await createController().invite({
|
||||
api: ApiVersion.v20200115,
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
identifier: 'invitee@test.te',
|
||||
inviterUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
@@ -187,7 +190,7 @@ describe('SubscriptionInvitesController', () => {
|
||||
|
||||
it('should not invite to user subscription if the identifier is missing in request', async () => {
|
||||
const result = await createController().invite({
|
||||
api: ApiVersion.v20200115,
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
identifier: '',
|
||||
inviterUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
@@ -205,7 +208,7 @@ describe('SubscriptionInvitesController', () => {
|
||||
})
|
||||
|
||||
const result = await createController().invite({
|
||||
api: ApiVersion.v20200115,
|
||||
api: ApiVersion.VERSIONS.v20200115,
|
||||
identifier: 'invitee@test.te',
|
||||
inviterUuid: '1-2-3',
|
||||
inviterEmail: 'test@test.te',
|
||||
|
||||
46
packages/auth/src/Domain/Api/ApiVersion.spec.ts
Normal file
46
packages/auth/src/Domain/Api/ApiVersion.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ApiVersion } from './ApiVersion'
|
||||
|
||||
describe('ApiVersion', () => {
|
||||
it('should create a value object', () => {
|
||||
const valueOrError = ApiVersion.create(ApiVersion.VERSIONS.v20200115)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().value).toEqual('20200115')
|
||||
})
|
||||
|
||||
it('should not create an invalid value object', () => {
|
||||
for (const value of ['', undefined, null, 0, 'SOME_VERSION']) {
|
||||
const valueOrError = ApiVersion.create(value as string)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('should tell if the version is supported for registration', () => {
|
||||
const version = ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue()
|
||||
|
||||
expect(version.isSupportedForRegistration()).toBeTruthy()
|
||||
|
||||
const version2 = ApiVersion.create(ApiVersion.VERSIONS.v20240226).getValue()
|
||||
|
||||
expect(version2.isSupportedForRegistration()).toBeTruthy()
|
||||
|
||||
const version3 = ApiVersion.create(ApiVersion.VERSIONS.v20161215).getValue()
|
||||
|
||||
expect(version3.isSupportedForRegistration()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should tell if the version is supported for recovery sign in', () => {
|
||||
const version = ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue()
|
||||
|
||||
expect(version.isSupportedForRecoverySignIn()).toBeTruthy()
|
||||
|
||||
const version2 = ApiVersion.create(ApiVersion.VERSIONS.v20240226).getValue()
|
||||
|
||||
expect(version2.isSupportedForRecoverySignIn()).toBeTruthy()
|
||||
|
||||
const version3 = ApiVersion.create(ApiVersion.VERSIONS.v20161215).getValue()
|
||||
|
||||
expect(version3.isSupportedForRecoverySignIn()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,37 @@
|
||||
export enum ApiVersion {
|
||||
v20161215 = '20161215',
|
||||
v20190520 = '20190520',
|
||||
v20200115 = '20200115',
|
||||
import { Result, ValueObject } from '@standardnotes/domain-core'
|
||||
|
||||
import { ApiVersionProps } from './ApiVersionProps'
|
||||
|
||||
export class ApiVersion extends ValueObject<ApiVersionProps> {
|
||||
static readonly VERSIONS = {
|
||||
v20161215: '20161215',
|
||||
v20190520: '20190520',
|
||||
v20200115: '20200115',
|
||||
v20240226: '20240226',
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
private constructor(props: ApiVersionProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(version: string): Result<ApiVersion> {
|
||||
const isValidVersion = Object.values(this.VERSIONS).includes(version)
|
||||
if (!isValidVersion) {
|
||||
return Result.fail(`Invalid api version: ${version}`)
|
||||
} else {
|
||||
return Result.ok(new ApiVersion({ value: version }))
|
||||
}
|
||||
}
|
||||
|
||||
isSupportedForRegistration(): boolean {
|
||||
return [ApiVersion.VERSIONS.v20200115, ApiVersion.VERSIONS.v20240226].includes(this.props.value)
|
||||
}
|
||||
|
||||
isSupportedForRecoverySignIn(): boolean {
|
||||
return [ApiVersion.VERSIONS.v20200115, ApiVersion.VERSIONS.v20240226].includes(this.props.value)
|
||||
}
|
||||
}
|
||||
|
||||
3
packages/auth/src/Domain/Api/ApiVersionProps.ts
Normal file
3
packages/auth/src/Domain/Api/ApiVersionProps.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface ApiVersionProps {
|
||||
value: string
|
||||
}
|
||||
@@ -3,6 +3,6 @@ import { KeyParamsData, SessionBody } from '@standardnotes/responses'
|
||||
import { AuthResponse } from './AuthResponse'
|
||||
|
||||
export interface AuthResponse20200115 extends AuthResponse {
|
||||
session: SessionBody
|
||||
key_params: KeyParamsData
|
||||
sessionBody: SessionBody
|
||||
keyParams: KeyParamsData
|
||||
}
|
||||
|
||||
10
packages/auth/src/Domain/Auth/AuthResponseCreationResult.ts
Normal file
10
packages/auth/src/Domain/Auth/AuthResponseCreationResult.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Session } from '../Session/Session'
|
||||
import { AuthResponse20161215 } from './AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from './AuthResponse20200115'
|
||||
|
||||
export interface AuthResponseCreationResult {
|
||||
response?: AuthResponse20200115
|
||||
legacyResponse?: AuthResponse20161215
|
||||
session?: Session
|
||||
cookies?: { accessToken: string; refreshToken: string }
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { Logger } from 'winston'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { User } from '../User/User'
|
||||
import { AuthResponseFactory20161215 } from './AuthResponseFactory20161215'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
|
||||
describe('AuthResponseFactory20161215', () => {
|
||||
let userProjector: ProjectorInterface<User>
|
||||
@@ -32,13 +33,13 @@ describe('AuthResponseFactory20161215', () => {
|
||||
it('should create a 20161215 auth response', async () => {
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20161215',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20161215).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(result.response).toEqual({
|
||||
expect(result.legacyResponse).toEqual({
|
||||
user: { foo: 'bar' },
|
||||
token: 'foobar',
|
||||
})
|
||||
|
||||
@@ -8,10 +8,9 @@ import TYPES from '../../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { AuthResponse20161215 } from './AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from './AuthResponse20200115'
|
||||
import { AuthResponseFactoryInterface } from './AuthResponseFactoryInterface'
|
||||
import { Session } from '../Session/Session'
|
||||
import { AuthResponseCreationResult } from './AuthResponseCreationResult'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
|
||||
@injectable()
|
||||
export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface {
|
||||
@@ -23,11 +22,13 @@ export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface
|
||||
|
||||
async createResponse(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
apiVersion: ApiVersion
|
||||
userAgent: string
|
||||
ephemeralSession: boolean
|
||||
readonlyAccess: boolean
|
||||
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }> {
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<AuthResponseCreationResult> {
|
||||
this.logger.debug(`Creating JWT auth response for user ${dto.user.uuid}`)
|
||||
|
||||
const data: SessionTokenData = {
|
||||
@@ -40,7 +41,7 @@ export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface
|
||||
this.logger.debug(`Created JWT token for user ${dto.user.uuid}: ${token}`)
|
||||
|
||||
return {
|
||||
response: {
|
||||
legacyResponse: {
|
||||
user: this.userProjector.projectSimple(dto.user) as {
|
||||
uuid: string
|
||||
email: string
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Logger } from 'winston'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { User } from '../User/User'
|
||||
import { AuthResponseFactory20190520 } from './AuthResponseFactory20190520'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
|
||||
describe('AuthResponseFactory20190520', () => {
|
||||
let userProjector: ProjectorInterface<User>
|
||||
@@ -31,13 +32,13 @@ describe('AuthResponseFactory20190520', () => {
|
||||
it('should create a 20161215 auth response', async () => {
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20161215',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20161215).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(result.response).toEqual({
|
||||
expect(result.legacyResponse).toEqual({
|
||||
user: { foo: 'bar' },
|
||||
token: 'foobar',
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ import { AuthResponseFactory20200115 } from './AuthResponseFactory20200115'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { Session } from '../Session/Session'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
|
||||
describe('AuthResponseFactory20200115', () => {
|
||||
let sessionService: SessionServiceInterface
|
||||
@@ -51,10 +52,10 @@ describe('AuthResponseFactory20200115', () => {
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.createNewSessionForUser = jest
|
||||
.fn()
|
||||
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, session: {} as jest.Mocked<Session> })
|
||||
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, sessionBody: {} as jest.Mocked<Session> })
|
||||
sessionService.createNewEphemeralSessionForUser = jest
|
||||
.fn()
|
||||
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, session: {} as jest.Mocked<Session> })
|
||||
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, sessionBody: {} as jest.Mocked<Session> })
|
||||
|
||||
keyParamsFactory = {} as jest.Mocked<KeyParamsFactoryInterface>
|
||||
keyParamsFactory.create = jest.fn().mockReturnValue({
|
||||
@@ -83,13 +84,13 @@ describe('AuthResponseFactory20200115', () => {
|
||||
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20161215',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20161215).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(result.response).toEqual({
|
||||
expect(result.legacyResponse).toEqual({
|
||||
user: { foo: 'bar' },
|
||||
token: expect.any(String),
|
||||
})
|
||||
@@ -100,18 +101,18 @@ describe('AuthResponseFactory20200115', () => {
|
||||
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20200115',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(result.response).toEqual({
|
||||
key_params: {
|
||||
keyParams: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
session: {
|
||||
sessionBody: {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
access_expiration: 123,
|
||||
@@ -131,18 +132,18 @@ describe('AuthResponseFactory20200115', () => {
|
||||
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20200115',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(result.response).toEqual({
|
||||
key_params: {
|
||||
keyParams: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
session: {
|
||||
sessionBody: {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
access_expiration: 123,
|
||||
@@ -160,18 +161,18 @@ describe('AuthResponseFactory20200115', () => {
|
||||
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20200115',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
ephemeralSession: true,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(result.response).toEqual({
|
||||
key_params: {
|
||||
keyParams: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
session: {
|
||||
sessionBody: {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
access_expiration: 123,
|
||||
@@ -192,23 +193,23 @@ describe('AuthResponseFactory20200115', () => {
|
||||
...sessionPayload,
|
||||
readonly_access: true,
|
||||
},
|
||||
session: {} as jest.Mocked<Session>,
|
||||
sessionBody: {} as jest.Mocked<Session>,
|
||||
})
|
||||
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20200115',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: true,
|
||||
})
|
||||
|
||||
expect(result.response).toEqual({
|
||||
key_params: {
|
||||
keyParams: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
session: {
|
||||
sessionBody: {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
access_expiration: 123,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
TokenEncoderInterface,
|
||||
} from '@standardnotes/security'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { SessionBody } from '@standardnotes/responses'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@@ -17,9 +16,9 @@ import { User } from '../User/User'
|
||||
import { AuthResponseFactory20190520 } from './AuthResponseFactory20190520'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
|
||||
import { AuthResponse20161215 } from './AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from './AuthResponse20200115'
|
||||
import { Session } from '../Session/Session'
|
||||
import { SessionCreationResult } from '../Session/SessionCreationResult'
|
||||
import { AuthResponseCreationResult } from './AuthResponseCreationResult'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
|
||||
@injectable()
|
||||
export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
@@ -37,11 +36,13 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
|
||||
override async createResponse(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
apiVersion: ApiVersion
|
||||
userAgent: string
|
||||
ephemeralSession: boolean
|
||||
readonlyAccess: boolean
|
||||
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }> {
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<AuthResponseCreationResult> {
|
||||
if (!dto.user.supportsSessions()) {
|
||||
this.logger.debug(`User ${dto.user.uuid} does not support sessions. Falling back to JWT auth response`)
|
||||
|
||||
@@ -50,29 +51,31 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
|
||||
const sessionCreationResult = await this.createSession(dto)
|
||||
|
||||
this.logger.debug(
|
||||
'Created session payload for user %s: %O',
|
||||
dto.user.uuid,
|
||||
sessionCreationResult.sessionHttpRepresentation,
|
||||
)
|
||||
this.logger.debug('Created session payload for user', {
|
||||
userId: dto.user.uuid,
|
||||
session: sessionCreationResult,
|
||||
})
|
||||
|
||||
return {
|
||||
response: {
|
||||
session: sessionCreationResult.sessionHttpRepresentation,
|
||||
key_params: this.keyParamsFactory.create(dto.user, true),
|
||||
sessionBody: sessionCreationResult.sessionHttpRepresentation,
|
||||
keyParams: this.keyParamsFactory.create(dto.user, true),
|
||||
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
|
||||
},
|
||||
session: sessionCreationResult.session,
|
||||
cookies: sessionCreationResult.sessionCookieRepresentation,
|
||||
}
|
||||
}
|
||||
|
||||
private async createSession(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
apiVersion: ApiVersion
|
||||
userAgent: string
|
||||
ephemeralSession: boolean
|
||||
readonlyAccess: boolean
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<SessionCreationResult> {
|
||||
if (dto.ephemeralSession) {
|
||||
return this.sessionService.createNewEphemeralSessionForUser(dto)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Session } from '../Session/Session'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
import { User } from '../User/User'
|
||||
import { AuthResponse20161215 } from './AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from './AuthResponse20200115'
|
||||
import { AuthResponseCreationResult } from './AuthResponseCreationResult'
|
||||
|
||||
export interface AuthResponseFactoryInterface {
|
||||
createResponse(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
apiVersion: ApiVersion
|
||||
userAgent: string
|
||||
ephemeralSession: boolean
|
||||
readonlyAccess: boolean
|
||||
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }>
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<AuthResponseCreationResult>
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AuthResponseFactory20161215 } from './AuthResponseFactory20161215'
|
||||
import { AuthResponseFactory20190520 } from './AuthResponseFactory20190520'
|
||||
import { AuthResponseFactory20200115 } from './AuthResponseFactory20200115'
|
||||
import { AuthResponseFactoryResolver } from './AuthResponseFactoryResolver'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
|
||||
describe('AuthResponseFactoryResolver', () => {
|
||||
let authResponseFactory20161215: AuthResponseFactory20161215
|
||||
@@ -30,18 +31,26 @@ describe('AuthResponseFactoryResolver', () => {
|
||||
})
|
||||
|
||||
it('should resolve 2016 response factory', () => {
|
||||
expect(createResolver().resolveAuthResponseFactoryVersion('20161215')).toEqual(authResponseFactory20161215)
|
||||
expect(
|
||||
createResolver().resolveAuthResponseFactoryVersion(ApiVersion.create(ApiVersion.VERSIONS.v20161215).getValue()),
|
||||
).toEqual(authResponseFactory20161215)
|
||||
})
|
||||
|
||||
it('should resolve 2019 response factory', () => {
|
||||
expect(createResolver().resolveAuthResponseFactoryVersion('20190520')).toEqual(authResponseFactory20190520)
|
||||
expect(
|
||||
createResolver().resolveAuthResponseFactoryVersion(ApiVersion.create(ApiVersion.VERSIONS.v20190520).getValue()),
|
||||
).toEqual(authResponseFactory20190520)
|
||||
})
|
||||
|
||||
it('should resolve 2020 response factory', () => {
|
||||
expect(createResolver().resolveAuthResponseFactoryVersion('20200115')).toEqual(authResponseFactory20200115)
|
||||
expect(
|
||||
createResolver().resolveAuthResponseFactoryVersion(ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue()),
|
||||
).toEqual(authResponseFactory20200115)
|
||||
})
|
||||
|
||||
it('should resolve 2016 response factory as default', () => {
|
||||
expect(createResolver().resolveAuthResponseFactoryVersion('')).toEqual(authResponseFactory20161215)
|
||||
it('should resolve 2024 response factory', () => {
|
||||
expect(
|
||||
createResolver().resolveAuthResponseFactoryVersion(ApiVersion.create(ApiVersion.VERSIONS.v20240226).getValue()),
|
||||
).toEqual(authResponseFactory20200115)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,13 +17,14 @@ export class AuthResponseFactoryResolver implements AuthResponseFactoryResolverI
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
resolveAuthResponseFactoryVersion(apiVersion: string): AuthResponseFactoryInterface {
|
||||
this.logger.debug(`Resolving auth response factory for api version: ${apiVersion}`)
|
||||
resolveAuthResponseFactoryVersion(apiVersion: ApiVersion): AuthResponseFactoryInterface {
|
||||
this.logger.debug(`Resolving auth response factory for api version: ${apiVersion.value}`)
|
||||
|
||||
switch (apiVersion) {
|
||||
case ApiVersion.v20190520:
|
||||
switch (apiVersion.value) {
|
||||
case ApiVersion.VERSIONS.v20190520:
|
||||
return this.authResponseFactory20190520
|
||||
case ApiVersion.v20200115:
|
||||
case ApiVersion.VERSIONS.v20200115:
|
||||
case ApiVersion.VERSIONS.v20240226:
|
||||
return this.authResponseFactory20200115
|
||||
default:
|
||||
return this.authResponseFactory20161215
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
import { AuthResponseFactoryInterface } from './AuthResponseFactoryInterface'
|
||||
|
||||
export interface AuthResponseFactoryResolverInterface {
|
||||
resolveAuthResponseFactoryVersion(apiVersion: string): AuthResponseFactoryInterface
|
||||
resolveAuthResponseFactoryVersion(apiVersion: ApiVersion): AuthResponseFactoryInterface
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ export type AuthenticationMethod = {
|
||||
user: User | null
|
||||
claims?: Record<string, unknown>
|
||||
session?: Session
|
||||
givenTokensWereInCooldown?: boolean
|
||||
revokedSession?: RevokedSession
|
||||
}
|
||||
|
||||
@@ -10,19 +10,29 @@ import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
import { AuthenticationMethodResolver } from './AuthenticationMethodResolver'
|
||||
import { Logger } from 'winston'
|
||||
import { GetSessionFromToken } from '../UseCase/GetSessionFromToken/GetSessionFromToken'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AuthenticationMethodResolver', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let sessionService: SessionServiceInterface
|
||||
let sessionTokenDecoder: TokenDecoderInterface<SessionTokenData>
|
||||
let fallbackTokenDecoder: TokenDecoderInterface<SessionTokenData>
|
||||
let getSessionFromToken: GetSessionFromToken
|
||||
let user: User
|
||||
let session: Session
|
||||
let revokedSession: RevokedSession
|
||||
let logger: Logger
|
||||
|
||||
const createResolver = () =>
|
||||
new AuthenticationMethodResolver(userRepository, sessionService, sessionTokenDecoder, fallbackTokenDecoder, logger)
|
||||
new AuthenticationMethodResolver(
|
||||
userRepository,
|
||||
sessionService,
|
||||
sessionTokenDecoder,
|
||||
fallbackTokenDecoder,
|
||||
getSessionFromToken,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
@@ -41,10 +51,12 @@ describe('AuthenticationMethodResolver', () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session: undefined, isEphemeral: false })
|
||||
sessionService.getRevokedSessionFromToken = jest.fn()
|
||||
sessionService.markRevokedSessionAsReceived = jest.fn().mockReturnValue(revokedSession)
|
||||
|
||||
getSessionFromToken = {} as jest.Mocked<GetSessionFromToken>
|
||||
getSessionFromToken.execute = jest.fn().mockReturnValue(Result.fail('No session found.'))
|
||||
|
||||
sessionTokenDecoder = {} as jest.Mocked<TokenDecoderInterface<SessionTokenData>>
|
||||
sessionTokenDecoder.decodeToken = jest.fn()
|
||||
|
||||
@@ -55,7 +67,12 @@ describe('AuthenticationMethodResolver', () => {
|
||||
it('should resolve jwt authentication method', async () => {
|
||||
sessionTokenDecoder.decodeToken = jest.fn().mockReturnValue({ user_uuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
expect(await createResolver().resolve('test')).toEqual({
|
||||
expect(
|
||||
await createResolver().resolve({
|
||||
authTokenFromHeaders: 'test',
|
||||
requestMetadata: { url: '/foobar', method: 'GET' },
|
||||
}),
|
||||
).toEqual({
|
||||
claims: {
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
@@ -67,31 +84,56 @@ describe('AuthenticationMethodResolver', () => {
|
||||
it('should not resolve jwt authentication method with invalid user uuid', async () => {
|
||||
sessionTokenDecoder.decodeToken = jest.fn().mockReturnValue({ user_uuid: 'invalid' })
|
||||
|
||||
expect(await createResolver().resolve('test')).toBeUndefined
|
||||
expect(
|
||||
await createResolver().resolve({
|
||||
authTokenFromHeaders: 'test',
|
||||
requestMetadata: { url: '/foobar', method: 'GET' },
|
||||
}),
|
||||
).toBeUndefined
|
||||
})
|
||||
|
||||
it('should resolve session authentication method', async () => {
|
||||
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
|
||||
getSessionFromToken.execute = jest
|
||||
.fn()
|
||||
.mockReturnValue(Result.ok({ session, isEphemeral: false, givenTokensWereInCooldown: false }))
|
||||
|
||||
expect(await createResolver().resolve('test')).toEqual({
|
||||
expect(
|
||||
await createResolver().resolve({
|
||||
authTokenFromHeaders: 'test',
|
||||
requestMetadata: { url: '/foobar', method: 'GET' },
|
||||
}),
|
||||
).toEqual({
|
||||
session,
|
||||
type: 'session_token',
|
||||
user,
|
||||
givenTokensWereInCooldown: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not resolve session authentication method with invalid user uuid on session', async () => {
|
||||
sessionService.getSessionFromToken = jest
|
||||
getSessionFromToken.execute = jest
|
||||
.fn()
|
||||
.mockReturnValue({ session: { userUuid: 'invalid' }, isEphemeral: false })
|
||||
.mockReturnValue(
|
||||
Result.ok({ session: { userUuid: 'invalid' }, isEphemeral: false, givenTokensWereInCooldown: false }),
|
||||
)
|
||||
|
||||
expect(await createResolver().resolve('test')).toBeUndefined
|
||||
expect(
|
||||
await createResolver().resolve({
|
||||
authTokenFromHeaders: 'test',
|
||||
requestMetadata: { url: '/foobar', method: 'GET' },
|
||||
}),
|
||||
).toBeUndefined
|
||||
})
|
||||
|
||||
it('should resolve archvied session authentication method', async () => {
|
||||
sessionService.getRevokedSessionFromToken = jest.fn().mockReturnValue(revokedSession)
|
||||
|
||||
expect(await createResolver().resolve('test')).toEqual({
|
||||
expect(
|
||||
await createResolver().resolve({
|
||||
authTokenFromHeaders: 'test',
|
||||
requestMetadata: { url: '/foobar', method: 'GET' },
|
||||
}),
|
||||
).toEqual({
|
||||
revokedSession,
|
||||
type: 'revoked',
|
||||
user: null,
|
||||
@@ -101,6 +143,11 @@ describe('AuthenticationMethodResolver', () => {
|
||||
})
|
||||
|
||||
it('should indicated that authentication method cannot be resolved', async () => {
|
||||
expect(await createResolver().resolve('test')).toBeUndefined
|
||||
expect(
|
||||
await createResolver().resolve({
|
||||
authTokenFromHeaders: 'test',
|
||||
requestMetadata: { url: '/foobar', method: 'GET' },
|
||||
}),
|
||||
).toBeUndefined
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
import { SessionTokenData, TokenDecoderInterface } from '@standardnotes/security'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { AuthenticationMethod } from './AuthenticationMethod'
|
||||
import { AuthenticationMethodResolverInterface } from './AuthenticationMethodResolverInterface'
|
||||
import { Logger } from 'winston'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { GetSessionFromToken } from '../UseCase/GetSessionFromToken/GetSessionFromToken'
|
||||
|
||||
@injectable()
|
||||
export class AuthenticationMethodResolver implements AuthenticationMethodResolverInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface,
|
||||
@inject(TYPES.Auth_SessionTokenDecoder) private sessionTokenDecoder: TokenDecoderInterface<SessionTokenData>,
|
||||
@inject(TYPES.Auth_FallbackSessionTokenDecoder)
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private sessionService: SessionServiceInterface,
|
||||
private sessionTokenDecoder: TokenDecoderInterface<SessionTokenData>,
|
||||
private fallbackSessionTokenDecoder: TokenDecoderInterface<SessionTokenData>,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
private getSessionFromToken: GetSessionFromToken,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async resolve(token: string): Promise<AuthenticationMethod | undefined> {
|
||||
let decodedToken: SessionTokenData | undefined = this.sessionTokenDecoder.decodeToken(token)
|
||||
async resolve(dto: {
|
||||
authTokenFromHeaders: string
|
||||
authCookies?: Map<string, string[]>
|
||||
requestMetadata: {
|
||||
url: string
|
||||
method: string
|
||||
snjs?: string
|
||||
application?: string
|
||||
userAgent?: string
|
||||
secChUa?: string
|
||||
}
|
||||
}): Promise<AuthenticationMethod | undefined> {
|
||||
let decodedToken: SessionTokenData | undefined = this.sessionTokenDecoder.decodeToken(dto.authTokenFromHeaders)
|
||||
if (decodedToken === undefined) {
|
||||
this.logger.debug('Could not decode token with primary decoder, trying fallback decoder.')
|
||||
|
||||
decodedToken = this.fallbackSessionTokenDecoder.decodeToken(token)
|
||||
decodedToken = this.fallbackSessionTokenDecoder.decodeToken(dto.authTokenFromHeaders)
|
||||
}
|
||||
|
||||
if (decodedToken) {
|
||||
@@ -47,8 +56,10 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
|
||||
}
|
||||
}
|
||||
|
||||
const { session } = await this.sessionService.getSessionFromToken(token)
|
||||
if (session) {
|
||||
const resultOrError = await this.getSessionFromToken.execute(dto)
|
||||
if (!resultOrError.isFailed()) {
|
||||
const { session, givenTokensWereInCooldown } = resultOrError.getValue()
|
||||
|
||||
this.logger.debug('Token decoded successfully. Session found.')
|
||||
|
||||
const userUuidOrError = Uuid.create(session.userUuid)
|
||||
@@ -61,10 +72,11 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
|
||||
type: 'session_token',
|
||||
user: await this.userRepository.findOneByUuid(userUuid),
|
||||
session: session,
|
||||
givenTokensWereInCooldown: givenTokensWereInCooldown,
|
||||
}
|
||||
}
|
||||
|
||||
const revokedSession = await this.sessionService.getRevokedSessionFromToken(token)
|
||||
const revokedSession = await this.sessionService.getRevokedSessionFromToken(dto.authTokenFromHeaders)
|
||||
if (revokedSession) {
|
||||
this.logger.debug('Token decoded successfully. Revoked session found.')
|
||||
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { AuthenticationMethod } from './AuthenticationMethod'
|
||||
|
||||
export interface AuthenticationMethodResolverInterface {
|
||||
resolve(token: string): Promise<AuthenticationMethod | undefined>
|
||||
resolve(dto: {
|
||||
authTokenFromHeaders: string
|
||||
authCookies?: Map<string, string[]>
|
||||
requestMetadata: {
|
||||
url: string
|
||||
method: string
|
||||
snjs?: string
|
||||
application?: string
|
||||
userAgent?: string
|
||||
secChUa?: string
|
||||
}
|
||||
}): Promise<AuthenticationMethod | undefined>
|
||||
}
|
||||
|
||||
28
packages/auth/src/Domain/Auth/Cookies/CookieFactory.ts
Normal file
28
packages/auth/src/Domain/Auth/Cookies/CookieFactory.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { CookieFactoryInterface } from './CookieFactoryInterface'
|
||||
|
||||
export class CookieFactory implements CookieFactoryInterface {
|
||||
constructor(
|
||||
private sameSite: 'None' | 'Lax' | 'Strict',
|
||||
private domain: string,
|
||||
private secure: boolean,
|
||||
private partitioned: boolean,
|
||||
) {}
|
||||
|
||||
createCookieHeaderValue(dto: {
|
||||
sessionUuid: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
refreshTokenExpiration: Date
|
||||
}): string[] {
|
||||
return [
|
||||
`access_token_${dto.sessionUuid}=${dto.accessToken}; HttpOnly;${this.secure ? 'Secure; ' : ' '}Path=/;${
|
||||
this.partitioned ? 'Partitioned; ' : ' '
|
||||
}SameSite=${this.sameSite}; Domain=${this.domain}; Expires=${dto.refreshTokenExpiration.toUTCString()};`,
|
||||
`refresh_token_${dto.sessionUuid}=${dto.refreshToken}; HttpOnly;${
|
||||
this.secure ? 'Secure; ' : ' '
|
||||
}Path=/v1/sessions/refresh;${this.partitioned ? 'Partitioned; ' : ' '}SameSite=${this.sameSite}; Domain=${
|
||||
this.domain
|
||||
}; Expires=${dto.refreshTokenExpiration.toUTCString()};`,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface CookieFactoryInterface {
|
||||
createCookieHeaderValue(dto: {
|
||||
sessionUuid: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
refreshTokenExpiration: Date
|
||||
}): string[]
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export const html = (userEmail: string, offlineSubscriptionDashboardUrl: string) => `<div class="sn-component">
|
||||
import { safeHtml } from '@standardnotes/common'
|
||||
|
||||
export const html = (userEmail: string, offlineSubscriptionDashboardUrl: string) => safeHtml`<div class="sn-component">
|
||||
<div class="sk-panel static">
|
||||
<div class="sk-panel-content">
|
||||
<div class="sk-panel-section">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const html = (inviterIdentifier: string, inviteUuid: string) => `<p>Hello,</p>
|
||||
import { safeHtml } from '@standardnotes/common'
|
||||
|
||||
export const html = (inviterIdentifier: string, inviteUuid: string) => safeHtml`<p>Hello,</p>
|
||||
<p>You've been invited to join a Standard Notes premium subscription at no cost. ${inviterIdentifier} has invited you to share the benefits of their subscription plan.</p>
|
||||
<p>
|
||||
<a href='https://app.standardnotes.com/?accept-subscription-invite=${inviteUuid}'>Accept Invite</a>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const html = (newEmail: string) => `
|
||||
import { safeHtml } from '@standardnotes/common'
|
||||
|
||||
export const html = (newEmail: string) => safeHtml`
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>We are writing to inform you that your request to update your email address has been successfully processed. The email address associated with your Standard Notes account has now been changed to the following:</p>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const html = () => `
|
||||
import { safeHtml } from '@standardnotes/common'
|
||||
|
||||
export const html = () => safeHtml`
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>You've been invited to join a shared vault. This shared workspace will help you collaborate and securely manage notes and files.</p>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const html = (email: string, device: string, browser: string, timeAndDate: string) => `
|
||||
import { safeHtml } from '@standardnotes/common'
|
||||
|
||||
export const html = (email: string, device: string, browser: string, timeAndDate: string) => safeHtml`
|
||||
<div>
|
||||
<p>Hello,</p>
|
||||
<p>We've detected a new sign-in to your account ${email}</p>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
SessionRefreshedEvent,
|
||||
AccountDeletionVerificationRequestedEvent,
|
||||
FileQuotaRecalculationRequestedEvent,
|
||||
SubscriptionStateRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
@@ -34,6 +35,21 @@ import { KeyParamsData } from '@standardnotes/responses'
|
||||
@injectable()
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
|
||||
createSubscriptionStateRequestedEvent(dto: { userEmail: string }): SubscriptionStateRequestedEvent {
|
||||
return {
|
||||
type: 'SUBSCRIPTION_STATE_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userEmail,
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent {
|
||||
return {
|
||||
type: 'FILE_QUOTA_RECALCULATION_REQUESTED',
|
||||
@@ -289,12 +305,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
}
|
||||
}
|
||||
|
||||
createEmailBackupRequestedEvent(
|
||||
userUuid: string,
|
||||
muteEmailsSettingUuid: string,
|
||||
userHasEmailsMuted: boolean,
|
||||
keyParams: KeyParamsData,
|
||||
): EmailBackupRequestedEvent {
|
||||
createEmailBackupRequestedEvent(userUuid: string, keyParams: KeyParamsData): EmailBackupRequestedEvent {
|
||||
return {
|
||||
type: 'EMAIL_BACKUP_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
@@ -307,8 +318,6 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
},
|
||||
payload: {
|
||||
userUuid,
|
||||
userHasEmailsMuted,
|
||||
muteEmailsSettingUuid,
|
||||
keyParams,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@ import {
|
||||
SessionRefreshedEvent,
|
||||
AccountDeletionVerificationRequestedEvent,
|
||||
FileQuotaRecalculationRequestedEvent,
|
||||
SubscriptionStateRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createSubscriptionStateRequestedEvent(dto: { userEmail: string }): SubscriptionStateRequestedEvent
|
||||
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent
|
||||
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: JSONString }): WebSocketMessageRequestedEvent
|
||||
createEmailRequestedEvent(dto: {
|
||||
@@ -41,12 +43,7 @@ export interface DomainEventFactoryInterface {
|
||||
email: string
|
||||
protocolVersion: ProtocolVersion
|
||||
}): UserRegisteredEvent
|
||||
createEmailBackupRequestedEvent(
|
||||
userUuid: string,
|
||||
muteEmailsSettingUuid: string,
|
||||
userHasEmailsMuted: boolean,
|
||||
keyParams: KeyParamsData,
|
||||
): EmailBackupRequestedEvent
|
||||
createEmailBackupRequestedEvent(userUuid: string, keyParams: KeyParamsData): EmailBackupRequestedEvent
|
||||
createAccountDeletionRequestedEvent(dto: {
|
||||
userUuid: string
|
||||
email: string
|
||||
|
||||
@@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -12,9 +13,20 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||
if (!event.payload.subscriptionId) {
|
||||
this.logger.error('Subscription ID is missing', {
|
||||
codeTag: 'SubscriptionCancelledEventHandler.handle',
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
userId: event.payload.userEmail,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.payload.offline) {
|
||||
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
|
||||
|
||||
|
||||
@@ -22,6 +22,16 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
||||
if (!event.payload.subscriptionId) {
|
||||
this.logger.error('Subscription ID is missing', {
|
||||
codeTag: 'SubscriptionExpiredEventHandler.handle',
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
userId: event.payload.userEmail,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.payload.offline) {
|
||||
await this.updateOfflineSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)
|
||||
|
||||
|
||||
@@ -25,6 +25,16 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
||||
if (!event.payload.subscriptionId) {
|
||||
this.logger.error('Subscription ID is missing', {
|
||||
codeTag: 'SubscriptionPurchasedEventHandler.handle',
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
userId: event.payload.userEmail,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.payload.offline) {
|
||||
const offlineUserSubscription = await this.createOfflineSubscription(
|
||||
event.payload.subscriptionId,
|
||||
|
||||
@@ -22,6 +22,16 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionReassignedEvent): Promise<void> {
|
||||
if (!event.payload.subscriptionId) {
|
||||
this.logger.error('Subscription ID is missing', {
|
||||
codeTag: 'SubscriptionReassignedEventHandler.handle',
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
userId: event.payload.userEmail,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const usernameOrError = Username.create(event.payload.userEmail)
|
||||
if (usernameOrError.isFailed()) {
|
||||
return
|
||||
|
||||
@@ -22,6 +22,16 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
||||
if (!event.payload.subscriptionId) {
|
||||
this.logger.error('Subscription ID is missing', {
|
||||
codeTag: 'SubscriptionRefundedEventHandler.handle',
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
userId: event.payload.userEmail,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.payload.offline) {
|
||||
await this.updateOfflineSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)
|
||||
|
||||
|
||||
@@ -23,6 +23,16 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
||||
if (!event.payload.subscriptionId) {
|
||||
this.logger.error('Subscription ID is missing', {
|
||||
codeTag: 'SubscriptionRenewedEventHandler.handle',
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
userId: event.payload.userEmail,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.payload.offline) {
|
||||
const offlineUserSubscription = await this.offlineUserSubscriptionRepository.findOneBySubscriptionId(
|
||||
event.payload.subscriptionId,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { DomainEventHandlerInterface, SubscriptionStateFetchedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
|
||||
export class SubscriptionStateFetchedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionStateFetchedEvent): Promise<void> {
|
||||
if (!event.payload.subscriptionId) {
|
||||
this.logger.error('Subscription ID is missing', {
|
||||
codeTag: 'SubscriptionStateFetchedEventHandler.handle',
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
userId: event.payload.userEmail,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.info('Subscription state update fetched', {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
if (event.payload.offline) {
|
||||
this.logger.info('Updating offline subscription', {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
const subscription = await this.offlineUserSubscriptionRepository.findOneByEmailAndSubscriptionId(
|
||||
event.payload.userEmail,
|
||||
0,
|
||||
)
|
||||
if (!subscription) {
|
||||
this.logger.error('Offline subscription not found', {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
subscription.planName = event.payload.subscriptionName
|
||||
subscription.email = event.payload.userEmail
|
||||
subscription.endsAt = event.payload.subscriptionExpiresAt
|
||||
subscription.cancelled = event.payload.canceled
|
||||
if (subscription.subscriptionId !== event.payload.subscriptionId) {
|
||||
this.logger.warn('Subscription IDs do not match', {
|
||||
previousSubscriptionId: subscription.subscriptionId,
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
}
|
||||
subscription.subscriptionId = event.payload.subscriptionId
|
||||
|
||||
await this.offlineUserSubscriptionRepository.save(subscription)
|
||||
|
||||
this.logger.info('Offline subscription updated', {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const usernameOrError = Username.create(event.payload.userEmail)
|
||||
if (usernameOrError.isFailed()) {
|
||||
this.logger.warn(`Could not update subscription: ${usernameOrError.getError()}`, {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
const username = usernameOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUsernameOrEmail(username)
|
||||
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with email: ${username.value}`, {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.info('Updating subscription', {
|
||||
userId: user.uuid,
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
const subscription = await this.userSubscriptionRepository.findOneByUserUuidAndSubscriptionId(user.uuid, 0)
|
||||
if (!subscription) {
|
||||
this.logger.error('Subscription not found', {
|
||||
userId: user.uuid,
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
subscription.planName = event.payload.subscriptionName
|
||||
subscription.endsAt = event.payload.subscriptionExpiresAt
|
||||
subscription.cancelled = event.payload.canceled
|
||||
if (subscription.subscriptionId !== event.payload.subscriptionId) {
|
||||
this.logger.warn('Subscription IDs do not match', {
|
||||
previousSubscriptionId: subscription.subscriptionId,
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
}
|
||||
subscription.subscriptionId = event.payload.subscriptionId
|
||||
|
||||
await this.userSubscriptionRepository.save(subscription)
|
||||
|
||||
this.logger.info('Subscription updated to current state', {
|
||||
userId: user.uuid,
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user