Compare commits

..

15 Commits

Author SHA1 Message Date
standardci
162a63ae2b chore(release): publish new version
Some checks failed
E2E Test Suite / E2E (push) Has been cancelled
Publish Packages / build (push) Has been cancelled
Publish Packages / lint (push) Has been cancelled
Publish Packages / test (push) Has been cancelled
Publish Packages / E2E Base Suite (push) Has been cancelled
Publish Packages / E2E Vaults Suite (push) Has been cancelled
Publish Packages / Publish Self Hosting Docker Image (push) Has been cancelled
Publish Packages / publish-services (push) Has been cancelled
- @standardnotes/analytics@2.34.18
 - @standardnotes/api-gateway@1.92.2
 - @standardnotes/auth-server@1.178.6
 - @standardnotes/common@1.52.4
 - @standardnotes/domain-core@1.41.3
 - @standardnotes/files-server@1.38.3
 - @standardnotes/home-server@1.23.2
 - @standardnotes/revisions-server@1.51.19
 - @standardnotes/scheduler-server@1.27.23
 - @standardnotes/settings@1.23.4
 - @standardnotes/syncing-server@1.136.5
 - @standardnotes/websockets-server@1.22.14
2025-04-29 11:54:24 +00:00
Aman Harwara
0d82819cba chore: release latest code (#1075) 2025-04-29 13:33:15 +02:00
Karol Sójko
578ce0e74e Fix puppeteer sandbox issue (#1077)
Force merging for the rules to apply in basic workflows
2025-04-29 13:05:12 +02:00
Aman Harwara
532be7c358 chore: upgrade github actions (#1076)
Some checks failed
E2E Test Suite / E2E (push) Has been cancelled
Publish Packages / build (push) Has been cancelled
Publish Packages / lint (push) Has been cancelled
Publish Packages / test (push) Has been cancelled
Publish Packages / E2E Base Suite (push) Has been cancelled
Publish Packages / E2E Vaults Suite (push) Has been cancelled
Publish Packages / Publish Self Hosting Docker Image (push) Has been cancelled
Publish Packages / publish-services (push) Has been cancelled
2025-04-25 20:03:20 +05:30
standardci
d406272f07 chore(release): publish new version
- @standardnotes/analytics@2.34.17
 - @standardnotes/api-gateway@1.92.1
 - @standardnotes/auth-server@1.178.5
 - @standardnotes/common@1.52.3
 - @standardnotes/domain-core@1.41.2
 - @standardnotes/domain-events-infra@1.23.4
 - @standardnotes/domain-events@2.141.1
 - @standardnotes/files-server@1.38.2
 - @standardnotes/grpc@1.4.2
 - @standardnotes/home-server@1.23.1
 - @standardnotes/predicates@1.8.2
 - @standardnotes/revisions-server@1.51.18
 - @standardnotes/scheduler-server@1.27.22
 - @standardnotes/security@1.17.4
 - @standardnotes/settings@1.23.3
 - @standardnotes/sncrypto-node@1.16.3
 - @standardnotes/syncing-server@1.136.4
 - @standardnotes/time@1.19.1
 - @standardnotes/websockets-server@1.22.13
2024-06-18 10:06:54 +00:00
Karol Sójko
9de3352885 fix(home-server): bump version 2024-06-18 11:46:16 +02:00
Karol Sójko
8575d20f7b fix: bump versions on packages 2024-06-18 10:33:39 +02:00
Karol Sójko
102d4b1e8a fix(api-gateway): bump version 2024-06-18 10:09:04 +02:00
Karol Sójko
1a57c247b2 chore: release latest changes (#1056)
* chore: release latest changes

* update yarn lockfile

* remove stale files

* fix ci env

* remove mysql command overwrite

* remove mysql overwrite from example

* fix cookie cooldown in memory
2024-06-18 09:29:24 +02:00
standardci
dbb0e4a974 chore(release): publish new version
- @standardnotes/api-gateway@1.91.0
 - @standardnotes/files-server@1.38.0
 - @standardnotes/home-server@1.23.0
2024-03-20 15:04:32 +00:00
Karol Sójko
5c02435ee4 feat: add CORS_ORIGIN_STRICT_MODE_ENABLED env var to determine if CORS origin should be restricted 2024-03-20 15:59:43 +01:00
standardci
0a1e555b13 chore(release): publish new version
- @standardnotes/api-gateway@1.90.3
 - @standardnotes/home-server@1.22.68
2024-03-18 10:22:49 +00:00
Karol Sójko
be668d7d7a fix(api-gateway): response headers cors issue - fixes #1046 2024-03-18 11:17:52 +01:00
standardci
87e50ec941 chore(release): publish new version
- @standardnotes/api-gateway@1.90.2
 - @standardnotes/files-server@1.37.12
 - @standardnotes/home-server@1.22.67
2024-03-18 08:48:11 +00:00
Karol Sójko
6d7ca1b926 fix: cors issues on clients - fixes #1046 (#1049) 2024-03-18 09:43:58 +01:00
294 changed files with 6084 additions and 1790 deletions

3
.github/ci.env vendored
View File

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

View File

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

View File

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

View File

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

View File

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

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,4 +26,5 @@ export interface ResponseLocals {
sharedVaultOwnerContext?: {
upload_bytes_limit: number
}
hasContentLimit: boolean
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -0,0 +1,3 @@
export interface ApiVersionProps {
value: string
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { ApiVersion } from '../Api/ApiVersion'
import { AuthResponseFactoryInterface } from './AuthResponseFactoryInterface'
export interface AuthResponseFactoryResolverInterface {
resolveAuthResponseFactoryVersion(apiVersion: string): AuthResponseFactoryInterface
resolveAuthResponseFactoryVersion(apiVersion: ApiVersion): AuthResponseFactoryInterface
}

View File

@@ -7,5 +7,6 @@ export type AuthenticationMethod = {
user: User | null
claims?: Record<string, unknown>
session?: Session
givenTokensWereInCooldown?: boolean
revokedSession?: RevokedSession
}

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,8 @@
export interface CookieFactoryInterface {
createCookieHeaderValue(dto: {
sessionUuid: string
accessToken: string
refreshToken: string
refreshTokenExpiration: Date
}): string[]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
export interface CaptchaServerInterface {
verify(hvmToken: string): Promise<boolean>
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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