mirror of
https://github.com/standardnotes/server
synced 2026-01-23 23:01:09 -05:00
Compare commits
15 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
162a63ae2b | ||
|
|
0d82819cba | ||
|
|
578ce0e74e | ||
|
|
532be7c358 | ||
|
|
d406272f07 | ||
|
|
9de3352885 | ||
|
|
8575d20f7b | ||
|
|
102d4b1e8a | ||
|
|
1a57c247b2 | ||
|
|
dbb0e4a974 | ||
|
|
5c02435ee4 | ||
|
|
0a1e555b13 | ||
|
|
be668d7d7a | ||
|
|
87e50ec941 | ||
|
|
6d7ca1b926 |
3
.github/ci.env
vendored
3
.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
|
||||
|
||||
6
.github/workflows/e2e-home-server.yml
vendored
6
.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'
|
||||
@@ -94,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'
|
||||
|
||||
44
.github/workflows/publish.yml
vendored
44
.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
|
||||
@@ -98,32 +98,30 @@ jobs:
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
# e2e-base:
|
||||
# needs: build
|
||||
# name: E2E Base Suite
|
||||
# uses: standardnotes/server/.github/workflows/common-e2e.yml@main
|
||||
# with:
|
||||
# snjs_image_tag: 'latest'
|
||||
# suite: 'base'
|
||||
e2e-base:
|
||||
needs: build
|
||||
name: E2E Base Suite
|
||||
uses: standardnotes/server/.github/workflows/common-e2e.yml@main
|
||||
with:
|
||||
snjs_image_tag: 'latest'
|
||||
suite: 'base'
|
||||
|
||||
# e2e-vaults:
|
||||
# needs: build
|
||||
# name: E2E Vaults Suite
|
||||
# uses: standardnotes/server/.github/workflows/common-e2e.yml@main
|
||||
# with:
|
||||
# snjs_image_tag: 'latest'
|
||||
# suite: 'vaults'
|
||||
e2e-vaults:
|
||||
needs: build
|
||||
name: E2E Vaults Suite
|
||||
uses: standardnotes/server/.github/workflows/common-e2e.yml@main
|
||||
with:
|
||||
snjs_image_tag: 'latest'
|
||||
suite: 'vaults'
|
||||
|
||||
publish-self-hosting:
|
||||
# needs: [ test, lint, e2e-base, e2e-vaults ]
|
||||
needs: [ test, lint ]
|
||||
needs: [ test, lint, e2e-base, e2e-vaults ]
|
||||
name: Publish Self Hosting Docker Image
|
||||
uses: standardnotes/server/.github/workflows/common-self-hosting.yml@main
|
||||
secrets: inherit
|
||||
|
||||
publish-services:
|
||||
# needs: [ test, lint, e2e-base, e2e-vaults ]
|
||||
needs: [ test, lint ]
|
||||
needs: [ test, lint, e2e-base, e2e-vaults ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -136,7 +134,7 @@ jobs:
|
||||
|
||||
- name: Cache build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/**/dist
|
||||
@@ -156,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,14 @@
|
||||
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
|
||||
|
||||
@@ -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.16",
|
||||
"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,16 @@
|
||||
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
|
||||
|
||||
@@ -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.1",
|
||||
"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,23 +3,15 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.178.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.178.2...@standardnotes/auth-server@1.178.3) (2024-03-18)
|
||||
## [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
|
||||
|
||||
* **auth:** allow registration on new api versions - fixes [#1046](https://github.com/standardnotes/server/issues/1046) ([#1048](https://github.com/standardnotes/server/issues/1048)) ([f939caf](https://github.com/standardnotes/server/commit/f939caf2d9a781d42989ad6e92a5c7150ff48e19))
|
||||
|
||||
## [1.178.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.178.1...@standardnotes/auth-server@1.178.2) (2024-03-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow handling of new api version ([9d49764](https://github.com/standardnotes/server/commit/9d49764b841e73655e19523eddf10498addc9fb4))
|
||||
|
||||
## [1.178.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.178.0...@standardnotes/auth-server@1.178.1) (2024-02-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow expired offline subscriptions to receive dashboard emails ([#1041](https://github.com/standardnotes/server/issues/1041)) ([4fe8e9a](https://github.com/standardnotes/server/commit/4fe8e9a79f652f3e39608d6683cb17cc08bb8717))
|
||||
* 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)
|
||||
|
||||
|
||||
@@ -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" ]
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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.178.3",
|
||||
"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'
|
||||
@@ -286,6 +286,19 @@ import { FixStorageQuotaForUser } from '../Domain/UseCase/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') {}
|
||||
@@ -330,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)
|
||||
@@ -597,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)
|
||||
@@ -633,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
|
||||
@@ -652,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
|
||||
@@ -679,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)
|
||||
@@ -691,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
|
||||
@@ -740,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)
|
||||
@@ -780,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)
|
||||
@@ -819,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)
|
||||
@@ -953,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),
|
||||
),
|
||||
)
|
||||
@@ -997,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(
|
||||
@@ -1017,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(
|
||||
@@ -1029,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(
|
||||
@@ -1130,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
|
||||
@@ -1262,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),
|
||||
@@ -1337,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
|
||||
@@ -1664,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),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1738,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),
|
||||
),
|
||||
)
|
||||
@@ -1745,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
|
||||
@@ -1772,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),
|
||||
),
|
||||
)
|
||||
@@ -1799,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'),
|
||||
@@ -207,6 +218,7 @@ const TYPES = {
|
||||
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'),
|
||||
@@ -259,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,6 +1,37 @@
|
||||
export enum ApiVersion {
|
||||
v20161215 = '20161215',
|
||||
v20190520 = '20190520',
|
||||
v20200115 = '20200115',
|
||||
v20240226 = '20240226',
|
||||
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,14 +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.v20240226:
|
||||
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>
|
||||
|
||||
@@ -305,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(),
|
||||
@@ -323,8 +318,6 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
},
|
||||
payload: {
|
||||
userUuid,
|
||||
userHasEmailsMuted,
|
||||
muteEmailsSettingUuid,
|
||||
keyParams,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -43,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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface CaptchaServerInterface {
|
||||
verify(hvmToken: string): Promise<boolean>
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { EphemeralSession } from './EphemeralSession'
|
||||
|
||||
export interface EphemeralSessionRepositoryInterface {
|
||||
findOneByUuid(uuid: string): Promise<EphemeralSession | null>
|
||||
findOneByPrivateIdentifier(privateIdentifier: string): Promise<EphemeralSession | null>
|
||||
findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<EphemeralSession | null>
|
||||
findAllByUserUuid(userUuid: string): Promise<Array<EphemeralSession>>
|
||||
deleteOne(uuid: string, userUuid: string): Promise<void>
|
||||
|
||||
@@ -6,6 +6,16 @@ export class RevokedSession {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
declare uuid: string
|
||||
|
||||
@Column({
|
||||
name: 'private_identifier',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
type: 'varchar',
|
||||
comment: 'Used to identify a session without exposing the UUID in client-side cookies.',
|
||||
})
|
||||
@Index('index_revoked_sessions_on_private_identifier')
|
||||
declare privateIdentifier: string | null
|
||||
|
||||
@Column({
|
||||
name: 'user_uuid',
|
||||
length: 36,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { RevokedSession } from './RevokedSession'
|
||||
|
||||
export interface RevokedSessionRepositoryInterface {
|
||||
findOneByUuid(uuid: string): Promise<RevokedSession | null>
|
||||
findOneByPrivateIdentifier(privateIdentifier: string): Promise<RevokedSession | null>
|
||||
findAllByUserUuid(userUuid: string): Promise<Array<RevokedSession>>
|
||||
insert(revokedSession: RevokedSession): Promise<void>
|
||||
update(revokedSession: RevokedSession): Promise<void>
|
||||
|
||||
@@ -13,6 +13,16 @@ export class Session {
|
||||
@Index('index_sessions_on_user_uuid')
|
||||
declare userUuid: string
|
||||
|
||||
@Column({
|
||||
name: 'private_identifier',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
type: 'varchar',
|
||||
comment: 'Used to identify a session without exposing the UUID in client-side cookies.',
|
||||
})
|
||||
@Index('index_sessions_on_private_identifier')
|
||||
declare privateIdentifier: string | null
|
||||
|
||||
@Column({
|
||||
name: 'hashed_access_token',
|
||||
length: 255,
|
||||
@@ -75,4 +85,28 @@ export class Session {
|
||||
default: 0,
|
||||
})
|
||||
declare readonlyAccess: boolean
|
||||
|
||||
@Column({
|
||||
name: 'version',
|
||||
type: 'smallint',
|
||||
nullable: true,
|
||||
default: 1,
|
||||
})
|
||||
declare version: number | null
|
||||
|
||||
@Column({
|
||||
name: 'application',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
})
|
||||
declare application: string | null
|
||||
|
||||
@Column({
|
||||
name: 'snjs',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
})
|
||||
declare snjs: string | null
|
||||
}
|
||||
|
||||
12
packages/auth/src/Domain/Session/SessionCreationResult.ts
Normal file
12
packages/auth/src/Domain/Session/SessionCreationResult.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SessionBody } from '@standardnotes/responses'
|
||||
|
||||
import { Session } from './Session'
|
||||
|
||||
export interface SessionCreationResult {
|
||||
sessionHttpRepresentation: SessionBody
|
||||
sessionCookieRepresentation: {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
session: Session
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Session } from './Session'
|
||||
|
||||
export interface SessionRepositoryInterface {
|
||||
findOneByUuid(uuid: string): Promise<Session | null>
|
||||
findOneByPrivateIdentifier(privateIdentifier: string): Promise<Session | null>
|
||||
findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Session | null>
|
||||
findAllByRefreshExpirationAndUserUuid(userUuid: string): Promise<Array<Session>>
|
||||
findAllByUserUuid(userUuid: string): Promise<Array<Session>>
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('SessionService', () => {
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
const readonlyUsers = ['demo@standardnotes.com']
|
||||
|
||||
const createService = () =>
|
||||
const createService = (forceLegacySessions = false) =>
|
||||
new SessionService(
|
||||
sessionRepository,
|
||||
ephemeralSessionRepository,
|
||||
@@ -51,6 +51,7 @@ describe('SessionService', () => {
|
||||
userSubscriptionRepository,
|
||||
readonlyUsers,
|
||||
getSetting,
|
||||
forceLegacySessions,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -58,16 +59,18 @@ describe('SessionService', () => {
|
||||
existingSession.uuid = '2e1e43'
|
||||
existingSession.userUuid = '1-2-3'
|
||||
existingSession.userAgent = 'Chrome'
|
||||
existingSession.apiVersion = ApiVersion.v20200115
|
||||
existingSession.apiVersion = ApiVersion.VERSIONS.v20200115
|
||||
existingSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
existingSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
existingSession.readonlyAccess = false
|
||||
existingSession.version = SessionService.HEADER_BASED_SESSION_VERSION
|
||||
|
||||
revokedSession = {} as jest.Mocked<RevokedSession>
|
||||
revokedSession.uuid = '2e1e43'
|
||||
|
||||
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
|
||||
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
sessionRepository.findOneByPrivateIdentifier = jest.fn().mockReturnValue(null)
|
||||
sessionRepository.deleteOneByUuid = jest.fn()
|
||||
sessionRepository.insert = jest.fn()
|
||||
sessionRepository.update = jest.fn()
|
||||
@@ -79,6 +82,7 @@ describe('SessionService', () => {
|
||||
ephemeralSessionRepository.insert = jest.fn()
|
||||
ephemeralSessionRepository.update = jest.fn()
|
||||
ephemeralSessionRepository.findOneByUuid = jest.fn()
|
||||
ephemeralSessionRepository.findOneByPrivateIdentifier = jest.fn()
|
||||
ephemeralSessionRepository.deleteOne = jest.fn()
|
||||
|
||||
revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
|
||||
@@ -140,25 +144,91 @@ describe('SessionService', () => {
|
||||
})
|
||||
|
||||
it('should refresh access and refresh tokens for a session', async () => {
|
||||
expect(await createService().refreshTokens({ session: existingSession, isEphemeral: false })).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
readonly_access: false,
|
||||
expect(
|
||||
await createService().refreshTokens({
|
||||
session: existingSession,
|
||||
isEphemeral: false,
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
}),
|
||||
).toEqual({
|
||||
sessionHttpRepresentation: {
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
readonly_access: false,
|
||||
},
|
||||
sessionCookieRepresentation: {
|
||||
accessToken: 'foobar',
|
||||
refreshToken: 'foobar',
|
||||
},
|
||||
session: existingSession,
|
||||
})
|
||||
|
||||
expect(sessionRepository.update).toHaveBeenCalled()
|
||||
expect(ephemeralSessionRepository.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should refresh access and refresh tokens for a session and turn it into a cookie based session', async () => {
|
||||
expect(
|
||||
await createService().refreshTokens({
|
||||
session: existingSession,
|
||||
isEphemeral: false,
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20240226).getValue(),
|
||||
}),
|
||||
).toEqual({
|
||||
sessionHttpRepresentation: {
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
readonly_access: false,
|
||||
},
|
||||
sessionCookieRepresentation: {
|
||||
accessToken: 'foobar',
|
||||
refreshToken: 'foobar',
|
||||
},
|
||||
session: existingSession,
|
||||
})
|
||||
|
||||
expect(sessionRepository.update).toHaveBeenCalledWith({
|
||||
apiVersion: ApiVersion.VERSIONS.v20240226,
|
||||
hashedAccessToken: expect.any(String),
|
||||
hashedRefreshToken: expect.any(String),
|
||||
refreshExpiration: expect.any(Date),
|
||||
privateIdentifier: expect.any(String),
|
||||
readonlyAccess: false,
|
||||
userAgent: 'Chrome',
|
||||
userUuid: '1-2-3',
|
||||
uuid: expect.any(String),
|
||||
version: 2,
|
||||
accessExpiration: expect.any(Date),
|
||||
snjs: null,
|
||||
application: null,
|
||||
})
|
||||
expect(ephemeralSessionRepository.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should refresh access and refresh tokens for an ephemeral session', async () => {
|
||||
expect(await createService().refreshTokens({ session: existingEphemeralSession, isEphemeral: true })).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
readonly_access: false,
|
||||
expect(
|
||||
await createService().refreshTokens({
|
||||
session: existingEphemeralSession,
|
||||
isEphemeral: true,
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
}),
|
||||
).toEqual({
|
||||
sessionHttpRepresentation: {
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
readonly_access: false,
|
||||
},
|
||||
sessionCookieRepresentation: {
|
||||
accessToken: 'foobar',
|
||||
refreshToken: 'foobar',
|
||||
},
|
||||
session: existingEphemeralSession,
|
||||
})
|
||||
|
||||
expect(sessionRepository.update).not.toHaveBeenCalled()
|
||||
@@ -171,7 +241,7 @@ describe('SessionService', () => {
|
||||
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
@@ -179,16 +249,137 @@ describe('SessionService', () => {
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith(expect.any(Session))
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith({
|
||||
accessExpiration: expect.any(Date),
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.VERSIONS.v20200115,
|
||||
createdAt: expect.any(Date),
|
||||
hashedAccessToken: expect.any(String),
|
||||
hashedRefreshToken: expect.any(String),
|
||||
refreshExpiration: expect.any(Date),
|
||||
privateIdentifier: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
userAgent: 'Google Chrome',
|
||||
userUuid: '123',
|
||||
uuid: expect.any(String),
|
||||
readonlyAccess: false,
|
||||
version: 1,
|
||||
snjs: null,
|
||||
application: null,
|
||||
})
|
||||
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
refresh_token: expect.any(String),
|
||||
readonly_access: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create new cookie based session for a user', async () => {
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20240226).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith(expect.any(Session))
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith({
|
||||
accessExpiration: expect.any(Date),
|
||||
apiVersion: ApiVersion.VERSIONS.v20240226,
|
||||
createdAt: expect.any(Date),
|
||||
hashedAccessToken: expect.any(String),
|
||||
hashedRefreshToken: expect.any(String),
|
||||
refreshExpiration: expect.any(Date),
|
||||
privateIdentifier: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
userAgent: 'Google Chrome',
|
||||
userUuid: '123',
|
||||
uuid: expect.any(String),
|
||||
readonlyAccess: false,
|
||||
version: 2,
|
||||
snjs: null,
|
||||
application: null,
|
||||
})
|
||||
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
refresh_token: expect.any(String),
|
||||
readonly_access: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create new legacy session for a user if cookie mode is disabled', async () => {
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
|
||||
const result = await createService(true).createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20240226).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith(expect.any(Session))
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith({
|
||||
accessExpiration: expect.any(Date),
|
||||
apiVersion: ApiVersion.VERSIONS.v20240226,
|
||||
createdAt: expect.any(Date),
|
||||
hashedAccessToken: expect.any(String),
|
||||
hashedRefreshToken: expect.any(String),
|
||||
refreshExpiration: expect.any(Date),
|
||||
privateIdentifier: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
userAgent: 'Google Chrome',
|
||||
userUuid: '123',
|
||||
uuid: expect.any(String),
|
||||
readonlyAccess: false,
|
||||
version: 1,
|
||||
snjs: null,
|
||||
application: null,
|
||||
})
|
||||
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
refresh_token: expect.any(String),
|
||||
readonly_access: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create new session for a user', async () => {
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith(expect.any(Session))
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith({
|
||||
accessExpiration: expect.any(Date),
|
||||
apiVersion: ApiVersion.VERSIONS.v20200115,
|
||||
createdAt: expect.any(Date),
|
||||
hashedAccessToken: expect.any(String),
|
||||
hashedRefreshToken: expect.any(String),
|
||||
privateIdentifier: expect.any(String),
|
||||
refreshExpiration: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
userAgent: 'Google Chrome',
|
||||
userUuid: '123',
|
||||
uuid: expect.any(String),
|
||||
readonlyAccess: false,
|
||||
version: 1,
|
||||
snjs: null,
|
||||
application: null,
|
||||
})
|
||||
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
@@ -207,7 +398,7 @@ describe('SessionService', () => {
|
||||
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
@@ -215,16 +406,20 @@ describe('SessionService', () => {
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith(expect.any(Session))
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith({
|
||||
accessExpiration: expect.any(Date),
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.VERSIONS.v20200115,
|
||||
createdAt: expect.any(Date),
|
||||
hashedAccessToken: expect.any(String),
|
||||
hashedRefreshToken: expect.any(String),
|
||||
refreshExpiration: expect.any(Date),
|
||||
privateIdentifier: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
userAgent: 'Google Chrome',
|
||||
userUuid: '123',
|
||||
uuid: expect.any(String),
|
||||
readonlyAccess: true,
|
||||
version: 1,
|
||||
snjs: null,
|
||||
application: null,
|
||||
})
|
||||
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
@@ -249,7 +444,7 @@ describe('SessionService', () => {
|
||||
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
@@ -257,15 +452,19 @@ describe('SessionService', () => {
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith(expect.any(Session))
|
||||
expect(sessionRepository.insert).toHaveBeenCalledWith({
|
||||
accessExpiration: expect.any(Date),
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.VERSIONS.v20200115,
|
||||
createdAt: expect.any(Date),
|
||||
hashedAccessToken: expect.any(String),
|
||||
hashedRefreshToken: expect.any(String),
|
||||
refreshExpiration: expect.any(Date),
|
||||
privateIdentifier: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
userUuid: '123',
|
||||
uuid: expect.any(String),
|
||||
readonlyAccess: false,
|
||||
version: 1,
|
||||
snjs: null,
|
||||
application: null,
|
||||
})
|
||||
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
@@ -284,7 +483,7 @@ describe('SessionService', () => {
|
||||
|
||||
await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
@@ -304,7 +503,7 @@ describe('SessionService', () => {
|
||||
|
||||
await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
@@ -325,7 +524,7 @@ describe('SessionService', () => {
|
||||
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
@@ -353,7 +552,7 @@ describe('SessionService', () => {
|
||||
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
@@ -381,7 +580,7 @@ describe('SessionService', () => {
|
||||
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
@@ -406,7 +605,7 @@ describe('SessionService', () => {
|
||||
|
||||
const result = await createService().createNewEphemeralSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.create(ApiVersion.VERSIONS.v20200115).getValue(),
|
||||
userAgent: 'Google Chrome',
|
||||
readonlyAccess: false,
|
||||
})
|
||||
@@ -414,16 +613,20 @@ describe('SessionService', () => {
|
||||
expect(ephemeralSessionRepository.insert).toHaveBeenCalledWith(expect.any(EphemeralSession))
|
||||
expect(ephemeralSessionRepository.insert).toHaveBeenCalledWith({
|
||||
accessExpiration: expect.any(Date),
|
||||
apiVersion: '003',
|
||||
apiVersion: ApiVersion.VERSIONS.v20200115,
|
||||
createdAt: expect.any(Date),
|
||||
hashedAccessToken: expect.any(String),
|
||||
hashedRefreshToken: expect.any(String),
|
||||
refreshExpiration: expect.any(Date),
|
||||
privateIdentifier: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
userAgent: 'Google Chrome',
|
||||
userUuid: '123',
|
||||
uuid: expect.any(String),
|
||||
readonlyAccess: false,
|
||||
version: 1,
|
||||
snjs: null,
|
||||
application: null,
|
||||
})
|
||||
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
@@ -435,57 +638,6 @@ describe('SessionService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should delete a session by token', async () => {
|
||||
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
|
||||
if (uuid === '2') {
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
await createService().deleteSessionByToken('1:2:3')
|
||||
|
||||
expect(sessionRepository.deleteOneByUuid).toHaveBeenCalledWith('2e1e43')
|
||||
expect(ephemeralSessionRepository.deleteOne).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should delete an ephemeral session by token', async () => {
|
||||
ephemeralSessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
|
||||
if (uuid === '2') {
|
||||
return existingEphemeralSession
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
await createService().deleteSessionByToken('1:2:3')
|
||||
|
||||
expect(sessionRepository.deleteOneByUuid).not.toHaveBeenCalled()
|
||||
expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2-3-4', '1-2-3')
|
||||
})
|
||||
|
||||
it('should not delete a session by token if session is not found', async () => {
|
||||
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
|
||||
if (uuid === '2') {
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
await createService().deleteSessionByToken('1:4:3')
|
||||
|
||||
expect(sessionRepository.deleteOneByUuid).not.toHaveBeenCalled()
|
||||
expect(ephemeralSessionRepository.deleteOne).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should determine if a refresh token is valid', async () => {
|
||||
expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2:3')).toBeTruthy()
|
||||
expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2:4')).toBeFalsy()
|
||||
expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return device info based on user agent', () => {
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on Mac 10.13')
|
||||
})
|
||||
@@ -626,67 +778,6 @@ describe('SessionService', () => {
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
|
||||
})
|
||||
|
||||
it('should retrieve a session from a session token', async () => {
|
||||
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
|
||||
if (uuid === '2') {
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
|
||||
|
||||
expect(session).toEqual(session)
|
||||
expect(isEphemeral).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should retrieve an ephemeral session from a session token', async () => {
|
||||
ephemeralSessionRepository.findOneByUuid = jest.fn().mockReturnValue(existingEphemeralSession)
|
||||
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
|
||||
|
||||
expect(session).toEqual(existingEphemeralSession)
|
||||
expect(isEphemeral).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not retrieve a session from a session token that has access token missing', async () => {
|
||||
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
|
||||
if (uuid === '2') {
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const { session } = await createService().getSessionFromToken('1:2')
|
||||
|
||||
expect(session).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not retrieve a session that is missing', async () => {
|
||||
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const { session } = await createService().getSessionFromToken('1:2:3')
|
||||
|
||||
expect(session).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not retrieve a session from a session token that has invalid access token', async () => {
|
||||
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
|
||||
if (uuid === '2') {
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const { session } = await createService().getSessionFromToken('1:2:4')
|
||||
|
||||
expect(session).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should revoked a session', async () => {
|
||||
await createService().createRevokedSession(existingSession)
|
||||
|
||||
@@ -714,4 +805,26 @@ describe('SessionService', () => {
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should retrieve a revoked cookie session from a session token', async () => {
|
||||
revokedSessionRepository.findOneByPrivateIdentifier = jest.fn().mockReturnValue(revokedSession)
|
||||
|
||||
const result = await createService().getRevokedSessionFromToken('2:3')
|
||||
|
||||
expect(result).toEqual(revokedSession)
|
||||
})
|
||||
|
||||
it('should not retrieve a revoked session if session id is missing from token', async () => {
|
||||
revokedSessionRepository.findOneByPrivateIdentifier = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createService().getRevokedSessionFromToken('2')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should not retrieve a revoked session if session token has unrecognizable version', async () => {
|
||||
const result = await createService().getRevokedSessionFromToken('3:2')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,9 +20,14 @@ import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInt
|
||||
import { TraceSession } from '../UseCase/TraceSession/TraceSession'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { GetSetting } from '../UseCase/GetSetting/GetSetting'
|
||||
import { SessionCreationResult } from './SessionCreationResult'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
|
||||
export class SessionService implements SessionServiceInterface {
|
||||
static readonly SESSION_TOKEN_VERSION = 1
|
||||
static readonly COOKIE_SESSION_TOKEN_VERSION = 2
|
||||
static readonly HEADER_BASED_SESSION_VERSION = 1
|
||||
static readonly COOKIE_BASED_SESSION_VERSION = 2
|
||||
|
||||
constructor(
|
||||
private sessionRepository: SessionRepositoryInterface,
|
||||
@@ -38,20 +43,23 @@ export class SessionService implements SessionServiceInterface {
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
private readonlyUsers: string[],
|
||||
private getSetting: GetSetting,
|
||||
private forceLegacySessions: boolean,
|
||||
) {}
|
||||
|
||||
async createNewSessionForUser(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
apiVersion: ApiVersion
|
||||
userAgent: string
|
||||
readonlyAccess: boolean
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<SessionCreationResult> {
|
||||
const session = await this.createSession({
|
||||
ephemeral: false,
|
||||
...dto,
|
||||
})
|
||||
|
||||
const sessionPayload = await this.createTokens(session)
|
||||
const sessionPayload = await this.createTokens(session, dto.apiVersion)
|
||||
|
||||
await this.sessionRepository.insert(session)
|
||||
|
||||
@@ -70,34 +78,49 @@ export class SessionService implements SessionServiceInterface {
|
||||
}
|
||||
|
||||
return {
|
||||
sessionHttpRepresentation: sessionPayload,
|
||||
...sessionPayload,
|
||||
session,
|
||||
}
|
||||
}
|
||||
|
||||
async createNewEphemeralSessionForUser(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
apiVersion: ApiVersion
|
||||
userAgent: string
|
||||
readonlyAccess: boolean
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<SessionCreationResult> {
|
||||
const ephemeralSession = await this.createSession({
|
||||
ephemeral: true,
|
||||
...dto,
|
||||
})
|
||||
|
||||
const sessionPayload = await this.createTokens(ephemeralSession)
|
||||
const sessionPayload = await this.createTokens(ephemeralSession, dto.apiVersion)
|
||||
|
||||
await this.ephemeralSessionRepository.insert(ephemeralSession)
|
||||
|
||||
return {
|
||||
sessionHttpRepresentation: sessionPayload,
|
||||
...sessionPayload,
|
||||
session: ephemeralSession,
|
||||
}
|
||||
}
|
||||
|
||||
async refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody> {
|
||||
const sessionPayload = await this.createTokens(dto.session)
|
||||
async refreshTokens(dto: {
|
||||
session: Session
|
||||
isEphemeral: boolean
|
||||
apiVersion: ApiVersion
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<SessionCreationResult> {
|
||||
const sessionPayload = await this.createTokens(dto.session, dto.apiVersion)
|
||||
|
||||
dto.session.apiVersion = dto.apiVersion.value
|
||||
dto.session.version = this.shouldOperateOnCookieBasedSessions(dto.apiVersion)
|
||||
? SessionService.COOKIE_BASED_SESSION_VERSION
|
||||
: SessionService.HEADER_BASED_SESSION_VERSION
|
||||
dto.session.snjs = dto.snjs ?? null
|
||||
dto.session.application = dto.application ?? null
|
||||
|
||||
if (dto.isEphemeral) {
|
||||
await this.ephemeralSessionRepository.update(dto.session)
|
||||
@@ -105,19 +128,10 @@ export class SessionService implements SessionServiceInterface {
|
||||
await this.sessionRepository.update(dto.session)
|
||||
}
|
||||
|
||||
return sessionPayload
|
||||
}
|
||||
|
||||
isRefreshTokenMatchingHashedSessionToken(session: Session, token: string): boolean {
|
||||
const tokenParts = token.split(':')
|
||||
const refreshToken = tokenParts[2]
|
||||
if (!refreshToken) {
|
||||
return false
|
||||
return {
|
||||
...sessionPayload,
|
||||
session: dto.session,
|
||||
}
|
||||
|
||||
const hashedRefreshToken = crypto.createHash('sha256').update(refreshToken).digest('hex')
|
||||
|
||||
return crypto.timingSafeEqual(Buffer.from(hashedRefreshToken), Buffer.from(session.hashedRefreshToken))
|
||||
}
|
||||
|
||||
getOperatingSystemInfoFromUserAgent(userAgent: string): string {
|
||||
@@ -182,35 +196,30 @@ export class SessionService implements SessionServiceInterface {
|
||||
return `${browserInfo} on ${osInfo}`
|
||||
}
|
||||
|
||||
async getSessionFromToken(token: string): Promise<{ session: Session | undefined; isEphemeral: boolean }> {
|
||||
const tokenParts = token.split(':')
|
||||
const sessionUuid = tokenParts[1]
|
||||
const accessToken = tokenParts[2]
|
||||
if (!accessToken) {
|
||||
return { session: undefined, isEphemeral: false }
|
||||
}
|
||||
|
||||
const { session, isEphemeral } = await this.getSession(sessionUuid)
|
||||
if (!session) {
|
||||
return { session: undefined, isEphemeral: false }
|
||||
}
|
||||
|
||||
const hashedAccessToken = crypto.createHash('sha256').update(accessToken).digest('hex')
|
||||
if (crypto.timingSafeEqual(Buffer.from(session.hashedAccessToken), Buffer.from(hashedAccessToken))) {
|
||||
return { session, isEphemeral }
|
||||
}
|
||||
|
||||
return { session: undefined, isEphemeral: false }
|
||||
}
|
||||
|
||||
async getRevokedSessionFromToken(token: string): Promise<RevokedSession | null> {
|
||||
const tokenParts = token.split(':')
|
||||
const sessionUuid = tokenParts[1]
|
||||
if (!sessionUuid) {
|
||||
return null
|
||||
const tokenVersion = parseInt(tokenParts[0])
|
||||
|
||||
switch (tokenVersion) {
|
||||
case SessionService.SESSION_TOKEN_VERSION: {
|
||||
const sessionUuid = tokenParts[1]
|
||||
if (!sessionUuid) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.revokedSessionRepository.findOneByUuid(sessionUuid)
|
||||
}
|
||||
case SessionService.COOKIE_SESSION_TOKEN_VERSION: {
|
||||
const privateIdentifier = tokenParts[1]
|
||||
if (!privateIdentifier) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.revokedSessionRepository.findOneByPrivateIdentifier(privateIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
return this.revokedSessionRepository.findOneByUuid(sessionUuid)
|
||||
return null
|
||||
}
|
||||
|
||||
async markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession> {
|
||||
@@ -222,22 +231,6 @@ export class SessionService implements SessionServiceInterface {
|
||||
return revokedSession
|
||||
}
|
||||
|
||||
async deleteSessionByToken(token: string): Promise<string | null> {
|
||||
const { session, isEphemeral } = await this.getSessionFromToken(token)
|
||||
|
||||
if (session) {
|
||||
if (isEphemeral) {
|
||||
await this.ephemeralSessionRepository.deleteOne(session.uuid, session.userUuid)
|
||||
} else {
|
||||
await this.sessionRepository.deleteOneByUuid(session.uuid)
|
||||
}
|
||||
|
||||
return session.userUuid
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async createRevokedSession(session: Session): Promise<RevokedSession> {
|
||||
const revokedSession = new RevokedSession()
|
||||
revokedSession.uuid = session.uuid
|
||||
@@ -245,6 +238,7 @@ export class SessionService implements SessionServiceInterface {
|
||||
revokedSession.createdAt = this.timer.getUTCDate()
|
||||
revokedSession.apiVersion = session.apiVersion
|
||||
revokedSession.userAgent = session.userAgent
|
||||
revokedSession.privateIdentifier = session.privateIdentifier
|
||||
|
||||
await this.revokedSessionRepository.insert(revokedSession)
|
||||
|
||||
@@ -253,23 +247,31 @@ export class SessionService implements SessionServiceInterface {
|
||||
|
||||
private async createSession(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
apiVersion: ApiVersion
|
||||
userAgent: string
|
||||
ephemeral: boolean
|
||||
readonlyAccess: boolean
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<Session> {
|
||||
let session = new Session()
|
||||
if (dto.ephemeral) {
|
||||
session = new EphemeralSession()
|
||||
}
|
||||
session.uuid = uuidv4()
|
||||
session.privateIdentifier = await this.cryptoNode.generateRandomKey(128)
|
||||
if (await this.isLoggingUserAgentEnabledOnSessions(dto.user)) {
|
||||
session.userAgent = dto.userAgent
|
||||
}
|
||||
session.snjs = dto.snjs ?? null
|
||||
session.application = dto.application ?? null
|
||||
session.userUuid = dto.user.uuid
|
||||
session.apiVersion = dto.apiVersion
|
||||
session.apiVersion = dto.apiVersion.value
|
||||
session.createdAt = this.timer.getUTCDate()
|
||||
session.updatedAt = this.timer.getUTCDate()
|
||||
session.version = this.shouldOperateOnCookieBasedSessions(dto.apiVersion)
|
||||
? SessionService.COOKIE_BASED_SESSION_VERSION
|
||||
: SessionService.HEADER_BASED_SESSION_VERSION
|
||||
|
||||
const userIsReadonly = this.readonlyUsers.includes(dto.user.email)
|
||||
session.readonlyAccess = userIsReadonly || dto.readonlyAccess
|
||||
@@ -277,22 +279,16 @@ export class SessionService implements SessionServiceInterface {
|
||||
return session
|
||||
}
|
||||
|
||||
private async getSession(uuid: string): Promise<{
|
||||
session: Session | null
|
||||
isEphemeral: boolean
|
||||
}> {
|
||||
let session = await this.ephemeralSessionRepository.findOneByUuid(uuid)
|
||||
let isEphemeral = true
|
||||
|
||||
if (!session) {
|
||||
session = await this.sessionRepository.findOneByUuid(uuid)
|
||||
isEphemeral = false
|
||||
private async createTokens(
|
||||
session: Session,
|
||||
apiVersion: ApiVersion,
|
||||
): Promise<{
|
||||
sessionHttpRepresentation: SessionBody
|
||||
sessionCookieRepresentation: {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
return { session, isEphemeral }
|
||||
}
|
||||
|
||||
private async createTokens(session: Session): Promise<SessionBody> {
|
||||
}> {
|
||||
const accessToken = this.cryptoNode.base64URLEncode(await this.cryptoNode.generateRandomKey(48))
|
||||
const refreshToken = this.cryptoNode.base64URLEncode(await this.cryptoNode.generateRandomKey(48))
|
||||
|
||||
@@ -305,13 +301,29 @@ export class SessionService implements SessionServiceInterface {
|
||||
const refreshTokenExpiration = dayjs.utc().add(this.refreshTokenAge, 'second').toDate()
|
||||
session.accessExpiration = accessTokenExpiration
|
||||
session.refreshExpiration = refreshTokenExpiration
|
||||
if (!session.privateIdentifier) {
|
||||
session.privateIdentifier = await this.cryptoNode.generateRandomKey(128)
|
||||
}
|
||||
|
||||
const accessTokenForHeaderPurposes = this.shouldOperateOnCookieBasedSessions(apiVersion)
|
||||
? `${SessionService.COOKIE_SESSION_TOKEN_VERSION}:${session.privateIdentifier}`
|
||||
: `${SessionService.SESSION_TOKEN_VERSION}:${session.uuid}:${accessToken}`
|
||||
const refreshTokenForHeaderPurposes = this.shouldOperateOnCookieBasedSessions(apiVersion)
|
||||
? `${SessionService.COOKIE_SESSION_TOKEN_VERSION}:${session.privateIdentifier}`
|
||||
: `${SessionService.SESSION_TOKEN_VERSION}:${session.uuid}:${refreshToken}`
|
||||
|
||||
return {
|
||||
access_token: `${SessionService.SESSION_TOKEN_VERSION}:${session.uuid}:${accessToken}`,
|
||||
refresh_token: `${SessionService.SESSION_TOKEN_VERSION}:${session.uuid}:${refreshToken}`,
|
||||
access_expiration: this.timer.convertStringDateToMilliseconds(accessTokenExpiration.toString()),
|
||||
refresh_expiration: this.timer.convertStringDateToMilliseconds(refreshTokenExpiration.toString()),
|
||||
readonly_access: session.readonlyAccess,
|
||||
sessionHttpRepresentation: {
|
||||
access_token: accessTokenForHeaderPurposes,
|
||||
refresh_token: refreshTokenForHeaderPurposes,
|
||||
access_expiration: this.timer.convertStringDateToMilliseconds(accessTokenExpiration.toString()),
|
||||
refresh_expiration: this.timer.convertStringDateToMilliseconds(refreshTokenExpiration.toString()),
|
||||
readonly_access: session.readonlyAccess,
|
||||
},
|
||||
sessionCookieRepresentation: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,4 +341,12 @@ export class SessionService implements SessionServiceInterface {
|
||||
|
||||
return loggingSetting.decryptedValue === LogSessionUserAgentOption.Enabled
|
||||
}
|
||||
|
||||
private shouldOperateOnCookieBasedSessions(apiVersion: ApiVersion): boolean {
|
||||
if (this.forceLegacySessions) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ApiVersion.VERSIONS.v20240226 === apiVersion.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import { SessionBody } from '@standardnotes/responses'
|
||||
import { User } from '../User/User'
|
||||
import { RevokedSession } from './RevokedSession'
|
||||
import { Session } from './Session'
|
||||
import { SessionCreationResult } from './SessionCreationResult'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
|
||||
export interface SessionServiceInterface {
|
||||
createNewSessionForUser(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
apiVersion: ApiVersion
|
||||
userAgent: string
|
||||
readonlyAccess: boolean
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }>
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<SessionCreationResult>
|
||||
createNewEphemeralSessionForUser(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
apiVersion: ApiVersion
|
||||
userAgent: string
|
||||
readonlyAccess: boolean
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }>
|
||||
refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody>
|
||||
getSessionFromToken(token: string): Promise<{ session: Session | undefined; isEphemeral: boolean }>
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<SessionCreationResult>
|
||||
refreshTokens(dto: {
|
||||
session: Session
|
||||
isEphemeral: boolean
|
||||
apiVersion: ApiVersion
|
||||
snjs?: string
|
||||
application?: string
|
||||
}): Promise<SessionCreationResult>
|
||||
getRevokedSessionFromToken(token: string): Promise<RevokedSession | null>
|
||||
markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession>
|
||||
deleteSessionByToken(token: string): Promise<string | null>
|
||||
isRefreshTokenMatchingHashedSessionToken(session: Session, token: string): boolean
|
||||
getDeviceInfo(session: Session): string
|
||||
getOperatingSystemInfoFromUserAgent(userAgent: string): string
|
||||
getBrowserInfoFromUserAgent(userAgent: string): string
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface SessionTokensCooldownRepositoryInterface {
|
||||
setCooldown(dto: {
|
||||
sessionUuid: Uuid
|
||||
hashedAccessToken: string
|
||||
hashedRefreshToken: string
|
||||
cooldownPeriodInSeconds: number
|
||||
}): Promise<void>
|
||||
getHashedTokens(sessionUuid: Uuid): Promise<{
|
||||
hashedAccessToken: string
|
||||
hashedRefreshToken: string
|
||||
} | null>
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user