mirror of
https://github.com/standardnotes/server
synced 2026-04-19 08:02:23 -04:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8dea171115 | |||
| aef9254713 | |||
| 31b7396006 | |||
| be0a2649da | |||
| bf8f91f83d | |||
| effdfebc19 | |||
| f4816e6c9a | |||
| 152a5cbd27 | |||
| 1488763115 | |||
| bbb35d16fc | |||
| ef07045ee9 | |||
| 3ba673b424 | |||
| 9c4032ebea | |||
| 05bb12c978 | |||
| df957f07e3 | |||
| b510284e01 | |||
| 205a1ed637 | |||
| 2073c735a5 | |||
| 34085ac6fb | |||
| 3d6559921b | |||
| 15a7f0e71a | |||
| 3e56243d6f | |||
| 032fcb938d | |||
| e98393452b | |||
| 302b624504 | |||
| e00d9d2ca0 | |||
| 9ab4601c8d | |||
| 19e43bdb1a | |||
| 49832e7944 | |||
| 916e98936a | |||
| 31d1eef7f7 | |||
| 2648d9a813 | |||
| b24b576209 | |||
| faee38bffd | |||
| 65f3503fe8 | |||
| 054023b791 | |||
| 383c3a68fa | |||
| 7d22b1c15c | |||
| c71e7cd926 | |||
| 83ad069c5d | |||
| 081108d9ba | |||
| 8f3df56a2b | |||
| d02124f4e5 | |||
| 09e351fedb | |||
| ad4b85b095 | |||
| 0bf7d8beae | |||
| 1ae7cca394 | |||
| bc1c7a8ae1 | |||
| c22c5e4584 | |||
| ac3646836c | |||
| 7a31ab75d6 | |||
| c49dc35ab5 | |||
| 06cedd11d8 | |||
| f496376fb3 | |||
| 091e2a57e8 | |||
| 0d40ef6796 | |||
| 1be33ba4c3 | |||
| aaeb311928 | |||
| a7a38c07ac | |||
| 56f49752b4 | |||
| 892d8b6fe2 | |||
| cec2005436 | |||
| 0eb86c0096 | |||
| b8e39d76c1 | |||
| 1c3ff526b7 | |||
| 373767248c | |||
| d7965b2748 | |||
| cbcd2ec87a | |||
| c74d37fc48 | |||
| 66f9352a06 | |||
| e5eef3aba0 | |||
| d261c81cd0 | |||
| 634e3bbb67 | |||
| f8c9e67063 | |||
| 18eddea6f8 | |||
| c6d655c5f5 | |||
| 46867c1a4d | |||
| d29903bab6 | |||
| 3415cae093 | |||
| 408fd5a0c6 | |||
| 0a16ee64fe | |||
| 22b00479b4 | |||
| 5311e74266 | |||
| 5be7db7788 | |||
| 3bd1547ce3 | |||
| a1fe15f7a9 | |||
| 19b8921f28 | |||
| 6b7879ba15 | |||
| bd5f492a73 | |||
| 67311cc002 | |||
| f39d3aca5b | |||
| 8e47491e3c | |||
| 0036d527bd |
+7
-1
@@ -10,7 +10,7 @@ REDIS_HOST=cache
|
||||
AUTH_SERVER_ACCESS_TOKEN_AGE=4
|
||||
AUTH_SERVER_REFRESH_TOKEN_AGE=10
|
||||
AUTH_SERVER_EPHEMERAL_SESSION_AGE=300
|
||||
SYNCING_SERVER_REVISIONS_FREQUENCY=5
|
||||
SYNCING_SERVER_REVISIONS_FREQUENCY=2
|
||||
AUTH_SERVER_LOG_LEVEL=debug
|
||||
SYNCING_SERVER_LOG_LEVEL=debug
|
||||
FILES_SERVER_LOG_LEVEL=debug
|
||||
@@ -22,6 +22,12 @@ MYSQL_USER=std_notes_user
|
||||
MYSQL_PASSWORD=changeme123
|
||||
MYSQL_ROOT_PASSWORD=changeme123
|
||||
|
||||
MONGO_HOST=secondary_db
|
||||
MONGO_PORT=27017
|
||||
MONGO_USERNAME=standardnotes
|
||||
MONGO_PASSWORD=standardnotes
|
||||
MONGO_DATABASE=standardnotes
|
||||
|
||||
AUTH_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
|
||||
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
|
||||
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
|
||||
|
||||
@@ -19,7 +19,12 @@ on:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: (Docker) E2E Test Suite
|
||||
name: (Self Hosting) E2E Test Suite
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
secondary_db_enabled: [true, false]
|
||||
transition_mode_enabled: [true, false]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
@@ -45,12 +50,14 @@ jobs:
|
||||
env:
|
||||
DB_TYPE: mysql
|
||||
CACHE_TYPE: redis
|
||||
SECONDARY_DB_ENABLED: ${{ matrix.secondary_db_enabled }}
|
||||
TRANSITION_MODE_ENABLED: ${{ matrix.transition_mode_enabled }}
|
||||
|
||||
- name: Wait for server to start
|
||||
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=enabled
|
||||
|
||||
- name: Show logs on failure
|
||||
if: ${{ failure() }}
|
||||
@@ -67,13 +74,8 @@ jobs:
|
||||
matrix:
|
||||
db_type: [mysql, sqlite]
|
||||
cache_type: [redis, memory]
|
||||
include:
|
||||
- cache_type: redis
|
||||
db_type: mysql
|
||||
redis_port: 6380
|
||||
- cache_type: redis
|
||||
db_type: sqlite
|
||||
redis_port: 6381
|
||||
secondary_db_enabled: [true, false]
|
||||
transition_mode_enabled: [true, false]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -85,16 +87,24 @@ jobs:
|
||||
cache:
|
||||
image: redis
|
||||
ports:
|
||||
- ${{ matrix.redis_port }}:6379
|
||||
- 6379:6379
|
||||
db:
|
||||
image: mysql
|
||||
ports:
|
||||
- 3307:3306
|
||||
- 3306:3306
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: standardnotes_${{ matrix.cache_type }}
|
||||
MYSQL_DATABASE: standardnotes
|
||||
MYSQL_USER: standardnotes
|
||||
MYSQL_PASSWORD: standardnotes
|
||||
secondary_db:
|
||||
image: mongo:5.0
|
||||
ports:
|
||||
- 27017:27017
|
||||
env:
|
||||
MONGO_INITDB_ROOT_USERNAME: standardnotes
|
||||
MONGO_INITDB_ROOT_PASSWORD: standardnotes
|
||||
MONGO_INITDB_DATABASE: standardnotes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -123,16 +133,23 @@ jobs:
|
||||
sed -i "s/VALET_TOKEN_SECRET=/VALET_TOKEN_SECRET=$(openssl rand -hex 32)/g" packages/home-server/.env
|
||||
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
|
||||
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
|
||||
echo "REVISIONS_FREQUENCY=5" >> packages/home-server/.env
|
||||
echo "REVISIONS_FREQUENCY=2" >> packages/home-server/.env
|
||||
echo "DB_HOST=localhost" >> packages/home-server/.env
|
||||
echo "DB_PORT=3307" >> packages/home-server/.env
|
||||
echo "DB_DATABASE=standardnotes_${{ matrix.cache_type }}" >> packages/home-server/.env
|
||||
echo "DB_SQLITE_DATABASE_PATH=sqlite_${{ matrix.cache_type }}.db" >> packages/home-server/.env
|
||||
echo "DB_PORT=3306" >> packages/home-server/.env
|
||||
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env
|
||||
echo "DB_SQLITE_DATABASE_PATH=homeserver.db" >> packages/home-server/.env
|
||||
echo "DB_USERNAME=standardnotes" >> packages/home-server/.env
|
||||
echo "DB_PASSWORD=standardnotes" >> packages/home-server/.env
|
||||
echo "DB_TYPE=${{ matrix.db_type }}" >> packages/home-server/.env
|
||||
echo "REDIS_URL=redis://localhost:${{ matrix.redis_port }}" >> packages/home-server/.env
|
||||
echo "REDIS_URL=redis://localhost:6379" >> packages/home-server/.env
|
||||
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
|
||||
echo "SECONDARY_DB_ENABLED=${{ matrix.secondary_db_enabled }}" >> packages/home-server/.env
|
||||
echo "TRANSITION_MODE_ENABLED=${{ matrix.transition_mode_enabled }}" >> packages/home-server/.env
|
||||
echo "MONGO_HOST=localhost" >> packages/home-server/.env
|
||||
echo "MONGO_PORT=27017" >> packages/home-server/.env
|
||||
echo "MONGO_DATABASE=standardnotes" >> packages/home-server/.env
|
||||
echo "MONGO_USERNAME=standardnotes" >> packages/home-server/.env
|
||||
echo "MONGO_PASSWORD=standardnotes" >> packages/home-server/.env
|
||||
echo "FILES_SERVER_URL=http://localhost:3123" >> packages/home-server/.env
|
||||
echo "E2E_TESTING=true" >> packages/home-server/.env
|
||||
|
||||
@@ -145,7 +162,7 @@ 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 1800000 -f http://localhost:9001/mocha/test.html
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=enabled
|
||||
|
||||
- name: Show logs on failure
|
||||
if: ${{ failure() }}
|
||||
|
||||
@@ -5191,6 +5191,7 @@ const RAW_RUNTIME_STATE =
|
||||
["inversify-express-utils", "npm:6.4.3"],\
|
||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
|
||||
["jsonwebtoken", "npm:9.0.0"],\
|
||||
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["newrelic", "npm:10.1.2"],\
|
||||
["nodemon", "npm:2.0.22"],\
|
||||
@@ -5201,7 +5202,7 @@ const RAW_RUNTIME_STATE =
|
||||
["semver", "npm:7.5.1"],\
|
||||
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
|
||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
|
||||
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.16"],\
|
||||
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16"],\
|
||||
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
|
||||
["ua-parser-js", "npm:1.0.35"],\
|
||||
["uuid", "npm:9.0.0"],\
|
||||
@@ -5869,6 +5870,26 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/webidl-conversions", [\
|
||||
["npm:7.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/@types-webidl-conversions-npm-7.0.0-0903313151-86c337dc1e.zip/node_modules/@types/webidl-conversions/",\
|
||||
"packageDependencies": [\
|
||||
["@types/webidl-conversions", "npm:7.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/whatwg-url", [\
|
||||
["npm:8.2.2", {\
|
||||
"packageLocation": "./.yarn/cache/@types-whatwg-url-npm-8.2.2-54c5c24e6c-25f20f5649.zip/node_modules/@types/whatwg-url/",\
|
||||
"packageDependencies": [\
|
||||
["@types/whatwg-url", "npm:8.2.2"],\
|
||||
["@types/node", "npm:20.2.5"],\
|
||||
["@types/webidl-conversions", "npm:7.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/yargs", [\
|
||||
["npm:17.0.24", {\
|
||||
"packageLocation": "./.yarn/cache/@types-yargs-npm-17.0.24-b034cf1d8b-f7811cc0b9.zip/node_modules/@types/yargs/",\
|
||||
@@ -7074,6 +7095,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["bson", [\
|
||||
["npm:5.4.0", {\
|
||||
"packageLocation": "./.yarn/cache/bson-npm-5.4.0-2f854c8216-2c913a45c0.zip/node_modules/bson/",\
|
||||
"packageDependencies": [\
|
||||
["bson", "npm:5.4.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["buffer", [\
|
||||
["npm:5.7.1", {\
|
||||
"packageLocation": "./.yarn/cache/buffer-npm-5.7.1-513ef8259e-8e611bed4d.zip/node_modules/buffer/",\
|
||||
@@ -11932,6 +11962,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["memory-pager", [\
|
||||
["npm:1.5.0", {\
|
||||
"packageLocation": "./.yarn/cache/memory-pager-npm-1.5.0-46e20e6c81-6b00ff499b.zip/node_modules/memory-pager/",\
|
||||
"packageDependencies": [\
|
||||
["memory-pager", "npm:1.5.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["meow", [\
|
||||
["npm:8.1.2", {\
|
||||
"packageLocation": "./.yarn/cache/meow-npm-8.1.2-bcfe48d4f3-e36c879078.zip/node_modules/meow/",\
|
||||
@@ -12290,6 +12329,59 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["mongodb", [\
|
||||
["npm:5.7.0", {\
|
||||
"packageLocation": "./.yarn/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
|
||||
"packageDependencies": [\
|
||||
["mongodb", "npm:5.7.0"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/mongodb-virtual-eb0cd47e23/0/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
|
||||
"packageDependencies": [\
|
||||
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
|
||||
["@aws-sdk/credential-providers", null],\
|
||||
["@mongodb-js/zstd", null],\
|
||||
["@types/aws-sdk__credential-providers", null],\
|
||||
["@types/kerberos", null],\
|
||||
["@types/mongodb-client-encryption", null],\
|
||||
["@types/mongodb-js__zstd", null],\
|
||||
["@types/snappy", null],\
|
||||
["bson", "npm:5.4.0"],\
|
||||
["kerberos", null],\
|
||||
["mongodb-client-encryption", null],\
|
||||
["mongodb-connection-string-url", "npm:2.6.0"],\
|
||||
["saslprep", "npm:1.0.3"],\
|
||||
["snappy", null],\
|
||||
["socks", "npm:2.7.1"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@aws-sdk/credential-providers",\
|
||||
"@mongodb-js/zstd",\
|
||||
"@types/aws-sdk__credential-providers",\
|
||||
"@types/kerberos",\
|
||||
"@types/mongodb-client-encryption",\
|
||||
"@types/mongodb-js__zstd",\
|
||||
"@types/snappy",\
|
||||
"kerberos",\
|
||||
"mongodb-client-encryption",\
|
||||
"snappy"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["mongodb-connection-string-url", [\
|
||||
["npm:2.6.0", {\
|
||||
"packageLocation": "./.yarn/cache/mongodb-connection-string-url-npm-2.6.0-af011ba17f-8a9186dd1b.zip/node_modules/mongodb-connection-string-url/",\
|
||||
"packageDependencies": [\
|
||||
["mongodb-connection-string-url", "npm:2.6.0"],\
|
||||
["@types/whatwg-url", "npm:8.2.2"],\
|
||||
["whatwg-url", "npm:11.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["ms", [\
|
||||
["npm:2.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/ms-npm-2.0.0-9e1101a471-de027828fc.zip/node_modules/ms/",\
|
||||
@@ -14249,6 +14341,16 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["saslprep", [\
|
||||
["npm:1.0.3", {\
|
||||
"packageLocation": "./.yarn/cache/saslprep-npm-1.0.3-8db649c346-23ebcda091.zip/node_modules/saslprep/",\
|
||||
"packageDependencies": [\
|
||||
["saslprep", "npm:1.0.3"],\
|
||||
["sparse-bitfield", "npm:3.0.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["schema-utils", [\
|
||||
["npm:3.1.2", {\
|
||||
"packageLocation": "./.yarn/cache/schema-utils-npm-3.1.2-d97c6dc247-11d35f997e.zip/node_modules/schema-utils/",\
|
||||
@@ -14604,6 +14706,16 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["sparse-bitfield", [\
|
||||
["npm:3.0.3", {\
|
||||
"packageLocation": "./.yarn/cache/sparse-bitfield-npm-3.0.3-cb80d0c89f-625ecdf6f4.zip/node_modules/sparse-bitfield/",\
|
||||
"packageDependencies": [\
|
||||
["sparse-bitfield", "npm:3.0.3"],\
|
||||
["memory-pager", "npm:1.5.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["spawn-please", [\
|
||||
["npm:2.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/spawn-please-npm-2.0.1-265b6b5432-fe19a7ceb5.zip/node_modules/spawn-please/",\
|
||||
@@ -15246,6 +15358,14 @@ const RAW_RUNTIME_STATE =
|
||||
["tr46", "npm:0.0.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:3.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/tr46-npm-3.0.0-e1ae1ea7c9-3a481676bf.zip/node_modules/tr46/",\
|
||||
"packageDependencies": [\
|
||||
["tr46", "npm:3.0.0"],\
|
||||
["punycode", "npm:2.3.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["treeverse", [\
|
||||
@@ -15757,6 +15877,98 @@ const RAW_RUNTIME_STATE =
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16", {\
|
||||
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-13b6364fde/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
|
||||
"packageDependencies": [\
|
||||
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16"],\
|
||||
["@google-cloud/spanner", null],\
|
||||
["@sap/hana-client", null],\
|
||||
["@sqltools/formatter", "npm:1.2.5"],\
|
||||
["@types/better-sqlite3", null],\
|
||||
["@types/google-cloud__spanner", null],\
|
||||
["@types/hdb-pool", null],\
|
||||
["@types/ioredis", null],\
|
||||
["@types/mongodb", null],\
|
||||
["@types/mssql", null],\
|
||||
["@types/mysql2", null],\
|
||||
["@types/oracledb", null],\
|
||||
["@types/pg", null],\
|
||||
["@types/pg-native", null],\
|
||||
["@types/pg-query-stream", null],\
|
||||
["@types/redis", null],\
|
||||
["@types/sap__hana-client", null],\
|
||||
["@types/sql.js", null],\
|
||||
["@types/sqlite3", null],\
|
||||
["@types/ts-node", null],\
|
||||
["@types/typeorm-aurora-data-api-driver", null],\
|
||||
["app-root-path", "npm:3.1.0"],\
|
||||
["better-sqlite3", null],\
|
||||
["buffer", "npm:6.0.3"],\
|
||||
["chalk", "npm:4.1.2"],\
|
||||
["cli-highlight", "npm:2.1.11"],\
|
||||
["date-fns", "npm:2.30.0"],\
|
||||
["debug", "virtual:ac3d8e680759ce54399273724d44e041d6c9b73454d191d411a8c44bb27e22f02aaf6ed9d3ad0ac1c298eac4833cff369c9c7b84c573016112c4f84be2cd8543#npm:4.3.4"],\
|
||||
["dotenv", "npm:16.1.3"],\
|
||||
["glob", "npm:8.1.0"],\
|
||||
["hdb-pool", null],\
|
||||
["ioredis", null],\
|
||||
["mkdirp", "npm:2.1.6"],\
|
||||
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
|
||||
["mssql", null],\
|
||||
["mysql2", "npm:3.3.3"],\
|
||||
["oracledb", null],\
|
||||
["pg", null],\
|
||||
["pg-native", null],\
|
||||
["pg-query-stream", null],\
|
||||
["redis", null],\
|
||||
["reflect-metadata", "npm:0.1.13"],\
|
||||
["sha.js", "npm:2.4.11"],\
|
||||
["sql.js", null],\
|
||||
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
|
||||
["ts-node", null],\
|
||||
["tslib", "npm:2.5.2"],\
|
||||
["typeorm-aurora-data-api-driver", null],\
|
||||
["uuid", "npm:9.0.0"],\
|
||||
["yargs", "npm:17.7.2"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@google-cloud/spanner",\
|
||||
"@sap/hana-client",\
|
||||
"@types/better-sqlite3",\
|
||||
"@types/google-cloud__spanner",\
|
||||
"@types/hdb-pool",\
|
||||
"@types/ioredis",\
|
||||
"@types/mongodb",\
|
||||
"@types/mssql",\
|
||||
"@types/mysql2",\
|
||||
"@types/oracledb",\
|
||||
"@types/pg-native",\
|
||||
"@types/pg-query-stream",\
|
||||
"@types/pg",\
|
||||
"@types/redis",\
|
||||
"@types/sap__hana-client",\
|
||||
"@types/sql.js",\
|
||||
"@types/sqlite3",\
|
||||
"@types/ts-node",\
|
||||
"@types/typeorm-aurora-data-api-driver",\
|
||||
"better-sqlite3",\
|
||||
"hdb-pool",\
|
||||
"ioredis",\
|
||||
"mongodb",\
|
||||
"mssql",\
|
||||
"mysql2",\
|
||||
"oracledb",\
|
||||
"pg-native",\
|
||||
"pg-query-stream",\
|
||||
"pg",\
|
||||
"redis",\
|
||||
"sql.js",\
|
||||
"sqlite3",\
|
||||
"ts-node",\
|
||||
"typeorm-aurora-data-api-driver"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.16", {\
|
||||
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-fc9b7b780b/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
|
||||
"packageDependencies": [\
|
||||
@@ -16191,6 +16403,13 @@ const RAW_RUNTIME_STATE =
|
||||
["webidl-conversions", "npm:3.0.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:7.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/webidl-conversions-npm-7.0.0-e8c8e30c68-bdbe11c68c.zip/node_modules/webidl-conversions/",\
|
||||
"packageDependencies": [\
|
||||
["webidl-conversions", "npm:7.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["webpack", [\
|
||||
@@ -16249,6 +16468,15 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["whatwg-url", [\
|
||||
["npm:11.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/whatwg-url-npm-11.0.0-073529d93a-ee3a532bfb.zip/node_modules/whatwg-url/",\
|
||||
"packageDependencies": [\
|
||||
["whatwg-url", "npm:11.0.0"],\
|
||||
["tr46", "npm:3.0.0"],\
|
||||
["webidl-conversions", "npm:7.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:5.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-bd0cc6b75b.zip/node_modules/whatwg-url/",\
|
||||
"packageDependencies": [\
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -23,6 +23,8 @@ services:
|
||||
environment:
|
||||
DB_TYPE: "${DB_TYPE}"
|
||||
CACHE_TYPE: "${CACHE_TYPE}"
|
||||
SECONDARY_DB_ENABLED: "${SECONDARY_DB_ENABLED}"
|
||||
TRANSITION_MODE_ENABLED: "${TRANSITION_MODE_ENABLED}"
|
||||
container_name: server-ci
|
||||
ports:
|
||||
- 3123:3000
|
||||
@@ -61,6 +63,21 @@ services:
|
||||
networks:
|
||||
- standardnotes_self_hosted
|
||||
|
||||
secondary_db:
|
||||
image: mongo:5.0
|
||||
container_name: secondary_db-ci
|
||||
expose:
|
||||
- 27017
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/mongo:/data/db
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: standardnotes
|
||||
MONGO_INITDB_ROOT_PASSWORD: standardnotes
|
||||
MONGO_INITDB_DATABASE: standardnotes
|
||||
networks:
|
||||
- standardnotes_self_hosted
|
||||
|
||||
cache:
|
||||
image: redis:6.0-alpine
|
||||
container_name: cache-ci
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
# Setup environment variables
|
||||
|
||||
export MODE="self-hosted"
|
||||
|
||||
#########
|
||||
# PORTS #
|
||||
#########
|
||||
@@ -63,6 +65,12 @@ fi
|
||||
if [ -z "$CACHE_TYPE" ]; then
|
||||
export CACHE_TYPE="redis"
|
||||
fi
|
||||
if [ -z "$SECONDARY_DB_ENABLED" ]; then
|
||||
export SECONDARY_DB_ENABLED=false
|
||||
fi
|
||||
if [ -z "$TRANSITION_MODE_ENABLED" ]; then
|
||||
export TRANSITION_MODE_ENABLED=false
|
||||
fi
|
||||
export DB_MIGRATIONS_PATH="dist/migrations/*.js"
|
||||
|
||||
#########
|
||||
|
||||
@@ -147,10 +147,16 @@ LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $SYNCING_SERVER_
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
|
||||
echo "linking topic $SYNCING_SERVER_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
|
||||
echo "linking topic $FILES_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $FILES_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
|
||||
echo "linking topic $SYNCING_SERVER_TOPIC_ARN to queue $AUTH_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $AUTH_QUEUE_ARN)
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
|
||||
echo "linking topic $AUTH_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $AUTH_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
|
||||
echo "linking done:"
|
||||
|
||||
+2
-1
@@ -19,7 +19,8 @@
|
||||
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
|
||||
"postversion": "./scripts/push-tags-one-by-one.sh",
|
||||
"upgrade:snjs": "yarn workspaces foreach --verbose run upgrade:snjs",
|
||||
"e2e": "yarn build packages/home-server && PORT=3123 yarn workspace @standardnotes/home-server start"
|
||||
"e2e": "yarn build packages/home-server && PORT=3123 yarn workspace @standardnotes/home-server start",
|
||||
"start": "yarn build packages/home-server && yarn workspace @standardnotes/home-server start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.2",
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.25.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.16...@standardnotes/analytics@2.25.17) (2023-08-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.15...@standardnotes/analytics@2.25.16) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.14...@standardnotes/analytics@2.25.15) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.13...@standardnotes/analytics@2.25.14) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.12...@standardnotes/analytics@2.25.13) (2023-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.11...@standardnotes/analytics@2.25.12) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.10...@standardnotes/analytics@2.25.11) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.9...@standardnotes/analytics@2.25.10) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.8...@standardnotes/analytics@2.25.9) (2023-08-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.7...@standardnotes/analytics@2.25.8) (2023-08-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.25.8",
|
||||
"version": "2.25.17",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
MODE=microservice # microservice | home-server
|
||||
MODE=microservice # microservice | home-server | self-hosted
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.72.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.72.0...@standardnotes/api-gateway@1.72.1) (2023-08-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow self hosted to use new model of items ([#714](https://github.com/standardnotes/api-gateway/issues/714)) ([aef9254](https://github.com/standardnotes/api-gateway/commit/aef9254713560c00a90a3e84e3cd94417e8f30d2))
|
||||
|
||||
# [1.72.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.1...@standardnotes/api-gateway@1.72.0) (2023-08-24)
|
||||
|
||||
### Features
|
||||
|
||||
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/api-gateway/issues/707)) ([05bb12c](https://github.com/standardnotes/api-gateway/commit/05bb12c97899824f06e6d01d105dec75fc328440))
|
||||
|
||||
## [1.71.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.0...@standardnotes/api-gateway@1.71.1) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/api-gateway/issues/704)) ([34085ac](https://github.com/standardnotes/api-gateway/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
## [1.70.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.4...@standardnotes/api-gateway@1.70.5) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.70.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.3...@standardnotes/api-gateway@1.70.4) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.70.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.2...@standardnotes/api-gateway@1.70.3) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.70.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.1...@standardnotes/api-gateway@1.70.2) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.70.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.0...@standardnotes/api-gateway@1.70.1) (2023-08-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.70.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.3...@standardnotes/api-gateway@1.70.0) (2023-08-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** limit shared vaults creation based on role ([#687](https://github.com/standardnotes/api-gateway/issues/687)) ([19b8921](https://github.com/standardnotes/api-gateway/commit/19b8921f286ff8f88c427e8ddd4512a8d61edb4f))
|
||||
|
||||
## [1.69.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.2...@standardnotes/api-gateway@1.69.3) (2023-08-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.69.3",
|
||||
"version": "1.72.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
@@ -28,16 +27,23 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
}
|
||||
|
||||
const authHeaderValue = request.headers.authorization as string
|
||||
const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
|
||||
const cacheKey = `${authHeaderValue}${
|
||||
sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
|
||||
}`
|
||||
|
||||
try {
|
||||
let crossServiceTokenFetchedFromCache = true
|
||||
let crossServiceToken = null
|
||||
if (this.crossServiceTokenCacheTTL) {
|
||||
crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue)
|
||||
crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
|
||||
}
|
||||
|
||||
if (crossServiceToken === null) {
|
||||
const authResponse = await this.serviceProxy.validateSession(authHeaderValue)
|
||||
if (this.crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken)) {
|
||||
const authResponse = await this.serviceProxy.validateSession({
|
||||
authorization: authHeaderValue,
|
||||
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
|
||||
})
|
||||
|
||||
if (!this.handleSessionValidationResponse(authResponse, response, next)) {
|
||||
return
|
||||
@@ -49,16 +55,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
|
||||
response.locals.authToken = crossServiceToken
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
|
||||
response.locals.freeUser =
|
||||
decodedToken.roles.length === 1 &&
|
||||
decodedToken.roles.find((role) => role.name === RoleName.NAMES.CoreUser) !== undefined
|
||||
const decodedToken = <CrossServiceTokenData>(
|
||||
verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
)
|
||||
|
||||
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
|
||||
await this.crossServiceTokenCache.set({
|
||||
authorizationHeaderValue: authHeaderValue,
|
||||
encodedCrossServiceToken: crossServiceToken,
|
||||
key: cacheKey,
|
||||
encodedCrossServiceToken: response.locals.authToken,
|
||||
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
|
||||
userUuid: decodedToken.user.uuid,
|
||||
})
|
||||
@@ -67,6 +71,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
response.locals.user = decodedToken.user
|
||||
response.locals.session = decodedToken.session
|
||||
response.locals.roles = decodedToken.roles
|
||||
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
@@ -123,4 +128,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
|
||||
return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration)
|
||||
}
|
||||
|
||||
private crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken: string | null) {
|
||||
if (crossServiceToken === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
|
||||
return decodedToken.ongoing_transition === true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
@@ -60,9 +59,6 @@ export class WebSocketAuthMiddleware extends BaseMiddleware {
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
|
||||
response.locals.freeUser =
|
||||
decodedToken.roles.length === 1 &&
|
||||
decodedToken.roles.find((role) => role.name === RoleName.NAMES.CoreUser) !== undefined
|
||||
response.locals.user = decodedToken.user
|
||||
response.locals.roles = decodedToken.roles
|
||||
} catch (error) {
|
||||
|
||||
@@ -34,6 +34,16 @@ export class ItemsController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/transition')
|
||||
async transition(request: Request, response: Response): Promise<void> {
|
||||
await this.serviceProxy.callSyncingServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'items/transition'),
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/:uuid')
|
||||
async getItem(request: Request, response: Response): Promise<void> {
|
||||
await this.serviceProxy.callSyncingServer(
|
||||
|
||||
@@ -80,6 +80,15 @@ export class UsersController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/transition-status', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
async getTransitionStatus(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/transition-status'),
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/:userId/params', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
async getKeyParams(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
|
||||
@@ -12,29 +12,29 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void> {
|
||||
let userAuthHeaders = []
|
||||
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
|
||||
if (userAuthHeadersJSON) {
|
||||
userAuthHeaders = JSON.parse(userAuthHeadersJSON)
|
||||
let userKeys = []
|
||||
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
|
||||
if (userKeysJSON) {
|
||||
userKeys = JSON.parse(userKeysJSON)
|
||||
}
|
||||
userAuthHeaders.push(dto.authorizationHeaderValue)
|
||||
userKeys.push(dto.key)
|
||||
|
||||
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userAuthHeaders))
|
||||
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userKeys))
|
||||
this.crossServiceTokenTTLCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
|
||||
|
||||
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
|
||||
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
|
||||
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
|
||||
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
async get(key: string): Promise<string | null> {
|
||||
this.invalidateExpiredTokens()
|
||||
|
||||
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${key}`)
|
||||
if (!cachedToken) {
|
||||
return null
|
||||
}
|
||||
@@ -43,15 +43,15 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
let userAuthorizationHeaderValues = []
|
||||
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
if (userAuthHeadersJSON) {
|
||||
userAuthorizationHeaderValues = JSON.parse(userAuthHeadersJSON)
|
||||
let userKeyValues = []
|
||||
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
if (userKeysJSON) {
|
||||
userKeyValues = JSON.parse(userKeysJSON)
|
||||
}
|
||||
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
this.crossServiceTokenCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
for (const key of userKeyValues) {
|
||||
this.crossServiceTokenCache.delete(`${this.PREFIX}:${key}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
@@ -12,32 +12,32 @@ export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterf
|
||||
constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void> {
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
|
||||
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.authorizationHeaderValue)
|
||||
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.key)
|
||||
pipeline.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
|
||||
|
||||
pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
|
||||
pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
|
||||
pipeline.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
|
||||
pipeline.expireat(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
async get(key: string): Promise<string | null> {
|
||||
return this.redisClient.get(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
const userAuthorizationHeaderValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
const userKeyValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
for (const key of userKeyValues) {
|
||||
pipeline.del(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export interface CrossServiceTokenCacheInterface {
|
||||
set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void>
|
||||
get(authorizationHeaderValue: string): Promise<string | null>
|
||||
get(key: string): Promise<string | null>
|
||||
invalidate(userUuid: string): Promise<void>
|
||||
}
|
||||
|
||||
@@ -24,14 +24,16 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async validateSession(
|
||||
authorizationHeaderValue: string,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: authorizationHeaderValue,
|
||||
Authorization: headers.authorization,
|
||||
Accept: 'application/json',
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface ServiceProxyInterface {
|
||||
endpointOrMethodIdentifier: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
validateSession(authorizationHeaderValue: string): Promise<{
|
||||
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
|
||||
@@ -6,9 +6,10 @@ import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
|
||||
export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
|
||||
|
||||
async validateSession(
|
||||
authorizationHeaderValue: string,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): 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')
|
||||
@@ -17,7 +18,8 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
const serviceResponse = (await authService.handleRequest(
|
||||
{
|
||||
headers: {
|
||||
authorization: authorizationHeaderValue,
|
||||
authorization: headers.authorization,
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
|
||||
@@ -42,7 +42,8 @@ export class EndpointResolver implements EndpointResolverInterface {
|
||||
// Users Controller
|
||||
['[PATCH]:users/:userId', 'auth.users.update'],
|
||||
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
|
||||
['[PUT]:auth/params', 'auth.users.getKeyParams'],
|
||||
['[GET]:users/params', 'auth.users.getKeyParams'],
|
||||
['[GET]:users/transition-status', 'auth.users.transition-status'],
|
||||
['[DELETE]:users/:userUuid', 'auth.users.delete'],
|
||||
['[POST]:listed', 'auth.users.createListedAccount'],
|
||||
['[POST]:auth', 'auth.users.register'],
|
||||
@@ -58,6 +59,7 @@ export class EndpointResolver implements EndpointResolverInterface {
|
||||
// Syncing Server
|
||||
['[POST]:items/sync', 'sync.items.sync'],
|
||||
['[POST]:items/check-integrity', 'sync.items.check_integrity'],
|
||||
['[POST]:items/transition', 'sync.items.transition'],
|
||||
['[GET]:items/:uuid', 'sync.items.get_item'],
|
||||
// Revisions Controller V2
|
||||
['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
MODE=microservice # microservice | home-server
|
||||
MODE=microservice # microservice | home-server | self-hosted
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
@@ -3,6 +3,82 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.135.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.1...@standardnotes/auth-server@1.135.2) (2023-08-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow self hosted to use new model of items ([#714](https://github.com/standardnotes/server/issues/714)) ([aef9254](https://github.com/standardnotes/server/commit/aef9254713560c00a90a3e84e3cd94417e8f30d2))
|
||||
|
||||
## [1.135.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.0...@standardnotes/auth-server@1.135.1) (2023-08-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** account enumeration with pseudo u2f and mfa ([#709](https://github.com/standardnotes/server/issues/709)) ([bbb35d1](https://github.com/standardnotes/server/commit/bbb35d16fc4f6a57fe774a648fbda13ec64a8865))
|
||||
|
||||
# [1.135.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.134.0...@standardnotes/auth-server@1.135.0) (2023-08-24)
|
||||
|
||||
### Features
|
||||
|
||||
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/server/issues/707)) ([05bb12c](https://github.com/standardnotes/server/commit/05bb12c97899824f06e6d01d105dec75fc328440))
|
||||
|
||||
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.133.0...@standardnotes/auth-server@1.134.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.132.0...@standardnotes/auth-server@1.133.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/server/issues/704)) ([34085ac](https://github.com/standardnotes/server/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
# [1.132.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.5...@standardnotes/auth-server@1.132.0) (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
|
||||
|
||||
## [1.131.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.4...@standardnotes/auth-server@1.131.5) (2023-08-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** passing the invalidate cache header ([#697](https://github.com/standardnotes/server/issues/697)) ([83ad069](https://github.com/standardnotes/server/commit/83ad069c5dd9afa3a6db881f0d8a55a58d0642aa))
|
||||
|
||||
## [1.131.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.3...@standardnotes/auth-server@1.131.4) (2023-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.131.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.2...@standardnotes/auth-server@1.131.3) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.131.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.1...@standardnotes/auth-server@1.131.2) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.131.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.0...@standardnotes/auth-server@1.131.1) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.131.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.130.1...@standardnotes/auth-server@1.131.0) (2023-08-08)
|
||||
|
||||
### Features
|
||||
|
||||
* update storage quota used for user based on shared vault files ([#689](https://github.com/standardnotes/server/issues/689)) ([5311e74](https://github.com/standardnotes/server/commit/5311e7426617da6fc75593dd0fcbff589ca4fc22))
|
||||
|
||||
## [1.130.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.130.0...@standardnotes/auth-server@1.130.1) (2023-08-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** update user agent upon refreshing session token ([#685](https://github.com/standardnotes/server/issues/685)) ([bd5f492](https://github.com/standardnotes/server/commit/bd5f492a733f783c64fa4bc5840b4a9f5c913d3d))
|
||||
|
||||
# [1.130.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.129.0...@standardnotes/auth-server@1.130.0) (2023-08-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** invalidate other sessions for user if the email or password are changed ([#684](https://github.com/standardnotes/server/issues/684)) ([f39d3ac](https://github.com/standardnotes/server/commit/f39d3aca5b7bb9e5f9c1c24cbe2359f30dea835c))
|
||||
|
||||
# [1.129.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.128.1...@standardnotes/auth-server@1.129.0) (2023-08-03)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddTransitionRole1692348191367 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddTransitionRole1692348280258 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.129.0",
|
||||
"version": "1.135.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@ import { GetUserKeyParams } from '../Domain/UseCase/GetUserKeyParams/GetUserKeyP
|
||||
import { UpdateUser } from '../Domain/UseCase/UpdateUser'
|
||||
import { RedisEphemeralSessionRepository } from '../Infra/Redis/RedisEphemeralSessionRepository'
|
||||
import { GetActiveSessionsForUser } from '../Domain/UseCase/GetActiveSessionsForUser'
|
||||
import { DeletePreviousSessionsForUser } from '../Domain/UseCase/DeletePreviousSessionsForUser'
|
||||
import { DeleteOtherSessionsForUser } from '../Domain/UseCase/DeleteOtherSessionsForUser'
|
||||
import { DeleteSessionForUser } from '../Domain/UseCase/DeleteSessionForUser'
|
||||
import { Register } from '../Domain/UseCase/Register'
|
||||
import { LockRepository } from '../Infra/Redis/LockRepository'
|
||||
@@ -253,6 +253,16 @@ import { BaseSessionsController } from '../Infra/InversifyExpressUtils/Base/Base
|
||||
import { Transform } from 'stream'
|
||||
import { ActivatePremiumFeatures } from '../Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures'
|
||||
import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAccountDeletedEventHandler'
|
||||
import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
|
||||
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
|
||||
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
|
||||
import { TransitionStatusRepositoryInterface } from '../Domain/Transition/TransitionStatusRepositoryInterface'
|
||||
import { RedisTransitionStatusRepository } from '../Infra/Redis/RedisTransitionStatusRepository'
|
||||
import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTransitionStatusRepository'
|
||||
import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
|
||||
import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
|
||||
import { GetTransitionStatus } from '../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: {
|
||||
@@ -557,6 +567,9 @@ 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_TRANSITION_MODE_ENABLED)
|
||||
.toConstantValue(env.get('TRANSITION_MODE_ENABLED', true) === 'true')
|
||||
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
@@ -603,6 +616,9 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
|
||||
.toConstantValue(new InMemoryTransitionStatusRepository())
|
||||
} else {
|
||||
container.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository).to(RedisPKCERepository)
|
||||
container.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository).to(LockRepository)
|
||||
@@ -615,6 +631,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<SubscriptionTokenRepositoryInterface>(TYPES.Auth_SubscriptionTokenRepository)
|
||||
.to(RedisSubscriptionTokenRepository)
|
||||
container
|
||||
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
|
||||
.toConstantValue(new RedisTransitionStatusRepository(container.get<Redis>(TYPES.Auth_Redis)))
|
||||
}
|
||||
|
||||
// Services
|
||||
@@ -827,9 +846,7 @@ export class ContainerConfigLoader {
|
||||
container.bind<UpdateUser>(TYPES.Auth_UpdateUser).to(UpdateUser)
|
||||
container.bind<Register>(TYPES.Auth_Register).to(Register)
|
||||
container.bind<GetActiveSessionsForUser>(TYPES.Auth_GetActiveSessionsForUser).to(GetActiveSessionsForUser)
|
||||
container
|
||||
.bind<DeletePreviousSessionsForUser>(TYPES.Auth_DeletePreviousSessionsForUser)
|
||||
.to(DeletePreviousSessionsForUser)
|
||||
container.bind<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser).to(DeleteOtherSessionsForUser)
|
||||
container.bind<DeleteSessionForUser>(TYPES.Auth_DeleteSessionForUser).to(DeleteSessionForUser)
|
||||
container.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials).to(ChangeCredentials)
|
||||
container.bind<GetSettings>(TYPES.Auth_GetSettings).to(GetSettings)
|
||||
@@ -884,6 +901,31 @@ export class ContainerConfigLoader {
|
||||
container.bind<VerifyPredicate>(TYPES.Auth_VerifyPredicate).to(VerifyPredicate)
|
||||
container.bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken).to(CreateCrossServiceToken)
|
||||
container.bind<ProcessUserRequest>(TYPES.Auth_ProcessUserRequest).to(ProcessUserRequest)
|
||||
container
|
||||
.bind<UpdateStorageQuotaUsedForUser>(TYPES.Auth_UpdateStorageQuotaUsedForUser)
|
||||
.toConstantValue(
|
||||
new UpdateStorageQuotaUsedForUser(
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_UserSubscriptionService),
|
||||
container.get(TYPES.Auth_SubscriptionSettingService),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus)
|
||||
.toConstantValue(
|
||||
new UpdateTransitionStatus(
|
||||
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus)
|
||||
.toConstantValue(
|
||||
new GetTransitionStatus(
|
||||
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
),
|
||||
)
|
||||
|
||||
// Controller
|
||||
container
|
||||
@@ -953,8 +995,46 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<UserEmailChangedEventHandler>(TYPES.Auth_UserEmailChangedEventHandler)
|
||||
.to(UserEmailChangedEventHandler)
|
||||
container.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler).to(FileUploadedEventHandler)
|
||||
container.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler).to(FileRemovedEventHandler)
|
||||
container
|
||||
.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler)
|
||||
.toConstantValue(
|
||||
new FileUploadedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultFileUploadedEventHandler>(TYPES.Auth_SharedVaultFileUploadedEventHandler)
|
||||
.toConstantValue(
|
||||
new SharedVaultFileUploadedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultFileMovedEventHandler>(TYPES.Auth_SharedVaultFileMovedEventHandler)
|
||||
.toConstantValue(
|
||||
new SharedVaultFileMovedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
|
||||
.toConstantValue(
|
||||
new FileRemovedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultFileRemovedEventHandler>(TYPES.Auth_SharedVaultFileRemovedEventHandler)
|
||||
.toConstantValue(
|
||||
new SharedVaultFileRemovedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ListedAccountCreatedEventHandler>(TYPES.Auth_ListedAccountCreatedEventHandler)
|
||||
.to(ListedAccountCreatedEventHandler)
|
||||
@@ -987,6 +1067,14 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Auth_TransitionStatusUpdatedEventHandler)
|
||||
.toConstantValue(
|
||||
new TransitionStatusUpdatedEventHandler(
|
||||
container.get<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
|
||||
@@ -1001,7 +1089,10 @@ export class ContainerConfigLoader {
|
||||
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.Auth_SubscriptionReassignedEventHandler)],
|
||||
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
|
||||
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
|
||||
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
|
||||
['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)],
|
||||
['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
|
||||
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
|
||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],
|
||||
['LISTED_ACCOUNT_DELETED', container.get(TYPES.Auth_ListedAccountDeletedEventHandler)],
|
||||
[
|
||||
@@ -1015,6 +1106,7 @@ export class ContainerConfigLoader {
|
||||
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.Auth_PredicateVerificationRequestedEventHandler)],
|
||||
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)],
|
||||
['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)],
|
||||
['TRANSITION_STATUS_UPDATED', container.get(TYPES.Auth_TransitionStatusUpdatedEventHandler)],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
@@ -1119,14 +1211,15 @@ export class ContainerConfigLoader {
|
||||
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
|
||||
.toConstantValue(
|
||||
new BaseUsersController(
|
||||
container.get(TYPES.Auth_UpdateUser),
|
||||
container.get(TYPES.Auth_GetUserKeyParams),
|
||||
container.get(TYPES.Auth_DeleteAccount),
|
||||
container.get(TYPES.Auth_GetUserSubscription),
|
||||
container.get(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get(TYPES.Auth_IncreaseLoginAttempts),
|
||||
container.get(TYPES.Auth_ChangeCredentials),
|
||||
container.get(TYPES.Auth_ControllerContainer),
|
||||
container.get<UpdateUser>(TYPES.Auth_UpdateUser),
|
||||
container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
|
||||
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
|
||||
container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription),
|
||||
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
|
||||
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
|
||||
container.get<ChangeCredentials>(TYPES.Auth_ChangeCredentials),
|
||||
container.get<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus),
|
||||
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -1178,7 +1271,7 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
new BaseSessionController(
|
||||
container.get(TYPES.Auth_DeleteSessionForUser),
|
||||
container.get(TYPES.Auth_DeletePreviousSessionsForUser),
|
||||
container.get(TYPES.Auth_DeleteOtherSessionsForUser),
|
||||
container.get(TYPES.Auth_RefreshSessionToken),
|
||||
container.get(TYPES.Auth_ControllerContainer),
|
||||
),
|
||||
|
||||
@@ -27,6 +27,7 @@ export class Service implements AuthServiceInterface {
|
||||
async activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>> {
|
||||
if (!this.container) {
|
||||
|
||||
@@ -35,6 +35,7 @@ const TYPES = {
|
||||
Auth_AuthenticatorRepository: Symbol.for('Auth_AuthenticatorRepository'),
|
||||
Auth_AuthenticatorChallengeRepository: Symbol.for('Auth_AuthenticatorChallengeRepository'),
|
||||
Auth_CacheEntryRepository: Symbol.for('Auth_CacheEntryRepository'),
|
||||
Auth_TransitionStatusRepository: Symbol.for('Auth_TransitionStatusRepository'),
|
||||
// ORM
|
||||
Auth_ORMOfflineSettingRepository: Symbol.for('Auth_ORMOfflineSettingRepository'),
|
||||
Auth_ORMOfflineUserSubscriptionRepository: Symbol.for('Auth_ORMOfflineUserSubscriptionRepository'),
|
||||
@@ -101,6 +102,7 @@ const TYPES = {
|
||||
Auth_U2F_EXPECTED_ORIGIN: Symbol.for('Auth_U2F_EXPECTED_ORIGIN'),
|
||||
Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'),
|
||||
Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'),
|
||||
Auth_TRANSITION_MODE_ENABLED: Symbol.for('Auth_TRANSITION_MODE_ENABLED'),
|
||||
// use cases
|
||||
Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'),
|
||||
Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'),
|
||||
@@ -113,7 +115,7 @@ const TYPES = {
|
||||
Auth_UpdateUser: Symbol.for('Auth_UpdateUser'),
|
||||
Auth_Register: Symbol.for('Auth_Register'),
|
||||
Auth_GetActiveSessionsForUser: Symbol.for('Auth_GetActiveSessionsForUser'),
|
||||
Auth_DeletePreviousSessionsForUser: Symbol.for('Auth_DeletePreviousSessionsForUser'),
|
||||
Auth_DeleteOtherSessionsForUser: Symbol.for('Auth_DeleteOtherSessionsForUser'),
|
||||
Auth_DeleteSessionForUser: Symbol.for('Auth_DeleteSessionForUser'),
|
||||
Auth_ChangeCredentials: Symbol.for('Auth_ChangePassword'),
|
||||
Auth_GetSettings: Symbol.for('Auth_GetSettings'),
|
||||
@@ -152,6 +154,9 @@ const TYPES = {
|
||||
Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'),
|
||||
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
|
||||
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
|
||||
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
|
||||
Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
|
||||
Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'),
|
||||
// Handlers
|
||||
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
|
||||
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
|
||||
@@ -165,7 +170,10 @@ const TYPES = {
|
||||
Auth_ExtensionKeyGrantedEventHandler: Symbol.for('Auth_ExtensionKeyGrantedEventHandler'),
|
||||
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
|
||||
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
|
||||
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
|
||||
Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
|
||||
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
|
||||
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
|
||||
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),
|
||||
Auth_ListedAccountDeletedEventHandler: Symbol.for('Auth_ListedAccountDeletedEventHandler'),
|
||||
Auth_UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for(
|
||||
@@ -177,6 +185,7 @@ const TYPES = {
|
||||
Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'),
|
||||
Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
|
||||
Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
|
||||
Auth_TransitionStatusUpdatedEventHandler: Symbol.for('Auth_TransitionStatusUpdatedEventHandler'),
|
||||
// Services
|
||||
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
||||
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('AuthResponseFactory20161215', () => {
|
||||
})
|
||||
|
||||
it('should create a 20161215 auth response', async () => {
|
||||
const response = await createFactory().createResponse({
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20161215',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -38,7 +38,7 @@ describe('AuthResponseFactory20161215', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
expect(result.response).toEqual({
|
||||
user: { foo: 'bar' },
|
||||
token: 'foobar',
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import { User } from '../User/User'
|
||||
import { AuthResponse20161215 } from './AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from './AuthResponse20200115'
|
||||
import { AuthResponseFactoryInterface } from './AuthResponseFactoryInterface'
|
||||
import { Session } from '../Session/Session'
|
||||
|
||||
@injectable()
|
||||
export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface {
|
||||
@@ -26,7 +27,7 @@ export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface
|
||||
userAgent: string
|
||||
ephemeralSession: boolean
|
||||
readonlyAccess: boolean
|
||||
}): Promise<AuthResponse20161215 | AuthResponse20200115> {
|
||||
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }> {
|
||||
this.logger.debug(`Creating JWT auth response for user ${dto.user.uuid}`)
|
||||
|
||||
const data: SessionTokenData = {
|
||||
@@ -39,12 +40,14 @@ export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface
|
||||
this.logger.debug(`Created JWT token for user ${dto.user.uuid}: ${token}`)
|
||||
|
||||
return {
|
||||
user: this.userProjector.projectSimple(dto.user) as {
|
||||
uuid: string
|
||||
email: string
|
||||
protocolVersion: ProtocolVersion
|
||||
response: {
|
||||
user: this.userProjector.projectSimple(dto.user) as {
|
||||
uuid: string
|
||||
email: string
|
||||
protocolVersion: ProtocolVersion
|
||||
},
|
||||
token,
|
||||
},
|
||||
token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('AuthResponseFactory20190520', () => {
|
||||
})
|
||||
|
||||
it('should create a 20161215 auth response', async () => {
|
||||
const response = await createFactory().createResponse({
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20161215',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -37,7 +37,7 @@ describe('AuthResponseFactory20190520', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
expect(result.response).toEqual({
|
||||
user: { foo: 'bar' },
|
||||
token: 'foobar',
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import { User } from '../User/User'
|
||||
import { AuthResponseFactory20200115 } from './AuthResponseFactory20200115'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { Session } from '../Session/Session'
|
||||
|
||||
describe('AuthResponseFactory20200115', () => {
|
||||
let sessionService: SessionServiceInterface
|
||||
@@ -48,8 +49,12 @@ describe('AuthResponseFactory20200115', () => {
|
||||
}
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.createNewSessionForUser = jest.fn().mockReturnValue(sessionPayload)
|
||||
sessionService.createNewEphemeralSessionForUser = jest.fn().mockReturnValue(sessionPayload)
|
||||
sessionService.createNewSessionForUser = jest
|
||||
.fn()
|
||||
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, session: {} as jest.Mocked<Session> })
|
||||
sessionService.createNewEphemeralSessionForUser = jest
|
||||
.fn()
|
||||
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, session: {} as jest.Mocked<Session> })
|
||||
|
||||
keyParamsFactory = {} as jest.Mocked<KeyParamsFactoryInterface>
|
||||
keyParamsFactory.create = jest.fn().mockReturnValue({
|
||||
@@ -76,7 +81,7 @@ describe('AuthResponseFactory20200115', () => {
|
||||
it('should create a 20161215 auth response if user does not support sessions', async () => {
|
||||
user.supportsSessions = jest.fn().mockReturnValue(false)
|
||||
|
||||
const response = await createFactory().createResponse({
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20161215',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -84,7 +89,7 @@ describe('AuthResponseFactory20200115', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
expect(result.response).toEqual({
|
||||
user: { foo: 'bar' },
|
||||
token: expect.any(String),
|
||||
})
|
||||
@@ -93,7 +98,7 @@ describe('AuthResponseFactory20200115', () => {
|
||||
it('should create a 20200115 auth response', async () => {
|
||||
user.supportsSessions = jest.fn().mockReturnValue(true)
|
||||
|
||||
const response = await createFactory().createResponse({
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20200115',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -101,7 +106,7 @@ describe('AuthResponseFactory20200115', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
expect(result.response).toEqual({
|
||||
key_params: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
@@ -124,7 +129,7 @@ describe('AuthResponseFactory20200115', () => {
|
||||
domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test'))
|
||||
user.supportsSessions = jest.fn().mockReturnValue(true)
|
||||
|
||||
const response = await createFactory().createResponse({
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20200115',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -132,7 +137,7 @@ describe('AuthResponseFactory20200115', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
expect(result.response).toEqual({
|
||||
key_params: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
@@ -153,7 +158,7 @@ describe('AuthResponseFactory20200115', () => {
|
||||
it('should create a 20200115 auth response with an ephemeral session', async () => {
|
||||
user.supportsSessions = jest.fn().mockReturnValue(true)
|
||||
|
||||
const response = await createFactory().createResponse({
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20200115',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -161,7 +166,7 @@ describe('AuthResponseFactory20200115', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
expect(result.response).toEqual({
|
||||
key_params: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
@@ -183,11 +188,14 @@ describe('AuthResponseFactory20200115', () => {
|
||||
user.supportsSessions = jest.fn().mockReturnValue(true)
|
||||
|
||||
sessionService.createNewSessionForUser = jest.fn().mockReturnValue({
|
||||
...sessionPayload,
|
||||
readonly_access: true,
|
||||
sessionHttpRepresentation: {
|
||||
...sessionPayload,
|
||||
readonly_access: true,
|
||||
},
|
||||
session: {} as jest.Mocked<Session>,
|
||||
})
|
||||
|
||||
const response = await createFactory().createResponse({
|
||||
const result = await createFactory().createResponse({
|
||||
user,
|
||||
apiVersion: '20200115',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -195,7 +203,7 @@ describe('AuthResponseFactory20200115', () => {
|
||||
readonlyAccess: true,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
expect(result.response).toEqual({
|
||||
key_params: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
|
||||
|
||||
import { AuthResponse20161215 } from './AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from './AuthResponse20200115'
|
||||
import { Session } from '../Session/Session'
|
||||
|
||||
@injectable()
|
||||
export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
@@ -40,21 +41,28 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
userAgent: string
|
||||
ephemeralSession: boolean
|
||||
readonlyAccess: boolean
|
||||
}): Promise<AuthResponse20161215 | AuthResponse20200115> {
|
||||
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }> {
|
||||
if (!dto.user.supportsSessions()) {
|
||||
this.logger.debug(`User ${dto.user.uuid} does not support sessions. Falling back to JWT auth response`)
|
||||
|
||||
return super.createResponse(dto)
|
||||
}
|
||||
|
||||
const sessionPayload = await this.createSession(dto)
|
||||
const sessionCreationResult = await this.createSession(dto)
|
||||
|
||||
this.logger.debug('Created session payload for user %s: %O', dto.user.uuid, sessionPayload)
|
||||
this.logger.debug(
|
||||
'Created session payload for user %s: %O',
|
||||
dto.user.uuid,
|
||||
sessionCreationResult.sessionHttpRepresentation,
|
||||
)
|
||||
|
||||
return {
|
||||
session: sessionPayload,
|
||||
key_params: this.keyParamsFactory.create(dto.user, true),
|
||||
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
|
||||
response: {
|
||||
session: sessionCreationResult.sessionHttpRepresentation,
|
||||
key_params: this.keyParamsFactory.create(dto.user, true),
|
||||
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
|
||||
},
|
||||
session: sessionCreationResult.session,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +72,12 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
userAgent: string
|
||||
ephemeralSession: boolean
|
||||
readonlyAccess: boolean
|
||||
}): Promise<SessionBody> {
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
|
||||
if (dto.ephemeralSession) {
|
||||
return this.sessionService.createNewEphemeralSessionForUser(dto)
|
||||
}
|
||||
|
||||
const session = this.sessionService.createNewSessionForUser(dto)
|
||||
const sessionCreationResult = await this.sessionService.createNewSessionForUser(dto)
|
||||
|
||||
try {
|
||||
await this.domainEventPublisher.publish(
|
||||
@@ -79,6 +87,6 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
|
||||
this.logger.error(`Failed to publish session created event: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
return session
|
||||
return sessionCreationResult
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Session } from '../Session/Session'
|
||||
import { User } from '../User/User'
|
||||
import { AuthResponse20161215 } from './AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from './AuthResponse20200115'
|
||||
@@ -9,5 +10,5 @@ export interface AuthResponseFactoryInterface {
|
||||
userAgent: string
|
||||
ephemeralSession: boolean
|
||||
readonlyAccess: boolean
|
||||
}): Promise<AuthResponse20161215 | AuthResponse20200115>
|
||||
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }>
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('AuthenticationMethodResolver', () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.getSessionFromToken = jest.fn()
|
||||
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session: undefined, isEphemeral: false })
|
||||
sessionService.getRevokedSessionFromToken = jest.fn()
|
||||
sessionService.markRevokedSessionAsReceived = jest.fn().mockReturnValue(revokedSession)
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('AuthenticationMethodResolver', () => {
|
||||
})
|
||||
|
||||
it('should resolve session authentication method', async () => {
|
||||
sessionService.getSessionFromToken = jest.fn().mockReturnValue(session)
|
||||
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
|
||||
|
||||
expect(await createResolver().resolve('test')).toEqual({
|
||||
session,
|
||||
@@ -80,7 +80,9 @@ describe('AuthenticationMethodResolver', () => {
|
||||
})
|
||||
|
||||
it('should not resolve session authentication method with invalid user uuid on session', async () => {
|
||||
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ userUuid: 'invalid' })
|
||||
sessionService.getSessionFromToken = jest
|
||||
.fn()
|
||||
.mockReturnValue({ session: { userUuid: 'invalid' }, isEphemeral: false })
|
||||
|
||||
expect(await createResolver().resolve('test')).toBeUndefined
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
|
||||
}
|
||||
}
|
||||
|
||||
const session = await this.sessionService.getSessionFromToken(token)
|
||||
const { session } = await this.sessionService.getSessionFromToken(token)
|
||||
if (session) {
|
||||
this.logger.debug('Token decoded successfully. Session found.')
|
||||
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { FileRemovedEventHandler } from './FileRemovedEventHandler'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
|
||||
describe('FileRemovedEventHandler', () => {
|
||||
let userSubscriptionService: UserSubscriptionServiceInterface
|
||||
let logger: Logger
|
||||
let regularUser: User
|
||||
let sharedUser: User
|
||||
let event: FileRemovedEvent
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let regularSubscription: UserSubscription
|
||||
let sharedSubscription: UserSubscription
|
||||
|
||||
const createHandler = () => new FileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
regularUser = {
|
||||
uuid: '123',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
sharedUser = {
|
||||
uuid: '234',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
regularSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
user: Promise.resolve(regularUser),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
sharedSubscription = {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
user: Promise.resolve(sharedUser),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription: null })
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
subscriptionSettingService.createOrReplace = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<FileRemovedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
fileByteSize: 123,
|
||||
filePath: '1-2-3/2-3-4',
|
||||
fileName: '2-3-4',
|
||||
regularSubscriptionUuid: '4-5-6',
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should do nothing a bytes used setting does not exist', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if a user subscription is not found', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 345,
|
||||
})
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update a bytes used setting', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 345,
|
||||
})
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
name: 'FILE_UPLOAD_BYTES_USED',
|
||||
sensitive: false,
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user: regularUser,
|
||||
userSubscription: {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: 'regular',
|
||||
user: Promise.resolve(regularUser),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should update a bytes used setting on both shared and regular subscription', async () => {
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription })
|
||||
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 345,
|
||||
})
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenNthCalledWith(1, {
|
||||
props: {
|
||||
name: 'FILE_UPLOAD_BYTES_USED',
|
||||
sensitive: false,
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user: regularUser,
|
||||
userSubscription: {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: 'regular',
|
||||
user: Promise.resolve(regularUser),
|
||||
},
|
||||
})
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenNthCalledWith(2, {
|
||||
props: {
|
||||
name: 'FILE_UPLOAD_BYTES_USED',
|
||||
sensitive: false,
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user: sharedUser,
|
||||
userSubscription: {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: 'shared',
|
||||
user: Promise.resolve(sharedUser),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,63 +1,19 @@
|
||||
import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
@injectable()
|
||||
export class FileRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingService)
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: FileRemovedEvent): Promise<void> {
|
||||
const { regularSubscription, sharedSubscription } =
|
||||
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(event.payload.userUuid)
|
||||
if (regularSubscription === null) {
|
||||
this.logger.warn(`Could not find regular user subscription for user with uuid: ${event.payload.userUuid}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, event.payload.fileByteSize)
|
||||
|
||||
if (sharedSubscription !== null) {
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, event.payload.fileByteSize)
|
||||
}
|
||||
}
|
||||
|
||||
private async updateUploadBytesUsedSetting(subscription: UserSubscription, byteSize: number): Promise<void> {
|
||||
const user = await subscription.user
|
||||
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: user.uuid,
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
|
||||
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
bytesUsed: -event.payload.fileByteSize,
|
||||
})
|
||||
if (bytesUsedSetting === null) {
|
||||
this.logger.warn(`Could not find bytes used setting for user with uuid: ${user.uuid}`)
|
||||
|
||||
return
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
|
||||
}
|
||||
|
||||
const bytesUsed = bytesUsedSetting.value as string
|
||||
|
||||
await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
unencryptedValue: (+bytesUsed - byteSize).toString(),
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,19 @@
|
||||
import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
@injectable()
|
||||
export class FileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingService)
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: FileUploadedEvent): Promise<void> {
|
||||
const userUuidOrError = Uuid.create(event.payload.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
this.logger.warn(userUuidOrError.getError())
|
||||
|
||||
return
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const { regularSubscription, sharedSubscription } =
|
||||
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(userUuid.value)
|
||||
if (regularSubscription === null) {
|
||||
this.logger.warn(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, user, event.payload.fileByteSize)
|
||||
|
||||
if (sharedSubscription !== null) {
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, user, event.payload.fileByteSize)
|
||||
}
|
||||
}
|
||||
|
||||
private async updateUploadBytesUsedSetting(
|
||||
subscription: UserSubscription,
|
||||
user: User,
|
||||
byteSize: number,
|
||||
): Promise<void> {
|
||||
let bytesUsed = '0'
|
||||
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: (await subscription.user).uuid,
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
|
||||
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
bytesUsed: event.payload.fileByteSize,
|
||||
})
|
||||
if (bytesUsedSetting !== null) {
|
||||
bytesUsed = bytesUsedSetting.value as string
|
||||
}
|
||||
|
||||
await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
unencryptedValue: (+bytesUsed + byteSize).toString(),
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
},
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { DomainEventHandlerInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
export class SharedVaultFileMovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: SharedVaultFileMovedEvent): Promise<void> {
|
||||
const subtractResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.from.ownerUuid,
|
||||
bytesUsed: -event.payload.fileByteSize,
|
||||
})
|
||||
|
||||
if (subtractResult.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${subtractResult.getError()}`)
|
||||
}
|
||||
|
||||
const addResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.to.ownerUuid,
|
||||
bytesUsed: event.payload.fileByteSize,
|
||||
})
|
||||
|
||||
if (addResult.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${addResult.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { DomainEventHandlerInterface, SharedVaultFileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
export class SharedVaultFileRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: SharedVaultFileRemovedEvent): Promise<void> {
|
||||
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.vaultOwnerUuid,
|
||||
bytesUsed: -event.payload.fileByteSize,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { DomainEventHandlerInterface, SharedVaultFileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
export class SharedVaultFileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: SharedVaultFileUploadedEvent): Promise<void> {
|
||||
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.vaultOwnerUuid,
|
||||
bytesUsed: event.payload.fileByteSize,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.removeUserRole = jest.fn()
|
||||
roleService.removeUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
})
|
||||
|
||||
it('should update subscription ends at', async () => {
|
||||
@@ -108,7 +108,7 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
|
||||
for (const userSubscription of userSubscriptions) {
|
||||
await this.roleService.removeUserRole(await userSubscription.user, subscriptionName)
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
roleService.setOfflineUserRole = jest.fn()
|
||||
|
||||
subscriptionExpiresAt = timestamp + 365 * 1000
|
||||
@@ -106,7 +106,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should update user default settings', async () => {
|
||||
@@ -162,7 +162,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -171,7 +171,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,7 +70,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
|
||||
await this.roleService.addUserRole(user, subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
|
||||
private async createSubscription(
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('SubscriptionReassignedEventHandler', () => {
|
||||
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
subscriptionExpiresAt = timestamp + 365 * 1000
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('SubscriptionReassignedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should create subscription', async () => {
|
||||
@@ -146,7 +146,7 @@ describe('SubscriptionReassignedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('SubscriptionReassignedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -67,7 +67,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
|
||||
await this.roleService.addUserRole(user, subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
|
||||
private async createSubscription(
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.removeUserRole = jest.fn()
|
||||
roleService.removeUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
|
||||
})
|
||||
|
||||
it('should update subscription ends at', async () => {
|
||||
@@ -109,7 +109,7 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
|
||||
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
|
||||
for (const userSubscription of userSubscriptions) {
|
||||
await this.roleService.removeUserRole(await userSubscription.user, subscriptionName)
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
roleService.setOfflineUserRole = jest.fn()
|
||||
|
||||
timestamp = dayjs.utc().valueOf()
|
||||
@@ -107,7 +107,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should update the offline user role', async () => {
|
||||
@@ -123,7 +123,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,7 +73,7 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
for (const userSubscription of userSubscriptions) {
|
||||
const user = await userSubscription.user
|
||||
|
||||
await this.roleService.addUserRole(user, subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
})
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
roleService.setOfflineUserRole = jest.fn()
|
||||
|
||||
subscriptionExpiresAt = timestamp + 365 * 1000
|
||||
@@ -121,7 +121,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
it('should update the user role', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
|
||||
})
|
||||
|
||||
it('should update user default settings', async () => {
|
||||
@@ -243,7 +243,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -252,7 +252,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -93,7 +93,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
event.payload.timestamp,
|
||||
)
|
||||
|
||||
await this.roleService.addUserRole(user, event.payload.subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { DomainEventHandlerInterface, TransitionStatusUpdatedEvent } from '@standardnotes/domain-events'
|
||||
import { UpdateTransitionStatus } from '../UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private updateTransitionStatusUseCase: UpdateTransitionStatus, private logger: Logger) {}
|
||||
|
||||
async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
|
||||
const result = await this.updateTransitionStatusUseCase.execute({
|
||||
status: event.payload.status,
|
||||
userUuid: event.payload.userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update transition status for user ${event.payload.userUuid}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { RoleName, Uuid } from '@standardnotes/domain-core'
|
||||
import { Role } from '../Role/Role'
|
||||
|
||||
import { ClientServiceInterface } from '../Client/ClientServiceInterface'
|
||||
@@ -81,9 +81,44 @@ describe('RoleService', () => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
describe('adding roles', () => {
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([basicRole]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
})
|
||||
|
||||
it('should add a role to a user', async () => {
|
||||
await createService().addRoleToUser(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
)
|
||||
|
||||
user.roles = Promise.resolve([basicRole, proRole])
|
||||
expect(userRepository.save).toHaveBeenCalledWith(user)
|
||||
})
|
||||
|
||||
it('should not add a role to a user if the user could not be found', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createService().addRoleToUser(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
)
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('adding roles based on subscription', () => {
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
@@ -96,7 +131,7 @@ describe('RoleService', () => {
|
||||
})
|
||||
|
||||
it('should add role to user', async () => {
|
||||
await createService().addUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
|
||||
user.roles = Promise.resolve([basicRole, proRole])
|
||||
@@ -112,7 +147,7 @@ describe('RoleService', () => {
|
||||
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
await createService().addUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
|
||||
expect(userRepository.save).toHaveBeenCalledWith(user)
|
||||
@@ -120,7 +155,7 @@ describe('RoleService', () => {
|
||||
})
|
||||
|
||||
it('should send websockets event', async () => {
|
||||
await createService().addUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user)
|
||||
})
|
||||
@@ -128,14 +163,14 @@ describe('RoleService', () => {
|
||||
it('should not add role if no role name exists for subscription name', async () => {
|
||||
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined)
|
||||
|
||||
await createService().addUserRole(user, 'test' as SubscriptionName)
|
||||
await createService().addUserRoleBasedOnSubscription(user, 'test' as SubscriptionName)
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not add role if no role exists for role name', async () => {
|
||||
roleRepository.findOneByName = jest.fn().mockReturnValue(null)
|
||||
await createService().addUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -169,7 +204,7 @@ describe('RoleService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('removing roles', () => {
|
||||
describe('removing roles based on subscription', () => {
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
@@ -182,13 +217,13 @@ describe('RoleService', () => {
|
||||
})
|
||||
|
||||
it('should remove role from user', async () => {
|
||||
await createService().removeUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith(user)
|
||||
})
|
||||
|
||||
it('should send websockets event', async () => {
|
||||
await createService().removeUserRole(user, SubscriptionName.ProPlan)
|
||||
await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
|
||||
|
||||
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user)
|
||||
})
|
||||
@@ -196,7 +231,7 @@ describe('RoleService', () => {
|
||||
it('should not remove role if role name does not exist for subscription name', async () => {
|
||||
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined)
|
||||
|
||||
await createService().removeUserRole(user, 'test' as SubscriptionName)
|
||||
await createService().removeUserRoleBasedOnSubscription(user, 'test' as SubscriptionName)
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import { RoleToSubscriptionMapInterface } from './RoleToSubscriptionMapInterface
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { Role } from './Role'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { RoleName, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
@injectable()
|
||||
export class RoleService implements RoleServiceInterface {
|
||||
@@ -54,7 +54,18 @@ export class RoleService implements RoleServiceInterface {
|
||||
return false
|
||||
}
|
||||
|
||||
async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
async addRoleToUser(userUuid: Uuid, roleName: RoleName): Promise<void> {
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (user === null) {
|
||||
this.logger.error(`Could not find user with uuid ${userUuid.value} to add role ${roleName.value}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.addToExistingRoles(user, roleName.value)
|
||||
}
|
||||
|
||||
async addUserRoleBasedOnSubscription(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
|
||||
|
||||
if (roleName === undefined) {
|
||||
@@ -62,25 +73,7 @@ export class RoleService implements RoleServiceInterface {
|
||||
return
|
||||
}
|
||||
|
||||
const role = await this.roleRepository.findOneByName(roleName)
|
||||
|
||||
if (role === null) {
|
||||
this.logger.warn(`Could not find role for role name: ${roleName}`)
|
||||
return
|
||||
}
|
||||
|
||||
const rolesMap = new Map<string, Role>()
|
||||
const currentRoles = await user.roles
|
||||
for (const currentRole of currentRoles) {
|
||||
rolesMap.set(currentRole.name, currentRole)
|
||||
}
|
||||
if (!rolesMap.has(role.name)) {
|
||||
rolesMap.set(role.name, role)
|
||||
}
|
||||
|
||||
user.roles = Promise.resolve([...rolesMap.values()])
|
||||
await this.userRepository.save(user)
|
||||
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
|
||||
await this.addToExistingRoles(user, roleName)
|
||||
}
|
||||
|
||||
async setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void> {
|
||||
@@ -107,7 +100,7 @@ export class RoleService implements RoleServiceInterface {
|
||||
await this.offlineUserSubscriptionRepository.save(offlineUserSubscription)
|
||||
}
|
||||
|
||||
async removeUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
async removeUserRoleBasedOnSubscription(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
|
||||
|
||||
if (roleName === undefined) {
|
||||
@@ -120,4 +113,27 @@ export class RoleService implements RoleServiceInterface {
|
||||
await this.userRepository.save(user)
|
||||
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
|
||||
}
|
||||
|
||||
private async addToExistingRoles(user: User, roleNameString: string): Promise<void> {
|
||||
const role = await this.roleRepository.findOneByName(roleNameString)
|
||||
|
||||
if (role === null) {
|
||||
this.logger.warn(`Could not find role for role name: ${roleNameString}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const rolesMap = new Map<string, Role>()
|
||||
const currentRoles = await user.roles
|
||||
for (const currentRole of currentRoles) {
|
||||
rolesMap.set(currentRole.name, currentRole)
|
||||
}
|
||||
if (!rolesMap.has(role.name)) {
|
||||
rolesMap.set(role.name, role)
|
||||
}
|
||||
|
||||
user.roles = Promise.resolve([...rolesMap.values()])
|
||||
await this.userRepository.save(user)
|
||||
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { RoleName, Uuid } from '@standardnotes/domain-core'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { User } from '../User/User'
|
||||
|
||||
export interface RoleServiceInterface {
|
||||
addUserRole(user: User, subscriptionName: string): Promise<void>
|
||||
addRoleToUser(userUuid: Uuid, roleName: RoleName): Promise<void>
|
||||
addUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
|
||||
setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void>
|
||||
removeUserRole(user: User, subscriptionName: string): Promise<void>
|
||||
removeUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
|
||||
userHasPermission(userUuid: string, permissionName: PermissionName): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -4,13 +4,6 @@ export interface EphemeralSessionRepositoryInterface {
|
||||
findOneByUuid(uuid: string): Promise<EphemeralSession | null>
|
||||
findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<EphemeralSession | null>
|
||||
findAllByUserUuid(userUuid: string): Promise<Array<EphemeralSession>>
|
||||
updateTokensAndExpirationDates(
|
||||
uuid: string,
|
||||
hashedAccessToken: string,
|
||||
hashedRefreshToken: string,
|
||||
accessExpiration: Date,
|
||||
refreshExpiration: Date,
|
||||
): Promise<void>
|
||||
deleteOne(uuid: string, userUuid: string): Promise<void>
|
||||
save(ephemeralSession: EphemeralSession): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Session } from './Session'
|
||||
|
||||
export interface SessionRepositoryInterface {
|
||||
@@ -5,10 +7,8 @@ export interface SessionRepositoryInterface {
|
||||
findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Session | null>
|
||||
findAllByRefreshExpirationAndUserUuid(userUuid: string): Promise<Array<Session>>
|
||||
findAllByUserUuid(userUuid: string): Promise<Array<Session>>
|
||||
deleteAllByUserUuid(userUuid: string, currentSessionUuid: string): Promise<void>
|
||||
deleteAllByUserUuidExceptOne(dto: { userUuid: Uuid; currentSessionUuid: Uuid }): Promise<void>
|
||||
deleteOneByUuid(uuid: string): Promise<void>
|
||||
updateHashedTokens(uuid: string, hashedAccessToken: string, hashedRefreshToken: string): Promise<void>
|
||||
updatedTokenExpirationDates(uuid: string, accessExpiration: Date, refreshExpiration: Date): Promise<void>
|
||||
save(session: Session): Promise<Session>
|
||||
remove(session: Session): Promise<Session>
|
||||
clearUserAgentByUserUuid(userUuid: string): Promise<void>
|
||||
|
||||
@@ -24,8 +24,8 @@ describe('SessionService', () => {
|
||||
let sessionRepository: SessionRepositoryInterface
|
||||
let ephemeralSessionRepository: EphemeralSessionRepositoryInterface
|
||||
let revokedSessionRepository: RevokedSessionRepositoryInterface
|
||||
let session: Session
|
||||
let ephemeralSession: EphemeralSession
|
||||
let existingSession: Session
|
||||
let existingEphemeralSession: EphemeralSession
|
||||
let revokedSession: RevokedSession
|
||||
let settingService: SettingServiceInterface
|
||||
let deviceDetector: UAParser
|
||||
@@ -54,14 +54,14 @@ describe('SessionService', () => {
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
session.uuid = '2e1e43'
|
||||
session.userUuid = '1-2-3'
|
||||
session.userAgent = 'Chrome'
|
||||
session.apiVersion = ApiVersion.v20200115
|
||||
session.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
session.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
session.readonlyAccess = false
|
||||
existingSession = {} as jest.Mocked<Session>
|
||||
existingSession.uuid = '2e1e43'
|
||||
existingSession.userUuid = '1-2-3'
|
||||
existingSession.userAgent = 'Chrome'
|
||||
existingSession.apiVersion = ApiVersion.v20200115
|
||||
existingSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
existingSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
existingSession.readonlyAccess = false
|
||||
|
||||
revokedSession = {} as jest.Mocked<RevokedSession>
|
||||
revokedSession.uuid = '2e1e43'
|
||||
@@ -69,9 +69,7 @@ describe('SessionService', () => {
|
||||
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
|
||||
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
sessionRepository.deleteOneByUuid = jest.fn()
|
||||
sessionRepository.save = jest.fn().mockReturnValue(session)
|
||||
sessionRepository.updateHashedTokens = jest.fn()
|
||||
sessionRepository.updatedTokenExpirationDates = jest.fn()
|
||||
sessionRepository.save = jest.fn().mockReturnValue(existingSession)
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
@@ -79,17 +77,18 @@ describe('SessionService', () => {
|
||||
ephemeralSessionRepository = {} as jest.Mocked<EphemeralSessionRepositoryInterface>
|
||||
ephemeralSessionRepository.save = jest.fn()
|
||||
ephemeralSessionRepository.findOneByUuid = jest.fn()
|
||||
ephemeralSessionRepository.updateTokensAndExpirationDates = jest.fn()
|
||||
ephemeralSessionRepository.deleteOne = jest.fn()
|
||||
|
||||
revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
|
||||
revokedSessionRepository.save = jest.fn()
|
||||
|
||||
ephemeralSession = {} as jest.Mocked<EphemeralSession>
|
||||
ephemeralSession.uuid = '2-3-4'
|
||||
ephemeralSession.userAgent = 'Mozilla Firefox'
|
||||
ephemeralSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
ephemeralSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
existingEphemeralSession = {} as jest.Mocked<EphemeralSession>
|
||||
existingEphemeralSession.uuid = '2-3-4'
|
||||
existingEphemeralSession.userUuid = '1-2-3'
|
||||
existingEphemeralSession.userAgent = 'Mozilla Firefox'
|
||||
existingEphemeralSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
existingEphemeralSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
existingEphemeralSession.readonlyAccess = false
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertStringDateToMilliseconds = jest.fn().mockReturnValue(123)
|
||||
@@ -138,7 +137,7 @@ describe('SessionService', () => {
|
||||
})
|
||||
|
||||
it('should refresh access and refresh tokens for a session', async () => {
|
||||
expect(await createService().refreshTokens(session)).toEqual({
|
||||
expect(await createService().refreshTokens({ session: existingSession, isEphemeral: false })).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
@@ -146,15 +145,28 @@ describe('SessionService', () => {
|
||||
readonly_access: false,
|
||||
})
|
||||
|
||||
expect(sessionRepository.updateHashedTokens).toHaveBeenCalled()
|
||||
expect(sessionRepository.updatedTokenExpirationDates).toHaveBeenCalled()
|
||||
expect(sessionRepository.save).toHaveBeenCalled()
|
||||
expect(ephemeralSessionRepository.save).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(sessionRepository.save).not.toHaveBeenCalled()
|
||||
expect(ephemeralSessionRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create new session for a user', async () => {
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
|
||||
const sessionPayload = await createService().createNewSessionForUser({
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -176,7 +188,7 @@ describe('SessionService', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(sessionPayload).toEqual({
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
@@ -190,7 +202,7 @@ describe('SessionService', () => {
|
||||
user.email = 'demo@standardnotes.com'
|
||||
user.uuid = '123'
|
||||
|
||||
const sessionPayload = await createService().createNewSessionForUser({
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -212,7 +224,7 @@ describe('SessionService', () => {
|
||||
readonlyAccess: true,
|
||||
})
|
||||
|
||||
expect(sessionPayload).toEqual({
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
@@ -229,7 +241,7 @@ describe('SessionService', () => {
|
||||
value: LogSessionUserAgentOption.Disabled,
|
||||
} as jest.Mocked<Setting>)
|
||||
|
||||
const sessionPayload = await createService().createNewSessionForUser({
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -250,7 +262,7 @@ describe('SessionService', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(sessionPayload).toEqual({
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
@@ -305,7 +317,7 @@ describe('SessionService', () => {
|
||||
user.uuid = '123'
|
||||
user.email = 'test@test.te'
|
||||
|
||||
const sessionPayload = await createService().createNewSessionForUser({
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -317,7 +329,7 @@ describe('SessionService', () => {
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: null,
|
||||
})
|
||||
expect(sessionPayload).toEqual({
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
@@ -333,7 +345,7 @@ describe('SessionService', () => {
|
||||
user.uuid = '123'
|
||||
user.email = 'test@test.te'
|
||||
|
||||
const sessionPayload = await createService().createNewSessionForUser({
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -345,7 +357,7 @@ describe('SessionService', () => {
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: null,
|
||||
})
|
||||
expect(sessionPayload).toEqual({
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
@@ -361,7 +373,7 @@ describe('SessionService', () => {
|
||||
user.uuid = '123'
|
||||
user.email = 'test@test.te'
|
||||
|
||||
const sessionPayload = await createService().createNewSessionForUser({
|
||||
const result = await createService().createNewSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -373,7 +385,7 @@ describe('SessionService', () => {
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: null,
|
||||
})
|
||||
expect(sessionPayload).toEqual({
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
@@ -386,7 +398,7 @@ describe('SessionService', () => {
|
||||
const user = {} as jest.Mocked<User>
|
||||
user.uuid = '123'
|
||||
|
||||
const sessionPayload = await createService().createNewEphemeralSessionForUser({
|
||||
const result = await createService().createNewEphemeralSessionForUser({
|
||||
user,
|
||||
apiVersion: '003',
|
||||
userAgent: 'Google Chrome',
|
||||
@@ -408,7 +420,7 @@ describe('SessionService', () => {
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
expect(sessionPayload).toEqual({
|
||||
expect(result.sessionHttpRepresentation).toEqual({
|
||||
access_expiration: 123,
|
||||
access_token: expect.any(String),
|
||||
refresh_expiration: 123,
|
||||
@@ -420,7 +432,7 @@ describe('SessionService', () => {
|
||||
it('should delete a session by token', async () => {
|
||||
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
|
||||
if (uuid === '2') {
|
||||
return session
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -429,13 +441,28 @@ describe('SessionService', () => {
|
||||
await createService().deleteSessionByToken('1:2:3')
|
||||
|
||||
expect(sessionRepository.deleteOneByUuid).toHaveBeenCalledWith('2e1e43')
|
||||
expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2e1e43', '1-2-3')
|
||||
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 session
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -448,13 +475,13 @@ describe('SessionService', () => {
|
||||
})
|
||||
|
||||
it('should determine if a refresh token is valid', async () => {
|
||||
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:3')).toBeTruthy()
|
||||
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:4')).toBeFalsy()
|
||||
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2')).toBeFalsy()
|
||||
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(session)).toEqual('Chrome 69.0 on Mac 10.13')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on Mac 10.13')
|
||||
})
|
||||
|
||||
it('should return device info based on undefined user agent', () => {
|
||||
@@ -463,7 +490,7 @@ describe('SessionService', () => {
|
||||
browser: { name: undefined, version: undefined },
|
||||
os: { name: undefined, version: undefined },
|
||||
})
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
|
||||
})
|
||||
|
||||
it('should return a shorter info based on lack of client in user agent', () => {
|
||||
@@ -473,7 +500,7 @@ describe('SessionService', () => {
|
||||
os: { name: 'iOS', version: '10.3' },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('iOS 10.3')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('iOS 10.3')
|
||||
})
|
||||
|
||||
it('should return a shorter info based on lack of os in user agent', () => {
|
||||
@@ -483,13 +510,13 @@ describe('SessionService', () => {
|
||||
os: { name: '', version: '' },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0')
|
||||
})
|
||||
|
||||
it('should return unknown client and os if user agent is cleaned out', () => {
|
||||
session.userAgent = null
|
||||
existingSession.userAgent = null
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
|
||||
})
|
||||
|
||||
it('should return a shorter info based on partial os in user agent', () => {
|
||||
@@ -499,7 +526,7 @@ describe('SessionService', () => {
|
||||
os: { name: 'Windows', version: '' },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on Windows')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on Windows')
|
||||
|
||||
deviceDetector.getResult = jest.fn().mockReturnValue({
|
||||
ua: 'dummy-data',
|
||||
@@ -507,7 +534,7 @@ describe('SessionService', () => {
|
||||
os: { name: '', version: '7' },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on 7')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on 7')
|
||||
})
|
||||
|
||||
it('should return a shorter info based on partial client in user agent', () => {
|
||||
@@ -517,7 +544,7 @@ describe('SessionService', () => {
|
||||
os: { name: 'Windows', version: '7' },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('69.0 on Windows 7')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('69.0 on Windows 7')
|
||||
|
||||
deviceDetector.getResult = jest.fn().mockReturnValue({
|
||||
ua: 'dummy-data',
|
||||
@@ -525,7 +552,7 @@ describe('SessionService', () => {
|
||||
os: { name: 'Windows', version: '7' },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Chrome on Windows 7')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome on Windows 7')
|
||||
})
|
||||
|
||||
it('should return a shorter info based on iOS agent', () => {
|
||||
@@ -538,7 +565,7 @@ describe('SessionService', () => {
|
||||
cpu: { architecture: undefined },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('iOS')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('iOS')
|
||||
})
|
||||
|
||||
it('should return a shorter info based on partial client and partial os in user agent', () => {
|
||||
@@ -548,7 +575,7 @@ describe('SessionService', () => {
|
||||
os: { name: 'Windows', version: '' },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('69.0 on Windows')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('69.0 on Windows')
|
||||
|
||||
deviceDetector.getResult = jest.fn().mockReturnValue({
|
||||
ua: 'dummy-data',
|
||||
@@ -556,7 +583,7 @@ describe('SessionService', () => {
|
||||
os: { name: '', version: '7' },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Chrome on 7')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome on 7')
|
||||
})
|
||||
|
||||
it('should return only Android os for okHttp client', () => {
|
||||
@@ -569,7 +596,7 @@ describe('SessionService', () => {
|
||||
cpu: { architecture: undefined },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Android')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Android')
|
||||
})
|
||||
|
||||
it('should detect the StandardNotes app in user agent', () => {
|
||||
@@ -582,7 +609,7 @@ describe('SessionService', () => {
|
||||
cpu: { architecture: undefined },
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Standard Notes Desktop 3.5.18 on Mac OS 10.16.0')
|
||||
expect(createService().getDeviceInfo(existingSession)).toEqual('Standard Notes Desktop 3.5.18 on Mac OS 10.16.0')
|
||||
})
|
||||
|
||||
it('should return unknown device info as fallback', () => {
|
||||
@@ -590,70 +617,72 @@ describe('SessionService', () => {
|
||||
throw new Error('something bad happened')
|
||||
})
|
||||
|
||||
expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
|
||||
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 session
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const result = await createService().getSessionFromToken('1:2:3')
|
||||
const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
|
||||
|
||||
expect(result).toEqual(session)
|
||||
expect(session).toEqual(session)
|
||||
expect(isEphemeral).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should retrieve an ephemeral session from a session token', async () => {
|
||||
ephemeralSessionRepository.findOneByUuid = jest.fn().mockReturnValue(ephemeralSession)
|
||||
ephemeralSessionRepository.findOneByUuid = jest.fn().mockReturnValue(existingEphemeralSession)
|
||||
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createService().getSessionFromToken('1:2:3')
|
||||
const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
|
||||
|
||||
expect(result).toEqual(ephemeralSession)
|
||||
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 session
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const result = await createService().getSessionFromToken('1:2')
|
||||
const { session } = await createService().getSessionFromToken('1:2')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
expect(session).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not retrieve a session that is missing', async () => {
|
||||
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createService().getSessionFromToken('1:2:3')
|
||||
const { session } = await createService().getSessionFromToken('1:2:3')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
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 session
|
||||
return existingSession
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const result = await createService().getSessionFromToken('1:2:4')
|
||||
const { session } = await createService().getSessionFromToken('1:2:4')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
expect(session).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should revoked a session', async () => {
|
||||
await createService().createRevokedSession(session)
|
||||
await createService().createRevokedSession(existingSession)
|
||||
|
||||
expect(revokedSessionRepository.save).toHaveBeenCalledWith({
|
||||
uuid: '2e1e43',
|
||||
|
||||
@@ -49,7 +49,7 @@ export class SessionService implements SessionServiceInterface {
|
||||
apiVersion: string
|
||||
userAgent: string
|
||||
readonlyAccess: boolean
|
||||
}): Promise<SessionBody> {
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
|
||||
const session = await this.createSession({
|
||||
ephemeral: false,
|
||||
...dto,
|
||||
@@ -73,7 +73,10 @@ export class SessionService implements SessionServiceInterface {
|
||||
this.logger.error(`Could not trace session while creating cross service token.: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
return sessionPayload
|
||||
return {
|
||||
sessionHttpRepresentation: sessionPayload,
|
||||
session,
|
||||
}
|
||||
}
|
||||
|
||||
async createNewEphemeralSessionForUser(dto: {
|
||||
@@ -81,7 +84,7 @@ export class SessionService implements SessionServiceInterface {
|
||||
apiVersion: string
|
||||
userAgent: string
|
||||
readonlyAccess: boolean
|
||||
}): Promise<SessionBody> {
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
|
||||
const ephemeralSession = await this.createSession({
|
||||
ephemeral: true,
|
||||
...dto,
|
||||
@@ -91,27 +94,20 @@ export class SessionService implements SessionServiceInterface {
|
||||
|
||||
await this.ephemeralSessionRepository.save(ephemeralSession)
|
||||
|
||||
return sessionPayload
|
||||
return {
|
||||
sessionHttpRepresentation: sessionPayload,
|
||||
session: ephemeralSession,
|
||||
}
|
||||
}
|
||||
|
||||
async refreshTokens(session: Session): Promise<SessionBody> {
|
||||
const sessionPayload = await this.createTokens(session)
|
||||
async refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody> {
|
||||
const sessionPayload = await this.createTokens(dto.session)
|
||||
|
||||
await this.sessionRepository.updateHashedTokens(session.uuid, session.hashedAccessToken, session.hashedRefreshToken)
|
||||
|
||||
await this.sessionRepository.updatedTokenExpirationDates(
|
||||
session.uuid,
|
||||
session.accessExpiration,
|
||||
session.refreshExpiration,
|
||||
)
|
||||
|
||||
await this.ephemeralSessionRepository.updateTokensAndExpirationDates(
|
||||
session.uuid,
|
||||
session.hashedAccessToken,
|
||||
session.hashedRefreshToken,
|
||||
session.accessExpiration,
|
||||
session.refreshExpiration,
|
||||
)
|
||||
if (dto.isEphemeral) {
|
||||
await this.ephemeralSessionRepository.save(dto.session)
|
||||
} else {
|
||||
await this.sessionRepository.save(dto.session)
|
||||
}
|
||||
|
||||
return sessionPayload
|
||||
}
|
||||
@@ -190,25 +186,25 @@ export class SessionService implements SessionServiceInterface {
|
||||
return `${browserInfo} on ${osInfo}`
|
||||
}
|
||||
|
||||
async getSessionFromToken(token: string): Promise<Session | undefined> {
|
||||
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 undefined
|
||||
return { session: undefined, isEphemeral: false }
|
||||
}
|
||||
|
||||
const session = await this.getSession(sessionUuid)
|
||||
const { session, isEphemeral } = await this.getSession(sessionUuid)
|
||||
if (!session) {
|
||||
return undefined
|
||||
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
|
||||
return { session, isEphemeral }
|
||||
}
|
||||
|
||||
return undefined
|
||||
return { session: undefined, isEphemeral: false }
|
||||
}
|
||||
|
||||
async getRevokedSessionFromToken(token: string): Promise<RevokedSession | null> {
|
||||
@@ -229,11 +225,14 @@ export class SessionService implements SessionServiceInterface {
|
||||
}
|
||||
|
||||
async deleteSessionByToken(token: string): Promise<string | null> {
|
||||
const session = await this.getSessionFromToken(token)
|
||||
const { session, isEphemeral } = await this.getSessionFromToken(token)
|
||||
|
||||
if (session) {
|
||||
await this.sessionRepository.deleteOneByUuid(session.uuid)
|
||||
await this.ephemeralSessionRepository.deleteOne(session.uuid, session.userUuid)
|
||||
if (isEphemeral) {
|
||||
await this.ephemeralSessionRepository.deleteOne(session.uuid, session.userUuid)
|
||||
} else {
|
||||
await this.sessionRepository.deleteOneByUuid(session.uuid)
|
||||
}
|
||||
|
||||
return session.userUuid
|
||||
}
|
||||
@@ -278,14 +277,19 @@ export class SessionService implements SessionServiceInterface {
|
||||
return session
|
||||
}
|
||||
|
||||
private async getSession(uuid: string): Promise<Session | null> {
|
||||
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
|
||||
}
|
||||
|
||||
return session
|
||||
return { session, isEphemeral }
|
||||
}
|
||||
|
||||
private async createTokens(session: Session): Promise<SessionBody> {
|
||||
|
||||
@@ -9,15 +9,15 @@ export interface SessionServiceInterface {
|
||||
apiVersion: string
|
||||
userAgent: string
|
||||
readonlyAccess: boolean
|
||||
}): Promise<SessionBody>
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }>
|
||||
createNewEphemeralSessionForUser(dto: {
|
||||
user: User
|
||||
apiVersion: string
|
||||
userAgent: string
|
||||
readonlyAccess: boolean
|
||||
}): Promise<SessionBody>
|
||||
refreshTokens(session: Session): Promise<SessionBody>
|
||||
getSessionFromToken(token: string): Promise<Session | undefined>
|
||||
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }>
|
||||
refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody>
|
||||
getSessionFromToken(token: string): Promise<{ session: Session | undefined; isEphemeral: boolean }>
|
||||
getRevokedSessionFromToken(token: string): Promise<RevokedSession | null>
|
||||
markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession>
|
||||
deleteSessionByToken(token: string): Promise<string | null>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface TransitionStatusRepositoryInterface {
|
||||
updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void>
|
||||
removeStatus(userUuid: string): Promise<void>
|
||||
getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null>
|
||||
}
|
||||
+8
-8
@@ -69,7 +69,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
userSubscriptionRepository.save = jest.fn().mockReturnValue(inviteeSubscription)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
|
||||
@@ -103,7 +103,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
updatedAt: 1,
|
||||
user: Promise.resolve(invitee),
|
||||
})
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
inviteeSubscription,
|
||||
)
|
||||
@@ -143,7 +143,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
updatedAt: 3,
|
||||
user: Promise.resolve(invitee),
|
||||
})
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
inviteeSubscription,
|
||||
)
|
||||
@@ -162,7 +162,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -180,7 +180,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -219,7 +219,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -244,7 +244,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
await this.roleService.addUserRole(user, subscriptionName)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
|
||||
}
|
||||
|
||||
private async createSharedSubscription(
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ describe('ActivatePremiumFeatures', () => {
|
||||
userSubscriptionRepository.save = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addUserRole = jest.fn()
|
||||
roleService.addUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
@@ -73,7 +73,7 @@ describe('ActivatePremiumFeatures', () => {
|
||||
expect(result.isFailed()).toBe(false)
|
||||
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).toHaveBeenCalled()
|
||||
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save a subscription with custom plan name and endsAt', async () => {
|
||||
|
||||
@@ -53,11 +53,11 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
|
||||
await this.userSubscriptionRepository.save(subscription)
|
||||
|
||||
await this.roleService.addUserRole(user, subscriptionPlanName.value)
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionPlanName.value)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
|
||||
subscription,
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]),
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
|
||||
)
|
||||
|
||||
return Result.ok('Premium features activated.')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface ActivatePremiumFeaturesDTO {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}
|
||||
|
||||
+4
-4
@@ -84,7 +84,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
userSubscriptionRepository.save = jest.fn()
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.removeUserRole = jest.fn()
|
||||
roleService.removeUserRoleBasedOnSubscription = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
|
||||
@@ -122,7 +122,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
endsAt: 1,
|
||||
user: Promise.resolve(invitee),
|
||||
})
|
||||
expect(roleService.removeUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createSharedSubscriptionInvitationCanceledEvent).toHaveBeenCalledWith({
|
||||
inviteeIdentifier: '123',
|
||||
@@ -156,7 +156,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
inviteeIdentifierType: 'email',
|
||||
})
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
})
|
||||
|
||||
it('should not cancel a shared subscription invitation if it is not found', async () => {
|
||||
@@ -204,7 +204,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
|
||||
inviteeIdentifierType: 'email',
|
||||
})
|
||||
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not cancel a shared subscription invitation if inviter subscription is not found', async () => {
|
||||
|
||||
+4
-1
@@ -90,7 +90,10 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
if (invitee !== null) {
|
||||
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
|
||||
|
||||
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
|
||||
await this.roleService.removeUserRoleBasedOnSubscription(
|
||||
invitee,
|
||||
inviterUserSubscription.planName as SubscriptionName,
|
||||
)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
|
||||
|
||||
@@ -11,7 +11,10 @@ import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { ChangeCredentials } from './ChangeCredentials'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { Result, Username } from '@standardnotes/domain-core'
|
||||
import { DeleteOtherSessionsForUser } from '../DeleteOtherSessionsForUser'
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
import { Session } from '../../Session/Session'
|
||||
|
||||
describe('ChangeCredentials', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -21,13 +24,23 @@ describe('ChangeCredentials', () => {
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let timer: TimerInterface
|
||||
let user: User
|
||||
let deleteOtherSessionsForUser: DeleteOtherSessionsForUser
|
||||
|
||||
const createUseCase = () =>
|
||||
new ChangeCredentials(userRepository, authResponseFactoryResolver, domainEventPublisher, domainEventFactory, timer)
|
||||
new ChangeCredentials(
|
||||
userRepository,
|
||||
authResponseFactoryResolver,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
timer,
|
||||
deleteOtherSessionsForUser,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
authResponseFactory = {} as jest.Mocked<AuthResponseFactoryInterface>
|
||||
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
authResponseFactory.createResponse = jest
|
||||
.fn()
|
||||
.mockReturnValue({ response: { foo: 'bar' }, session: { uuid: '1-2-3' } as jest.Mocked<Session> })
|
||||
|
||||
authResponseFactoryResolver = {} as jest.Mocked<AuthResponseFactoryResolverInterface>
|
||||
authResponseFactoryResolver.resolveAuthResponseFactoryVersion = jest.fn().mockReturnValue(authResponseFactory)
|
||||
@@ -49,27 +62,25 @@ describe('ChangeCredentials', () => {
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
|
||||
|
||||
deleteOtherSessionsForUser = {} as jest.Mocked<DeleteOtherSessionsForUser>
|
||||
deleteOtherSessionsForUser.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
})
|
||||
|
||||
it('should change password', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: '20190520',
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
authResponse: {
|
||||
foo: 'bar',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
encryptedPassword: expect.any(String),
|
||||
pwNonce: 'asdzxc',
|
||||
@@ -81,29 +92,24 @@ describe('ChangeCredentials', () => {
|
||||
})
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
|
||||
expect(deleteOtherSessionsForUser.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should change email', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValueOnce(user).mockReturnValueOnce(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: '20190520',
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
newEmail: 'new@test.te',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
authResponse: {
|
||||
foo: 'bar',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
newEmail: 'new@test.te',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
encryptedPassword: expect.any(String),
|
||||
@@ -116,6 +122,7 @@ describe('ChangeCredentials', () => {
|
||||
})
|
||||
expect(domainEventFactory.createUserEmailChangedEvent).toHaveBeenCalledWith('1-2-3', 'test@test.te', 'new@test.te')
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(deleteOtherSessionsForUser.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not change email if already taken', async () => {
|
||||
@@ -124,22 +131,19 @@ describe('ChangeCredentials', () => {
|
||||
.mockReturnValueOnce(user)
|
||||
.mockReturnValueOnce({} as jest.Mocked<User>)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: '20190520',
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
newEmail: 'new@test.te',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorMessage: 'The email you entered is already taken. Please try again.',
|
||||
const result = await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
newEmail: 'new@test.te',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('The email you entered is already taken. Please try again.')
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
|
||||
@@ -147,22 +151,19 @@ describe('ChangeCredentials', () => {
|
||||
})
|
||||
|
||||
it('should not change email if the new email is invalid', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: '20190520',
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
newEmail: '',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorMessage: 'Username cannot be empty',
|
||||
const result = await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
newEmail: '',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Username cannot be empty')
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
|
||||
@@ -172,63 +173,52 @@ describe('ChangeCredentials', () => {
|
||||
it('should not change email if the user is not found', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: '20190520',
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
newEmail: '',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorMessage: 'User not found.',
|
||||
const result = await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
newEmail: '',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('User not found.')
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not change password if current password is incorrect', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: '20190520',
|
||||
currentPassword: 'test123',
|
||||
newPassword: 'test234',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorMessage: 'The current password you entered is incorrect. Please try again.',
|
||||
const result = await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
currentPassword: 'test123',
|
||||
newPassword: 'test234',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('The current password you entered is incorrect. Please try again.')
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update protocol version while changing password', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: '20190520',
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
protocolVersion: '004',
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
authResponse: {
|
||||
foo: 'bar',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
protocolVersion: '004',
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
encryptedPassword: expect.any(String),
|
||||
@@ -239,4 +229,63 @@ describe('ChangeCredentials', () => {
|
||||
updatedAt: new Date(1),
|
||||
})
|
||||
})
|
||||
|
||||
it('should not delete other sessions for user if neither passoword nor email are changed', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValueOnce(user)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'qweqwe123123',
|
||||
newEmail: undefined,
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
encryptedPassword: expect.any(String),
|
||||
email: 'test@test.te',
|
||||
uuid: '1-2-3',
|
||||
pwNonce: 'asdzxc',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
updatedAt: new Date(1),
|
||||
})
|
||||
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(deleteOtherSessionsForUser.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not delete other sessions for user if the caller does not support sessions', async () => {
|
||||
authResponseFactory.createResponse = jest.fn().mockReturnValue({ response: { foo: 'bar' } })
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
currentPassword: 'qweqwe123123',
|
||||
newPassword: 'test234',
|
||||
pwNonce: 'asdzxc',
|
||||
updatedWithUserAgent: 'Google Chrome',
|
||||
kpCreated: '123',
|
||||
kpOrigination: 'password-change',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
encryptedPassword: expect.any(String),
|
||||
pwNonce: 'asdzxc',
|
||||
kpCreated: '123',
|
||||
email: 'test@test.te',
|
||||
uuid: '1-2-3',
|
||||
kpOrigination: 'password-change',
|
||||
updatedAt: new Date(1),
|
||||
})
|
||||
|
||||
expect(deleteOtherSessionsForUser.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import * as bcrypt from 'bcryptjs'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { DomainEventPublisherInterface, UserEmailChangedEvent } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { AuthResponseFactoryResolverInterface } from '../../Auth/AuthResponseFactoryResolverInterface'
|
||||
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { ChangeCredentialsDTO } from './ChangeCredentialsDTO'
|
||||
import { ChangeCredentialsResponse } from './ChangeCredentialsResponse'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { DomainEventPublisherInterface, UserEmailChangedEvent } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { DeleteOtherSessionsForUser } from '../DeleteOtherSessionsForUser'
|
||||
import { AuthResponse20161215 } from '../../Auth/AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
|
||||
import { Session } from '../../Session/Session'
|
||||
|
||||
@injectable()
|
||||
export class ChangeCredentials implements UseCaseInterface {
|
||||
export class ChangeCredentials implements UseCaseInterface<AuthResponse20161215 | AuthResponse20200115> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_AuthResponseFactoryResolver)
|
||||
@@ -22,22 +24,18 @@ export class ChangeCredentials implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Auth_DeleteOtherSessionsForUser)
|
||||
private deleteOtherSessionsForUserUseCase: DeleteOtherSessionsForUser,
|
||||
) {}
|
||||
|
||||
async execute(dto: ChangeCredentialsDTO): Promise<ChangeCredentialsResponse> {
|
||||
async execute(dto: ChangeCredentialsDTO): Promise<Result<AuthResponse20161215 | AuthResponse20200115>> {
|
||||
const user = await this.userRepository.findOneByUsernameOrEmail(dto.username)
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'User not found.',
|
||||
}
|
||||
return Result.fail('User not found.')
|
||||
}
|
||||
|
||||
if (!(await bcrypt.compare(dto.currentPassword, user.encryptedPassword))) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'The current password you entered is incorrect. Please try again.',
|
||||
}
|
||||
return Result.fail('The current password you entered is incorrect. Please try again.')
|
||||
}
|
||||
|
||||
user.encryptedPassword = await bcrypt.hash(dto.newPassword, User.PASSWORD_HASH_COST)
|
||||
@@ -46,19 +44,13 @@ export class ChangeCredentials implements UseCaseInterface {
|
||||
if (dto.newEmail !== undefined) {
|
||||
const newUsernameOrError = Username.create(dto.newEmail)
|
||||
if (newUsernameOrError.isFailed()) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: newUsernameOrError.getError(),
|
||||
}
|
||||
return Result.fail(newUsernameOrError.getError())
|
||||
}
|
||||
const newUsername = newUsernameOrError.getValue()
|
||||
|
||||
const existingUser = await this.userRepository.findOneByUsernameOrEmail(newUsername)
|
||||
if (existingUser !== null) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'The email you entered is already taken. Please try again.',
|
||||
}
|
||||
return Result.fail('The email you entered is already taken. Please try again.')
|
||||
}
|
||||
|
||||
userEmailChangedEvent = this.domainEventFactory.createUserEmailChangedEvent(
|
||||
@@ -90,15 +82,35 @@ export class ChangeCredentials implements UseCaseInterface {
|
||||
|
||||
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
authResponse: await authResponseFactory.createResponse({
|
||||
user: updatedUser,
|
||||
apiVersion: dto.apiVersion,
|
||||
userAgent: dto.updatedWithUserAgent,
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
}),
|
||||
const authResponse = await authResponseFactory.createResponse({
|
||||
user: updatedUser,
|
||||
apiVersion: dto.apiVersion,
|
||||
userAgent: dto.updatedWithUserAgent,
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
|
||||
if (authResponse.session) {
|
||||
await this.deleteOtherSessionsForUserIfNeeded(user.uuid, authResponse.session, dto)
|
||||
}
|
||||
|
||||
return Result.ok(authResponse.response)
|
||||
}
|
||||
|
||||
private async deleteOtherSessionsForUserIfNeeded(
|
||||
userUuid: string,
|
||||
session: Session,
|
||||
dto: ChangeCredentialsDTO,
|
||||
): Promise<void> {
|
||||
const passwordHasChanged = dto.newPassword !== dto.currentPassword
|
||||
const userEmailChanged = dto.newEmail !== undefined
|
||||
|
||||
if (passwordHasChanged || userEmailChanged) {
|
||||
await this.deleteOtherSessionsForUserUseCase.execute({
|
||||
userUuid,
|
||||
currentSessionUuid: session.uuid,
|
||||
markAsRevoked: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { AuthResponse20161215 } from '../../Auth/AuthResponse20161215'
|
||||
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
|
||||
|
||||
export type ChangeCredentialsResponse = {
|
||||
success: boolean
|
||||
authResponse?: AuthResponse20161215 | AuthResponse20200115
|
||||
errorMessage?: string
|
||||
}
|
||||
+118
-19
@@ -8,6 +8,9 @@ import { Role } from '../../Role/Role'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
|
||||
import { GetSetting } from '../GetSetting/GetSetting'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
|
||||
describe('CreateCrossServiceToken', () => {
|
||||
let userProjector: ProjectorInterface<User>
|
||||
@@ -15,6 +18,8 @@ describe('CreateCrossServiceToken', () => {
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
let userRepository: UserRepositoryInterface
|
||||
let getSettingUseCase: GetSetting
|
||||
let transitionStatusRepository: TransitionStatusRepositoryInterface
|
||||
const jwtTTL = 60
|
||||
|
||||
let session: Session
|
||||
@@ -22,7 +27,16 @@ describe('CreateCrossServiceToken', () => {
|
||||
let role: Role
|
||||
|
||||
const createUseCase = () =>
|
||||
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
|
||||
new CreateCrossServiceToken(
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
userRepository,
|
||||
jwtTTL,
|
||||
getSettingUseCase,
|
||||
transitionStatusRepository,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
@@ -50,6 +64,12 @@ describe('CreateCrossServiceToken', () => {
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
|
||||
getSettingUseCase = {} as jest.Mocked<GetSetting>
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } }))
|
||||
|
||||
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
|
||||
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('TO-DO')
|
||||
})
|
||||
|
||||
it('should create a cross service token for user', async () => {
|
||||
@@ -73,6 +93,36 @@ describe('CreateCrossServiceToken', () => {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: false,
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user that has an ongoing transaction', async () => {
|
||||
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
|
||||
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: true,
|
||||
},
|
||||
60,
|
||||
)
|
||||
@@ -95,6 +145,7 @@ describe('CreateCrossServiceToken', () => {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: false,
|
||||
},
|
||||
60,
|
||||
)
|
||||
@@ -117,6 +168,7 @@ describe('CreateCrossServiceToken', () => {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: false,
|
||||
},
|
||||
60,
|
||||
)
|
||||
@@ -125,28 +177,75 @@ describe('CreateCrossServiceToken', () => {
|
||||
it('should throw an error if user does not exist', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw an error if user uuid is invalid', async () => {
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('shared vault context', () => {
|
||||
it('should add shared vault context if shared vault owner uuid is provided', async () => {
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
shared_vault_owner_context: {
|
||||
upload_bytes_limit: 100,
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
ongoing_transition: false,
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if shared vault owner context is sensitive', async () => {
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sensitive: true }))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw an error if it fails to retrieve shared vault owner setting', async () => {
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+36
-10
@@ -1,5 +1,6 @@
|
||||
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
|
||||
@@ -7,14 +8,14 @@ import { Role } from '../../Role/Role'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
|
||||
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
|
||||
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { GetSetting } from '../GetSetting/GetSetting'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
export class CreateCrossServiceToken implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>,
|
||||
@inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface<Session>,
|
||||
@@ -22,14 +23,18 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
|
||||
@inject(TYPES.Auth_GetSetting)
|
||||
private getSettingUseCase: GetSetting,
|
||||
@inject(TYPES.Auth_TransitionStatusRepository)
|
||||
private transitionStatusRepository: TransitionStatusRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
|
||||
async execute(dto: CreateCrossServiceTokenDTO): Promise<Result<string>> {
|
||||
let user: User | undefined | null = dto.user
|
||||
if (user === undefined && dto.userUuid !== undefined) {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
throw new Error(userUuidOrError.getError())
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
@@ -37,23 +42,44 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with uuid ${dto.userUuid}`)
|
||||
return Result.fail(`Could not find user with uuid ${dto.userUuid}`)
|
||||
}
|
||||
|
||||
const transitionStatus = await this.transitionStatusRepository.getStatus(user.uuid)
|
||||
|
||||
const roles = await user.roles
|
||||
|
||||
const authTokenData: CrossServiceTokenData = {
|
||||
user: this.projectUser(user),
|
||||
roles: this.projectRoles(roles),
|
||||
shared_vault_owner_context: undefined,
|
||||
ongoing_transition: transitionStatus === 'STARTED',
|
||||
}
|
||||
|
||||
if (dto.sharedVaultOwnerContext !== undefined) {
|
||||
const uploadBytesLimitSettingOrError = await this.getSettingUseCase.execute({
|
||||
settingName: SettingName.NAMES.FileUploadBytesLimit,
|
||||
userUuid: dto.sharedVaultOwnerContext,
|
||||
})
|
||||
if (uploadBytesLimitSettingOrError.isFailed()) {
|
||||
return Result.fail(uploadBytesLimitSettingOrError.getError())
|
||||
}
|
||||
const uploadBytesLimitSetting = uploadBytesLimitSettingOrError.getValue()
|
||||
if (uploadBytesLimitSetting.sensitive) {
|
||||
return Result.fail('Shared vault owner upload bytes limit setting is sensitive!')
|
||||
}
|
||||
const uploadBytesLimit = parseInt(uploadBytesLimitSetting.setting.value as string)
|
||||
|
||||
authTokenData.shared_vault_owner_context = {
|
||||
upload_bytes_limit: uploadBytesLimit,
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.session !== undefined) {
|
||||
authTokenData.session = this.projectSession(dto.session)
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
|
||||
}
|
||||
return Result.ok(this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL))
|
||||
}
|
||||
|
||||
private projectUser(user: User): { uuid: string; email: string } {
|
||||
|
||||
@@ -6,6 +6,7 @@ export type CreateCrossServiceTokenDTO = Either<
|
||||
{
|
||||
user: User
|
||||
session?: Session
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
{
|
||||
userUuid: string
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export type CreateCrossServiceTokenResponse = {
|
||||
token: string
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Session } from '../Session/Session'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
|
||||
import { DeleteOtherSessionsForUser } from './DeleteOtherSessionsForUser'
|
||||
|
||||
describe('DeleteOtherSessionsForUser', () => {
|
||||
let sessionRepository: SessionRepositoryInterface
|
||||
let sessionService: SessionServiceInterface
|
||||
let session: Session
|
||||
let currentSession: Session
|
||||
|
||||
const createUseCase = () => new DeleteOtherSessionsForUser(sessionRepository, sessionService)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
session.uuid = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
currentSession = {} as jest.Mocked<Session>
|
||||
currentSession.uuid = '00000000-0000-0000-0000-000000000001'
|
||||
|
||||
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
|
||||
sessionRepository.deleteAllByUserUuidExceptOne = jest.fn()
|
||||
sessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([session, currentSession])
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.createRevokedSession = jest.fn()
|
||||
})
|
||||
|
||||
it('should delete all sessions except current for a given user', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
currentSessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
markAsRevoked: true,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(sessionRepository.deleteAllByUserUuidExceptOne).toHaveBeenCalled()
|
||||
|
||||
expect(sessionService.createRevokedSession).toHaveBeenCalledWith(session)
|
||||
expect(sessionService.createRevokedSession).not.toHaveBeenCalledWith(currentSession)
|
||||
})
|
||||
|
||||
it('should delete all sessions except current for a given user without marking as revoked', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
currentSessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
markAsRevoked: false,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(sessionRepository.deleteAllByUserUuidExceptOne).toHaveBeenCalled()
|
||||
|
||||
expect(sessionService.createRevokedSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not delete any sessions if the user uuid is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
currentSessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
markAsRevoked: true,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(sessionRepository.deleteAllByUserUuidExceptOne).not.toHaveBeenCalled()
|
||||
expect(sessionService.createRevokedSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not delete any sessions if the current session uuid is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
currentSessionUuid: 'invalid',
|
||||
markAsRevoked: true,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(sessionRepository.deleteAllByUserUuidExceptOne).not.toHaveBeenCalled()
|
||||
expect(sessionService.createRevokedSession).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { Session } from '../Session/Session'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { DeleteOtherSessionsForUserDTO } from './DeleteOtherSessionsForUserDTO'
|
||||
|
||||
@injectable()
|
||||
export class DeleteOtherSessionsForUser implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface,
|
||||
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: DeleteOtherSessionsForUserDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const currentSessionUuidOrError = Uuid.create(dto.currentSessionUuid)
|
||||
if (currentSessionUuidOrError.isFailed()) {
|
||||
return Result.fail(currentSessionUuidOrError.getError())
|
||||
}
|
||||
const currentSessionUuid = currentSessionUuidOrError.getValue()
|
||||
|
||||
const sessions = await this.sessionRepository.findAllByUserUuid(dto.userUuid)
|
||||
|
||||
if (dto.markAsRevoked) {
|
||||
await Promise.all(
|
||||
sessions.map(async (session: Session) => {
|
||||
if (session.uuid !== currentSessionUuid.value) {
|
||||
await this.sessionService.createRevokedSession(session)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
await this.sessionRepository.deleteAllByUserUuidExceptOne({ userUuid, currentSessionUuid })
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type DeleteOtherSessionsForUserDTO = {
|
||||
userUuid: string
|
||||
currentSessionUuid: string
|
||||
markAsRevoked: boolean
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { Session } from '../Session/Session'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
|
||||
import { DeletePreviousSessionsForUser } from './DeletePreviousSessionsForUser'
|
||||
|
||||
describe('DeletePreviousSessionsForUser', () => {
|
||||
let sessionRepository: SessionRepositoryInterface
|
||||
let sessionService: SessionServiceInterface
|
||||
let session: Session
|
||||
let currentSession: Session
|
||||
|
||||
const createUseCase = () => new DeletePreviousSessionsForUser(sessionRepository, sessionService)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
session.uuid = '1-2-3'
|
||||
|
||||
currentSession = {} as jest.Mocked<Session>
|
||||
currentSession.uuid = '2-3-4'
|
||||
|
||||
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
|
||||
sessionRepository.deleteAllByUserUuid = jest.fn()
|
||||
sessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([session, currentSession])
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.createRevokedSession = jest.fn()
|
||||
})
|
||||
|
||||
it('should delete all sessions except current for a given user', async () => {
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3', currentSessionUuid: '2-3-4' })).toEqual({ success: true })
|
||||
|
||||
expect(sessionRepository.deleteAllByUserUuid).toHaveBeenCalledWith('1-2-3', '2-3-4')
|
||||
|
||||
expect(sessionService.createRevokedSession).toHaveBeenCalledWith(session)
|
||||
expect(sessionService.createRevokedSession).not.toHaveBeenCalledWith(currentSession)
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { Session } from '../Session/Session'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { DeletePreviousSessionsForUserDTO } from './DeletePreviousSessionsForUserDTO'
|
||||
import { DeletePreviousSessionsForUserResponse } from './DeletePreviousSessionsForUserResponse'
|
||||
import { UseCaseInterface } from './UseCaseInterface'
|
||||
|
||||
@injectable()
|
||||
export class DeletePreviousSessionsForUser implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface,
|
||||
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: DeletePreviousSessionsForUserDTO): Promise<DeletePreviousSessionsForUserResponse> {
|
||||
const sessions = await this.sessionRepository.findAllByUserUuid(dto.userUuid)
|
||||
|
||||
await Promise.all(
|
||||
sessions.map(async (session: Session) => {
|
||||
if (session.uuid !== dto.currentSessionUuid) {
|
||||
await this.sessionService.createRevokedSession(session)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
await this.sessionRepository.deleteAllByUserUuid(dto.userUuid, dto.currentSessionUuid)
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export type DeletePreviousSessionsForUserDTO = {
|
||||
userUuid: string
|
||||
currentSessionUuid: string
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type DeletePreviousSessionsForUserResponse = {
|
||||
success: boolean
|
||||
}
|
||||
@@ -73,35 +73,30 @@ describe('GetSetting', () => {
|
||||
|
||||
describe('no subscription', () => {
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.DropboxBackupFrequency,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'bar' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should not find a setting if the setting name is invalid', async () => {
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid setting name: invalid',
|
||||
},
|
||||
})
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not get a setting for user if it does not exist', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.DropboxBackupFrequency,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not retrieve a sensitive setting for user', async () => {
|
||||
@@ -112,21 +107,19 @@ describe('GetSetting', () => {
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
sensitive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not retrieve a subscription setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'No subscription found.',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
|
||||
@@ -137,14 +130,13 @@ describe('GetSetting', () => {
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
allowSensitiveRetrieval: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
allowSensitiveRetrieval: true,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'bar' },
|
||||
})
|
||||
@@ -159,10 +151,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
@@ -171,14 +165,11 @@ describe('GetSetting', () => {
|
||||
it('should not get a suscription setting for user if it does not exist', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not retrieve a sensitive subscription setting for user', async () => {
|
||||
@@ -188,10 +179,12 @@ describe('GetSetting', () => {
|
||||
.fn()
|
||||
.mockReturnValue(subscriptionSetting)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
sensitive: true,
|
||||
})
|
||||
})
|
||||
@@ -205,10 +198,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
@@ -221,10 +216,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a regular subscription only setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.FileUploadBytesLimit,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user