Compare commits

...

85 Commits

Author SHA1 Message Date
standardci
c6d655c5f5 chore(release): publish new version
- @standardnotes/analytics@2.25.10
 - @standardnotes/api-gateway@1.70.2
 - @standardnotes/auth-server@1.131.1
 - @standardnotes/domain-core@1.25.0
 - @standardnotes/event-store@1.11.17
 - @standardnotes/files-server@1.20.1
 - @standardnotes/home-server@1.13.37
 - @standardnotes/revisions-server@1.26.4
 - @standardnotes/scheduler-server@1.20.19
 - @standardnotes/settings@1.21.22
 - @standardnotes/syncing-server@1.78.0
 - @standardnotes/websockets-server@1.10.14
2023-08-09 13:46:50 +00:00
Karol Sójko
46867c1a4d feat(syncing-server): notify shared vault users upon file uploads or removals (#692) 2023-08-09 15:08:17 +02:00
standardci
d29903bab6 chore(release): publish new version
- @standardnotes/home-server@1.13.36
 - @standardnotes/syncing-server@1.77.2
2023-08-09 08:37:21 +00:00
Karol Sójko
3415cae093 fix(syncing-server): update storage quota used in a shared vault (#691) 2023-08-09 10:05:48 +02:00
standardci
408fd5a0c6 chore(release): publish new version
- @standardnotes/home-server@1.13.35
 - @standardnotes/syncing-server@1.77.1
2023-08-08 13:05:40 +00:00
Karol Sójko
0a16ee64fe fix(syncing-server): inviting already existing members to shared vault (#690)
* fix(syncing-server): inviting already existing members to shared vault

* fix(syncing-server): finding method for existing members
2023-08-08 14:31:23 +02:00
standardci
22b00479b4 chore(release): publish new version
- @standardnotes/analytics@2.25.9
 - @standardnotes/api-gateway@1.70.1
 - @standardnotes/auth-server@1.131.0
 - @standardnotes/domain-events-infra@1.12.11
 - @standardnotes/domain-events@2.115.0
 - @standardnotes/event-store@1.11.16
 - @standardnotes/files-server@1.20.0
 - @standardnotes/home-server@1.13.34
 - @standardnotes/revisions-server@1.26.3
 - @standardnotes/scheduler-server@1.20.18
 - @standardnotes/security@1.9.0
 - @standardnotes/syncing-server@1.77.0
 - @standardnotes/websockets-server@1.10.13
2023-08-08 12:06:10 +00:00
Karol Sójko
5311e74266 feat: update storage quota used for user based on shared vault files (#689)
* feat: update storage quota used for user based on shared vault files

* fix: use case binding

* fix: increase file upload bytes limit for shared vaults
2023-08-08 13:36:35 +02:00
standardci
5be7db7788 chore(release): publish new version
- @standardnotes/home-server@1.13.33
 - @standardnotes/syncing-server@1.76.1
2023-08-08 09:29:54 +00:00
Karol Sójko
3bd1547ce3 fix(syncing-server): race condition when adding admin user to newly created shared vault (#688) 2023-08-08 11:02:10 +02:00
standardci
a1fe15f7a9 chore(release): publish new version
- @standardnotes/api-gateway@1.70.0
 - @standardnotes/home-server@1.13.32
 - @standardnotes/syncing-server@1.76.0
2023-08-07 16:09:21 +00:00
Karol Sójko
19b8921f28 feat(syncing-server): limit shared vaults creation based on role (#687)
* feat(syncing-server): limit shared vaults creation based on role

* fix: add role names emptyness validation

* fix: roles passing to response locals
2023-08-07 17:35:47 +02:00
standardci
6b7879ba15 chore(release): publish new version
- @standardnotes/auth-server@1.130.1
 - @standardnotes/home-server@1.13.31
2023-08-07 11:50:26 +00:00
Karol Sójko
bd5f492a73 fix(auth): update user agent upon refreshing session token (#685) 2023-08-07 13:21:21 +02:00
standardci
67311cc002 chore(release): publish new version
- @standardnotes/auth-server@1.130.0
 - @standardnotes/home-server@1.13.30
2023-08-07 08:32:08 +00:00
Karol Sójko
f39d3aca5b feat(auth): invalidate other sessions for user if the email or password are changed (#684)
* feat(auth): invalidate other sessions for user if the email or password are changed

* fix(auth): handling credentials change in a legacy protocol scenario

* fix(auth): leave only the newly created session when changing credentials
2023-08-07 10:02:47 +02:00
standardci
8e47491e3c chore(release): publish new version
- @standardnotes/home-server@1.13.29
 - @standardnotes/syncing-server@1.75.4
2023-08-03 13:38:37 +00:00
Karol Sójko
0036d527bd fix(syncing-server): skip retrieval of items with invalid uuids (#683) 2023-08-03 15:05:59 +02:00
standardci
f565f1d950 chore(release): publish new version
- @standardnotes/analytics@2.25.8
 - @standardnotes/api-gateway@1.69.3
 - @standardnotes/auth-server@1.129.0
 - @standardnotes/domain-events-infra@1.12.10
 - @standardnotes/domain-events@2.114.0
 - @standardnotes/event-store@1.11.15
 - @standardnotes/files-server@1.19.18
 - @standardnotes/home-server@1.13.28
 - @standardnotes/revisions-server@1.26.2
 - @standardnotes/scheduler-server@1.20.17
 - @standardnotes/syncing-server@1.75.3
 - @standardnotes/websockets-server@1.10.12
2023-08-03 10:34:26 +00:00
Karol Sójko
8e35dfa4b7 feat(auth): add handling payments account deleted events STA-1769 (#682)
* feat(auth): add handling payments account deleted events

* fix(auth): result type for accounts already deleted

---------

Co-authored-by: Karol Sójko <karolsojko@proton.me>
2023-08-03 12:01:42 +02:00
standardci
f911473be9 chore(release): publish new version
- @standardnotes/analytics@2.25.7
 - @standardnotes/api-gateway@1.69.2
 - @standardnotes/auth-server@1.128.1
 - @standardnotes/domain-core@1.24.2
 - @standardnotes/event-store@1.11.14
 - @standardnotes/files-server@1.19.17
 - @standardnotes/home-server@1.13.27
 - @standardnotes/revisions-server@1.26.1
 - @standardnotes/scheduler-server@1.20.16
 - @standardnotes/settings@1.21.21
 - @standardnotes/syncing-server@1.75.2
 - @standardnotes/websockets-server@1.10.11
2023-08-02 15:51:56 +00:00
Karol Sójko
71624f1897 fix(domain-core): remove unused content types 2023-08-02 17:34:40 +02:00
standardci
17de6ea7e1 chore(release): publish new version
- @standardnotes/home-server@1.13.26
 - @standardnotes/syncing-server@1.75.1
2023-08-02 11:41:20 +00:00
Karol Sójko
6aad7cd207 fix(syncing-server): update unknown content type on items migration 2023-08-02 13:24:14 +02:00
standardci
63af335877 chore(release): publish new version
- @standardnotes/auth-server@1.128.0
 - @standardnotes/home-server@1.13.25
 - @standardnotes/revisions-server@1.26.0
 - @standardnotes/syncing-server@1.75.0
2023-08-02 08:09:24 +00:00
Karol Sójko
8cd7a138ab feat: enable Write Ahead Log mode for SQLite (#681)
Co-authored-by: Karol Sójko <karolsojko@proton.me>
2023-08-02 09:47:37 +02:00
standardci
f69cdc7b03 chore(release): publish new version
- @standardnotes/home-server@1.13.24
 - @standardnotes/syncing-server@1.74.1
 - @standardnotes/websockets-server@1.10.10
2023-08-02 07:35:12 +00:00
Karol Sójko
2ca649cf31 fix(syncing-server): encapsulate delete queries into transactions 2023-08-02 08:35:19 +02:00
Mo
f2ada08201 chore: remove unused package (#680) 2023-08-02 07:46:10 +02:00
Mo
54ba1f69e5 chore: bump mocha timeout 2023-08-01 16:46:19 -05:00
standardci
f13a99f5fd chore(release): publish new version
- @standardnotes/home-server@1.13.23
 - @standardnotes/syncing-server@1.74.0
2023-08-01 16:32:29 +00:00
Karol Sójko
e9bba6fd3a feat(syncing-server): remove legacy privileges items (#679)
Co-authored-by: Karol Sójko <karolsojko@proton.me>
2023-08-01 18:15:16 +02:00
standardci
f0d1a70c87 chore(release): publish new version
- @standardnotes/auth-server@1.127.2
 - @standardnotes/files-server@1.19.16
 - @standardnotes/home-server@1.13.22
 - @standardnotes/revisions-server@1.25.7
 - @standardnotes/syncing-server@1.73.1
 - @standardnotes/websockets-server@1.10.9
2023-08-01 07:53:07 +00:00
Karol Sójko
56f0aef21d fix: controller naming (#678)
* fix: rename home server controllers to base controllers

* fix: rename inversify express controllers to annotated controllers
2023-08-01 09:34:52 +02:00
standardci
75e266cb9e chore(release): publish new version
- @standardnotes/home-server@1.13.21
 - @standardnotes/syncing-server@1.73.0
2023-08-01 05:16:33 +00:00
Karol Sójko
b9bb83c0ce feat(syncing-server): add shared vault snjs filter (#677)
Co-authored-by: Mo <mo@standardnotes.com>
2023-08-01 07:00:14 +02:00
standardci
da645c5ab3 chore(release): publish new version
- @standardnotes/auth-server@1.127.1
 - @standardnotes/home-server@1.13.20
2023-07-31 13:33:09 +00:00
Karol Sójko
318af5757d fix(auth): auth middleware on delete account 2023-07-31 15:09:52 +02:00
standardci
b1cc156a25 chore(release): publish new version
- @standardnotes/api-gateway@1.69.1
 - @standardnotes/home-server@1.13.19
2023-07-31 12:35:22 +00:00
Karol Sójko
79d71ca161 fix(api-gateway): remove duplicating req/res objects on return raw response from payments 2023-07-31 14:19:01 +02:00
standardci
cedd50b366 chore(release): publish new version
- @standardnotes/api-gateway@1.69.0
 - @standardnotes/auth-server@1.127.0
 - @standardnotes/home-server@1.13.18
2023-07-31 11:41:55 +00:00
Karol Sójko
0d5dcdd8ec feat: refactor deleting account (#676)
* fix(api-gateway): TYPES aliases

* feat: refactor account deleting
2023-07-31 13:23:39 +02:00
standardci
d2b0fb144b chore(release): publish new version
- @standardnotes/home-server@1.13.17
 - @standardnotes/syncing-server@1.72.2
2023-07-30 13:24:19 +00:00
Mo
053852b46c fix: missing var reference and brackets (#675) 2023-07-30 08:09:23 -05:00
Karol Sójko
6ad349d379 fix: db name encapsulation 2023-07-28 13:32:13 +02:00
Karol Sójko
f7d33c7164 fix: separate databases and redis instances 2023-07-28 12:00:17 +02:00
Karol Sójko
b53b67328f fix: displaying logs on docker failure 2023-07-28 11:23:16 +02:00
Karol Sójko
573ffbfcf3 fix: add showing logs on docker error 2023-07-28 10:19:32 +02:00
Karol Sójko
501ac0e99f fix: env vars for database runs 2023-07-27 14:32:55 +02:00
Karol Sójko
959a11293a fix: separate database for different configuration 2023-07-27 14:19:21 +02:00
standardci
fee1f1a3a7 chore(release): publish new version
- @standardnotes/analytics@2.25.6
 - @standardnotes/api-gateway@1.68.1
 - @standardnotes/auth-server@1.126.5
 - @standardnotes/domain-core@1.24.1
 - @standardnotes/event-store@1.11.13
 - @standardnotes/files-server@1.19.15
 - @standardnotes/home-server@1.13.16
 - @standardnotes/revisions-server@1.25.6
 - @standardnotes/scheduler-server@1.20.15
 - @standardnotes/settings@1.21.20
 - @standardnotes/syncing-server@1.72.1
 - @standardnotes/websockets-server@1.10.8
2023-07-27 11:10:28 +00:00
Karol Sójko
b0fbe0bb58 fix: extended access token refresh ttl during home server e2e 2023-07-27 12:54:50 +02:00
Karol Sójko
0087c70007 fix: missing env var on e2e 2023-07-27 12:25:31 +02:00
Karol Sójko
36e496dd7c fix: remove dns aliases on accessing mysql and redis in home server e2e 2023-07-27 12:19:01 +02:00
Karol Sójko
f2e2030e85 fix: disable fail-fast on common e2e suite 2023-07-27 12:11:38 +02:00
Karol Sójko
0c3737dc19 fix: redirect STDERR for e2e to common output 2023-07-27 12:07:32 +02:00
Karol Sójko
f7471119e1 fix: add logging error stack on home server failure 2023-07-27 12:00:40 +02:00
Karol Sójko
9bd97b95e9 fix: unset the custom dbsqlite path for e2e 2023-07-27 11:46:48 +02:00
Karol Sójko
b7400c198f fix: setting env vars on common-e2e test suite 2023-07-27 11:30:49 +02:00
Karol Sójko
f87036e3a8 fix: setting env vars on home server in e2e environment 2023-07-27 11:26:05 +02:00
Karol Sójko
a43e5ef724 fix: outputing logs on e2e - already existing logs directory 2023-07-27 10:32:43 +02:00
Karol Sójko
913ced70b0 fix: outputing logs on e2e 2023-07-27 08:33:49 +02:00
standardci
6ffce30a36 chore(release): publish new version
- @standardnotes/api-gateway@1.68.0
 - @standardnotes/home-server@1.13.15
 - @standardnotes/syncing-server@1.72.0
2023-07-27 06:28:46 +00:00
Karol Sójko
f5a57d886c fix: show logs on failing suite 2023-07-27 08:13:49 +02:00
Karol Sójko
e8ba49ecca feat(syncing-server): add deleting outbound messages
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-27 08:06:56 +02:00
Karol Sójko
c79a5dc94b fix: add e2e yarn command for convenience 2023-07-26 15:23:08 +02:00
standardci
4db83ae678 chore(release): publish new version
- @standardnotes/analytics@2.25.5
 - @standardnotes/api-gateway@1.67.4
 - @standardnotes/auth-server@1.126.4
 - @standardnotes/domain-core@1.24.0
 - @standardnotes/event-store@1.11.12
 - @standardnotes/files-server@1.19.14
 - @standardnotes/home-server@1.13.14
 - @standardnotes/revisions-server@1.25.5
 - @standardnotes/scheduler-server@1.20.14
 - @standardnotes/settings@1.21.19
 - @standardnotes/syncing-server@1.71.0
 - @standardnotes/websockets-server@1.10.7
2023-07-26 12:07:57 +00:00
basiljelly
84ceb7ffd2 Update docker-compose.example.yml (#673)
added restart policy for container server_self_hosted so it restarts after rebooting system
2023-07-26 13:52:40 +02:00
Karol Sójko
e215ac4343 feat: extract shared vault user permission to domain-core 2023-07-26 13:45:53 +02:00
standardci
bc8048790f chore(release): publish new version
- @standardnotes/home-server@1.13.13
 - @standardnotes/syncing-server@1.70.5
2023-07-26 11:19:11 +00:00
Karol Sójko
886ccf84c1 fix(syncing-server): uuid comparison when removing user 2023-07-26 13:02:03 +02:00
standardci
c067cb9fe4 chore(release): publish new version
- @standardnotes/home-server@1.13.12
 - @standardnotes/syncing-server@1.70.4
2023-07-26 10:54:46 +00:00
Karol Sójko
6b2389cdc3 fix(syncing-serve): removing other users from shared vault 2023-07-26 12:39:24 +02:00
standardci
d93916b159 chore(release): publish new version
- @standardnotes/analytics@2.25.4
 - @standardnotes/api-gateway@1.67.3
 - @standardnotes/auth-server@1.126.3
 - @standardnotes/domain-core@1.23.4
 - @standardnotes/event-store@1.11.11
 - @standardnotes/files-server@1.19.13
 - @standardnotes/home-server@1.13.11
 - @standardnotes/revisions-server@1.25.4
 - @standardnotes/scheduler-server@1.20.13
 - @standardnotes/settings@1.21.18
 - @standardnotes/syncing-server@1.70.3
 - @standardnotes/websockets-server@1.10.6
2023-07-26 10:38:44 +00:00
Karol Sójko
c34f548e45 fix(syncing-server): persisting aggregate changes from root (#674) 2023-07-26 12:23:10 +02:00
standardci
6fcd56cc86 chore(release): publish new version
- @standardnotes/home-server@1.13.10
 - @standardnotes/syncing-server@1.70.2
2023-07-25 14:07:10 +00:00
Karol Sójko
8f88a87c93 fix(syncing-server): remove notifications after adding item to vault (#672) 2023-07-25 15:51:42 +02:00
standardci
f8c2f84322 chore(release): publish new version
- @standardnotes/home-server@1.13.9
 - @standardnotes/syncing-server@1.70.1
2023-07-25 11:25:55 +00:00
Karol Sójko
46c4947871 fix(syncing-server): allow sender to decline the invite (#671) 2023-07-25 13:05:10 +02:00
standardci
64759ec2da chore(release): publish new version
- @standardnotes/home-server@1.13.8
 - @standardnotes/syncing-server@1.70.0
2023-07-25 11:03:19 +00:00
Karol Sójko
5f7e768e64 feat(syncing-server): filtering items by shared vault permissions (#670)
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-25 12:45:49 +02:00
standardci
4bc189f1c5 chore(release): publish new version
- @standardnotes/home-server@1.13.7
 - @standardnotes/syncing-server@1.69.0
2023-07-24 14:29:21 +00:00
Karol Sójko
71721ab198 feat(syncing-server): determin shared vault operation type (#669)
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-24 15:29:56 +02:00
standardci
5536a48966 chore(release): publish new version
- @standardnotes/home-server@1.13.6
 - @standardnotes/syncing-server@1.68.4
2023-07-24 09:15:15 +00:00
Karol Sójko
f77e29d3c9 fix(syncing-server): force remove shared vault owner when removing shared vault 2023-07-24 10:59:03 +02:00
351 changed files with 6438 additions and 2035 deletions

View File

@@ -50,14 +50,30 @@ jobs:
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
- name: Run E2E Test Suite
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
- name: Show logs on failure
if: ${{ failure() }}
run: |
echo "# Errors:"
tail -n 100 logs/*.err
echo "# Logs:"
tail -n 100 logs/*.log
e2e-home-server:
name: (Home Server) E2E Test Suite
strategy:
fail-fast: false
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
runs-on: ubuntu-latest
@@ -69,14 +85,14 @@ jobs:
cache:
image: redis
ports:
- 6379:6379
- ${{ matrix.redis_port }}:6379
db:
image: mysql
ports:
- 3306:3306
- 3307:3306
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: standardnotes
MYSQL_DATABASE: standardnotes_${{ matrix.cache_type }}
MYSQL_USER: standardnotes
MYSQL_PASSWORD: standardnotes
@@ -106,20 +122,22 @@ jobs:
sed -i "s/PSEUDO_KEY_PARAMS_KEY=/PSEUDO_KEY_PARAMS_KEY=$(openssl rand -hex 32)/g" packages/home-server/.env
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=7" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=5" >> packages/home-server/.env
echo "DB_HOST=db" >> packages/home-server/.env
echo "DB_PORT=3306" >> 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_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://cache" >> packages/home-server/.env
echo "REDIS_URL=redis://localhost:${{ matrix.redis_port }}" >> packages/home-server/.env
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
echo "FILES_SERVER_URL=http://localhost:3123" >> packages/home-server/.env
echo "E2E_TESTING=true" >> packages/home-server/.env
- name: Run Server
run: nohup yarn workspace @standardnotes/home-server start &
run: nohup yarn workspace @standardnotes/home-server start > logs/output.log 2>&1 &
env:
PORT: 3123
@@ -127,4 +145,8 @@ 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 1200000 -f http://localhost:9001/mocha/test.html
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
- name: Show logs on failure
if: ${{ failure() }}
run: tail -n 500 logs/output.log

1
.pnp.cjs generated
View File

@@ -5259,7 +5259,6 @@ const RAW_RUNTIME_STATE =
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/responses", "npm:1.13.27"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/utils", "npm:1.17.5"],\
["@types/cors", "npm:2.8.13"],\
["@types/express", "npm:4.17.17"],\
["@types/ioredis", "npm:5.0.0"],\

View File

@@ -3,6 +3,7 @@ services:
image: standardnotes/server
env_file: .env
container_name: server_self_hosted
restart: unless-stopped
ports:
- 3000:3000
- 3125:3104

View File

@@ -12,12 +12,15 @@
},
"scripts": {
"lint": "yarn workspaces foreach -p -j 10 --verbose run lint",
"lint:fix": "yarn workspaces foreach -p -j 10 --verbose run lint:fix",
"clean": "yarn workspaces foreach -p --verbose run clean",
"setup:env": "cp .env.sample .env && yarn workspaces foreach -p --verbose run setup:env",
"release": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish new version\"",
"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"
"upgrade:snjs": "yarn workspaces foreach --verbose run upgrade:snjs",
"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",

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [2.25.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.6...@standardnotes/analytics@2.25.7) (2023-08-02)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.5...@standardnotes/analytics@2.25.6) (2023-07-27)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.4...@standardnotes/analytics@2.25.5) (2023-07-26)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.3...@standardnotes/analytics@2.25.4) (2023-07-26)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.2...@standardnotes/analytics@2.25.3) (2023-07-21)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.25.3",
"version": "2.25.10",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,58 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.69.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.1...@standardnotes/api-gateway@1.69.2) (2023-08-02)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.69.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.0...@standardnotes/api-gateway@1.69.1) (2023-07-31)
### Bug Fixes
* **api-gateway:** remove duplicating req/res objects on return raw response from payments ([79d71ca](https://github.com/standardnotes/api-gateway/commit/79d71ca161cc18135fcd1a83b021662e189b3ddb))
# [1.69.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.68.1...@standardnotes/api-gateway@1.69.0) (2023-07-31)
### Features
* refactor deleting account ([#676](https://github.com/standardnotes/api-gateway/issues/676)) ([0d5dcdd](https://github.com/standardnotes/api-gateway/commit/0d5dcdd8ec2336e41e7604c4157f79a89163ed29))
## [1.68.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.68.0...@standardnotes/api-gateway@1.68.1) (2023-07-27)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.68.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.67.4...@standardnotes/api-gateway@1.68.0) (2023-07-27)
### Features
* **syncing-server:** add deleting outbound messages ([e8ba49e](https://github.com/standardnotes/api-gateway/commit/e8ba49ecca38ab10c0ea0e1f4cf4db9fb17366db))
## [1.67.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.67.3...@standardnotes/api-gateway@1.67.4) (2023-07-26)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.67.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.67.2...@standardnotes/api-gateway@1.67.3) (2023-07-26)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.67.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.67.1...@standardnotes/api-gateway@1.67.2) (2023-07-21)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -46,7 +46,7 @@ void container.load().then((container) => {
server.setConfig((app) => {
app.use((_request: Request, response: Response, next: NextFunction) => {
response.setHeader('X-API-Gateway-Version', container.get(TYPES.VERSION))
response.setHeader('X-API-Gateway-Version', container.get(TYPES.ApiGateway_VERSION))
next()
})
app.use(
@@ -87,7 +87,7 @@ void container.load().then((container) => {
)
})
const logger: winston.Logger = container.get(TYPES.Logger)
const logger: winston.Logger = container.get(TYPES.ApiGateway_Logger)
server.setErrorConfig((app) => {
app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.67.2",
"version": "1.70.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -21,6 +21,7 @@
"clean": "rm -fr dist",
"build": "tsc --build",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --fix --ext .ts",
"setup:env": "cp .env.sample .env",
"start": "yarn node dist/bin/server.js",
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'"

View File

@@ -57,7 +57,7 @@ export class ContainerConfigLoader {
defaultMeta: { service: 'api-gateway' },
})
}
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
container.bind<winston.Logger>(TYPES.ApiGateway_Logger).toConstantValue(logger)
if (!isConfiguredForInMemoryCache) {
const redisUrl = env.get('REDIS_URL')
@@ -68,36 +68,39 @@ export class ContainerConfigLoader {
} else {
redis = new Redis(redisUrl)
}
container.bind(TYPES.Redis).toConstantValue(redis)
container.bind(TYPES.ApiGateway_Redis).toConstantValue(redis)
}
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
container.bind<AxiosInstance>(TYPES.ApiGateway_HTTPClient).toConstantValue(axios.create())
// env vars
container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL', true))
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL', true))
container.bind(TYPES.REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL', true))
container.bind(TYPES.EMAIL_SERVER_URL).toConstantValue(env.get('EMAIL_SERVER_URL', true))
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
container.bind(TYPES.WEB_SOCKET_SERVER_URL).toConstantValue(env.get('WEB_SOCKET_SERVER_URL', true))
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
container.bind(TYPES.ApiGateway_SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL', true))
container.bind(TYPES.ApiGateway_AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL', true))
container.bind(TYPES.ApiGateway_REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL', true))
container.bind(TYPES.ApiGateway_EMAIL_SERVER_URL).toConstantValue(env.get('EMAIL_SERVER_URL', true))
container.bind(TYPES.ApiGateway_PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
container.bind(TYPES.ApiGateway_FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
container.bind(TYPES.ApiGateway_WEB_SOCKET_SERVER_URL).toConstantValue(env.get('WEB_SOCKET_SERVER_URL', true))
container.bind(TYPES.ApiGateway_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
container
.bind(TYPES.HTTP_CALL_TIMEOUT)
.bind(TYPES.ApiGateway_HTTP_CALL_TIMEOUT)
.toConstantValue(env.get('HTTP_CALL_TIMEOUT', true) ? +env.get('HTTP_CALL_TIMEOUT', true) : 60_000)
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
container.bind(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL).toConstantValue(+env.get('CROSS_SERVICE_TOKEN_CACHE_TTL', true))
container.bind(TYPES.ApiGateway_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
container
.bind(TYPES.ApiGateway_CROSS_SERVICE_TOKEN_CACHE_TTL)
.toConstantValue(+env.get('CROSS_SERVICE_TOKEN_CACHE_TTL', true))
container.bind(TYPES.ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER).toConstantValue(isConfiguredForHomeServer)
// Middleware
container
.bind<RequiredCrossServiceTokenMiddleware>(TYPES.RequiredCrossServiceTokenMiddleware)
.bind<RequiredCrossServiceTokenMiddleware>(TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
.to(RequiredCrossServiceTokenMiddleware)
container
.bind<OptionalCrossServiceTokenMiddleware>(TYPES.OptionalCrossServiceTokenMiddleware)
.bind<OptionalCrossServiceTokenMiddleware>(TYPES.ApiGateway_OptionalCrossServiceTokenMiddleware)
.to(OptionalCrossServiceTokenMiddleware)
container.bind<WebSocketAuthMiddleware>(TYPES.WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
container.bind<WebSocketAuthMiddleware>(TYPES.ApiGateway_WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
container
.bind<SubscriptionTokenAuthMiddleware>(TYPES.SubscriptionTokenAuthMiddleware)
.bind<SubscriptionTokenAuthMiddleware>(TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
.to(SubscriptionTokenAuthMiddleware)
// Services
@@ -106,24 +109,26 @@ export class ContainerConfigLoader {
throw new Error('Service container is required when configured for home server')
}
container
.bind<ServiceProxyInterface>(TYPES.ServiceProxy)
.bind<ServiceProxyInterface>(TYPES.ApiGateway_ServiceProxy)
.toConstantValue(
new DirectCallServiceProxy(configuration.serviceContainer, container.get(TYPES.FILES_SERVER_URL)),
new DirectCallServiceProxy(configuration.serviceContainer, container.get(TYPES.ApiGateway_FILES_SERVER_URL)),
)
} else {
container.bind<ServiceProxyInterface>(TYPES.ServiceProxy).to(HttpServiceProxy)
container.bind<ServiceProxyInterface>(TYPES.ApiGateway_ServiceProxy).to(HttpServiceProxy)
}
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
container.bind<TimerInterface>(TYPES.ApiGateway_Timer).toConstantValue(new Timer())
if (isConfiguredForHomeServer) {
container
.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache)
.toConstantValue(new InMemoryCrossServiceTokenCache(container.get(TYPES.Timer)))
.bind<CrossServiceTokenCacheInterface>(TYPES.ApiGateway_CrossServiceTokenCache)
.toConstantValue(new InMemoryCrossServiceTokenCache(container.get(TYPES.ApiGateway_Timer)))
} else {
container.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache).to(RedisCrossServiceTokenCache)
container
.bind<CrossServiceTokenCacheInterface>(TYPES.ApiGateway_CrossServiceTokenCache)
.to(RedisCrossServiceTokenCache)
}
container
.bind<EndpointResolverInterface>(TYPES.EndpointResolver)
.bind<EndpointResolverInterface>(TYPES.ApiGateway_EndpointResolver)
.toConstantValue(new EndpointResolver(isConfiguredForHomeServer))
logger.debug('Configuration complete')

View File

@@ -1,29 +1,28 @@
export const TYPES = {
Logger: Symbol.for('Logger'),
Redis: Symbol.for('Redis'),
HTTPClient: Symbol.for('HTTPClient'),
ApiGateway_Logger: Symbol.for('ApiGateway_Logger'),
ApiGateway_Redis: Symbol.for('ApiGateway_Redis'),
ApiGateway_HTTPClient: Symbol.for('ApiGateway_HTTPClient'),
// env vars
SYNCING_SERVER_JS_URL: Symbol.for('SYNCING_SERVER_JS_URL'),
AUTH_SERVER_URL: Symbol.for('AUTH_SERVER_URL'),
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
REVISIONS_SERVER_URL: Symbol.for('REVISIONS_SERVER_URL'),
EMAIL_SERVER_URL: Symbol.for('EMAIL_SERVER_URL'),
WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'),
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
HTTP_CALL_TIMEOUT: Symbol.for('HTTP_CALL_TIMEOUT'),
VERSION: Symbol.for('VERSION'),
CROSS_SERVICE_TOKEN_CACHE_TTL: Symbol.for('CROSS_SERVICE_TOKEN_CACHE_TTL'),
ApiGateway_SYNCING_SERVER_JS_URL: Symbol.for('ApiGateway_SYNCING_SERVER_JS_URL'),
ApiGateway_AUTH_SERVER_URL: Symbol.for('ApiGateway_AUTH_SERVER_URL'),
ApiGateway_PAYMENTS_SERVER_URL: Symbol.for('ApiGateway_PAYMENTS_SERVER_URL'),
ApiGateway_FILES_SERVER_URL: Symbol.for('ApiGateway_FILES_SERVER_URL'),
ApiGateway_REVISIONS_SERVER_URL: Symbol.for('ApiGateway_REVISIONS_SERVER_URL'),
ApiGateway_EMAIL_SERVER_URL: Symbol.for('ApiGateway_EMAIL_SERVER_URL'),
ApiGateway_WEB_SOCKET_SERVER_URL: Symbol.for('ApiGateway_WEB_SOCKET_SERVER_URL'),
ApiGateway_AUTH_JWT_SECRET: Symbol.for('ApiGateway_AUTH_JWT_SECRET'),
ApiGateway_HTTP_CALL_TIMEOUT: Symbol.for('ApiGateway_HTTP_CALL_TIMEOUT'),
ApiGateway_VERSION: Symbol.for('ApiGateway_VERSION'),
ApiGateway_CROSS_SERVICE_TOKEN_CACHE_TTL: Symbol.for('ApiGateway_CROSS_SERVICE_TOKEN_CACHE_TTL'),
ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER: Symbol.for('ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER'),
// Middleware
RequiredCrossServiceTokenMiddleware: Symbol.for('RequiredCrossServiceTokenMiddleware'),
OptionalCrossServiceTokenMiddleware: Symbol.for('OptionalCrossServiceTokenMiddleware'),
WebSocketAuthMiddleware: Symbol.for('WebSocketAuthMiddleware'),
SubscriptionTokenAuthMiddleware: Symbol.for('SubscriptionTokenAuthMiddleware'),
ApiGateway_RequiredCrossServiceTokenMiddleware: Symbol.for('ApiGateway_RequiredCrossServiceTokenMiddleware'),
ApiGateway_OptionalCrossServiceTokenMiddleware: Symbol.for('ApiGateway_OptionalCrossServiceTokenMiddleware'),
ApiGateway_WebSocketAuthMiddleware: Symbol.for('ApiGateway_WebSocketAuthMiddleware'),
ApiGateway_SubscriptionTokenAuthMiddleware: Symbol.for('ApiGateway_SubscriptionTokenAuthMiddleware'),
// Services
ServiceProxy: Symbol.for('ServiceProxy'),
CrossServiceTokenCache: Symbol.for('CrossServiceTokenCache'),
Timer: Symbol.for('Timer'),
EndpointResolver: Symbol.for('EndpointResolver'),
ApiGateway_ServiceProxy: Symbol.for('ApiGateway_ServiceProxy'),
ApiGateway_CrossServiceTokenCache: Symbol.for('ApiGateway_CrossServiceTokenCache'),
ApiGateway_Timer: Symbol.for('ApiGateway_Timer'),
ApiGateway_EndpointResolver: Symbol.for('ApiGateway_EndpointResolver'),
}
// export default TYPES

View File

@@ -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'
@@ -51,10 +50,6 @@ export abstract class AuthMiddleware 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
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({
authorizationHeaderValue: authHeaderValue,

View File

@@ -9,7 +9,7 @@ export class LegacyController extends BaseHttpController {
private AUTH_ROUTES: Map<string, string>
private PARAMETRIZED_AUTH_ROUTES: Map<string, string>
constructor(@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface) {
constructor(@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface) {
super()
this.AUTH_ROUTES = new Map([
@@ -29,17 +29,17 @@ export class LegacyController extends BaseHttpController {
])
}
@httpPost('/items/sync', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/items/sync', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async legacyItemsSync(request: Request, response: Response): Promise<void> {
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
}
@httpGet('/items/:item_id/revisions', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/items/:item_id/revisions', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async legacyGetRevisions(request: Request, response: Response): Promise<void> {
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
}
@httpGet('/items/:item_id/revisions/:id', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/items/:item_id/revisions/:id', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async legacyGetRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
}

View File

@@ -11,12 +11,12 @@ import { AuthMiddleware } from './AuthMiddleware'
@injectable()
export class OptionalCrossServiceTokenMiddleware extends AuthMiddleware {
constructor(
@inject(TYPES.ServiceProxy) serviceProxy: ServiceProxyInterface,
@inject(TYPES.AUTH_JWT_SECRET) jwtSecret: string,
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) crossServiceTokenCacheTTL: number,
@inject(TYPES.CrossServiceTokenCache) crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Timer) timer: TimerInterface,
@inject(TYPES.Logger) logger: Logger,
@inject(TYPES.ApiGateway_ServiceProxy) serviceProxy: ServiceProxyInterface,
@inject(TYPES.ApiGateway_AUTH_JWT_SECRET) jwtSecret: string,
@inject(TYPES.ApiGateway_CROSS_SERVICE_TOKEN_CACHE_TTL) crossServiceTokenCacheTTL: number,
@inject(TYPES.ApiGateway_CrossServiceTokenCache) crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.ApiGateway_Timer) timer: TimerInterface,
@inject(TYPES.ApiGateway_Logger) logger: Logger,
) {
super(serviceProxy, jwtSecret, crossServiceTokenCacheTTL, crossServiceTokenCache, timer, logger)
}

View File

@@ -11,12 +11,12 @@ import { AuthMiddleware } from './AuthMiddleware'
@injectable()
export class RequiredCrossServiceTokenMiddleware extends AuthMiddleware {
constructor(
@inject(TYPES.ServiceProxy) serviceProxy: ServiceProxyInterface,
@inject(TYPES.AUTH_JWT_SECRET) jwtSecret: string,
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) crossServiceTokenCacheTTL: number,
@inject(TYPES.CrossServiceTokenCache) crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Timer) timer: TimerInterface,
@inject(TYPES.Logger) logger: Logger,
@inject(TYPES.ApiGateway_ServiceProxy) serviceProxy: ServiceProxyInterface,
@inject(TYPES.ApiGateway_AUTH_JWT_SECRET) jwtSecret: string,
@inject(TYPES.ApiGateway_CROSS_SERVICE_TOKEN_CACHE_TTL) crossServiceTokenCacheTTL: number,
@inject(TYPES.ApiGateway_CrossServiceTokenCache) crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.ApiGateway_Timer) timer: TimerInterface,
@inject(TYPES.ApiGateway_Logger) logger: Logger,
) {
super(serviceProxy, jwtSecret, crossServiceTokenCacheTTL, crossServiceTokenCache, timer, logger)
}

View File

@@ -11,10 +11,10 @@ import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
@injectable()
export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
@inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.ApiGateway_HTTPClient) private httpClient: AxiosInstance,
@inject(TYPES.ApiGateway_AUTH_SERVER_URL) private authServerUrl: string,
@inject(TYPES.ApiGateway_AUTH_JWT_SECRET) private jwtSecret: string,
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
) {
super()
}

View File

@@ -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'
@@ -12,10 +11,10 @@ import { TYPES } from '../Bootstrap/Types'
@injectable()
export class WebSocketAuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
@inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.ApiGateway_HTTPClient) private httpClient: AxiosInstance,
@inject(TYPES.ApiGateway_AUTH_SERVER_URL) private authServerUrl: string,
@inject(TYPES.ApiGateway_AUTH_JWT_SECRET) private jwtSecret: string,
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
) {
super()
}
@@ -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) {

View File

@@ -8,8 +8,8 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v1')
export class ActionsController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}
@@ -24,7 +24,7 @@ export class ActionsController extends BaseHttpController {
)
}
@httpGet('/login-params', TYPES.OptionalCrossServiceTokenMiddleware)
@httpGet('/login-params', TYPES.ApiGateway_OptionalCrossServiceTokenMiddleware)
async loginParams(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,
@@ -34,7 +34,7 @@ export class ActionsController extends BaseHttpController {
)
}
@httpPost('/logout', TYPES.OptionalCrossServiceTokenMiddleware)
@httpPost('/logout', TYPES.ApiGateway_OptionalCrossServiceTokenMiddleware)
async logout(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,
@@ -54,7 +54,7 @@ export class ActionsController extends BaseHttpController {
)
}
@httpPost('/recovery/codes', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/recovery/codes', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async recoveryCodes(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,

View File

@@ -9,13 +9,13 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v1/authenticators')
export class AuthenticatorsController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}
@httpDelete('/:authenticatorId', TYPES.RequiredCrossServiceTokenMiddleware)
@httpDelete('/:authenticatorId', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async delete(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -29,7 +29,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpGet('/', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async list(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -39,7 +39,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpGet('/generate-registration-options', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/generate-registration-options', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async generateRegistrationOptions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -59,7 +59,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpPost('/verify-registration', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/verify-registration', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async verifyRegistration(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -9,13 +9,13 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v1/files')
export class FilesController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}
@httpPost('/valet-tokens', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/valet-tokens', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async createToken(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -6,11 +6,11 @@ import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
@controller('/v1')
export class InvoicesController extends BaseHttpController {
constructor(@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface) {
constructor(@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface) {
super()
}
@httpPost('/invoices/send-latest', TYPES.SubscriptionTokenAuthMiddleware)
@httpPost('/invoices/send-latest', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async sendLatestInvoice(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/send-invoice', request.body)
}

View File

@@ -5,11 +5,11 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v1/items', TYPES.RequiredCrossServiceTokenMiddleware)
@controller('/v1/items', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
export class ItemsController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}

View File

@@ -5,11 +5,11 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v1/messages', TYPES.RequiredCrossServiceTokenMiddleware)
@controller('/v1/messages', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
export class MessagesController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}

View File

@@ -9,8 +9,8 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v1/offline')
export class OfflineController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}

View File

@@ -6,7 +6,7 @@ import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
@controller('/v1')
export class PaymentsController extends BaseHttpController {
constructor(@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface) {
constructor(@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface) {
super()
}
@@ -40,12 +40,12 @@ export class PaymentsController extends BaseHttpController {
await this.httpService.callPaymentsServer(request, response, 'api/extensions', request.body)
}
@httpPost('/subscriptions/tiered', TYPES.SubscriptionTokenAuthMiddleware)
@httpPost('/subscriptions/tiered', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async createTieredSubscription(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/tiered', request.body)
}
@httpPost('/subscriptions/apple_iap_confirm', TYPES.SubscriptionTokenAuthMiddleware)
@httpPost('/subscriptions/apple_iap_confirm', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async appleIAPConfirm(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/apple_iap_confirm', request.body)
}
@@ -140,7 +140,7 @@ export class PaymentsController extends BaseHttpController {
)
}
@httpPost('/payments/stripe-setup-intent', TYPES.SubscriptionTokenAuthMiddleware)
@httpPost('/payments/stripe-setup-intent', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async createStripeSetupIntent(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/stripe-setup-intent', request.body)
}

View File

@@ -1,7 +1,7 @@
import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils'
import { TYPES } from '../../Bootstrap/Types'
@controller('/v1/items/:item_id/revisions', TYPES.RequiredCrossServiceTokenMiddleware)
@controller('/v1/items/:item_id/revisions', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
export class RevisionsController extends BaseHttpController {
@httpGet('/')
async getRevisions(): Promise<results.JsonResult> {

View File

@@ -8,13 +8,13 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v1/sessions')
export class SessionsController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}
@httpGet('/', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getSessions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -23,7 +23,7 @@ export class SessionsController extends BaseHttpController {
)
}
@httpDelete('/:uuid', TYPES.RequiredCrossServiceTokenMiddleware)
@httpDelete('/:uuid', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async deleteSession(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -35,7 +35,7 @@ export class SessionsController extends BaseHttpController {
)
}
@httpDelete('/', TYPES.RequiredCrossServiceTokenMiddleware)
@httpDelete('/', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async deleteSessions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -5,11 +5,11 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v1/shared-vaults', TYPES.RequiredCrossServiceTokenMiddleware)
@controller('/v1/shared-vaults', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
export class SharedVaultInvitesController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}
@@ -83,6 +83,16 @@ export class SharedVaultInvitesController extends BaseHttpController {
)
}
@httpDelete('/invites/outbound')
async deleteOutboundUserInvites(request: Request, response: Response): Promise<void> {
await this.httpService.callSyncingServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('DELETE', 'shared-vaults/invites/outbound'),
request.body,
)
}
@httpGet('/invites/outbound')
async getOutboundUserInvites(request: Request, response: Response): Promise<void> {
await this.httpService.callSyncingServer(

View File

@@ -5,11 +5,11 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v1/shared-vaults/:sharedVaultUuid/users', TYPES.RequiredCrossServiceTokenMiddleware)
@controller('/v1/shared-vaults/:sharedVaultUuid/users', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
export class SharedVaultUsersController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}

View File

@@ -5,11 +5,11 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v1/shared-vaults', TYPES.RequiredCrossServiceTokenMiddleware)
@controller('/v1/shared-vaults', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
export class SharedVaultsController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}

View File

@@ -9,13 +9,13 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v1/subscription-invites')
export class SubscriptionInvitesController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}
@httpPost('/', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -25,7 +25,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
)
}
@httpGet('/', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async listInvites(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -35,7 +35,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
)
}
@httpDelete('/:inviteUuid', TYPES.RequiredCrossServiceTokenMiddleware)
@httpDelete('/:inviteUuid', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async cancelSubscriptionSharing(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -48,7 +48,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
)
}
@httpPost('/:inviteUuid/accept', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/:inviteUuid/accept', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async acceptInvite(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -9,13 +9,13 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v1/subscription-tokens')
export class TokensController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}
@httpPost('/', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async createToken(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -20,9 +20,10 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v1/users')
export class UsersController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
@inject(TYPES.ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER) private isConfiguredForHomeServer: boolean,
) {
super()
}
@@ -32,12 +33,12 @@ export class UsersController extends BaseHttpController {
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/claim-account', request.body)
}
@httpPost('/send-activation-code', TYPES.SubscriptionTokenAuthMiddleware)
@httpPost('/send-activation-code', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async sendActivationCode(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/send-activation-code', request.body)
}
@httpPatch('/:userId', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPatch('/:userId', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async updateUser(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -47,7 +48,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpPut('/:userUuid/password', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPut('/:userUuid/password', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async changePassword(request: Request, response: Response): Promise<void> {
this.logger.debug(
'[DEPRECATED] use endpoint /v1/users/:userUuid/attributes/credentials instead of /v1/users/:userUuid/password',
@@ -65,7 +66,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpPut('/:userUuid/attributes/credentials', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPut('/:userUuid/attributes/credentials', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async changeCredentials(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -79,7 +80,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userId/params', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/:userId/params', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getKeyParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -88,12 +89,12 @@ export class UsersController extends BaseHttpController {
)
}
@all('/:userId/mfa', TYPES.RequiredCrossServiceTokenMiddleware)
@all('/:userId/mfa', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async blockMFA(): Promise<results.StatusCodeResult> {
return this.statusCode(401)
}
@httpPost('/:userUuid/integrations/listed', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/:userUuid/integrations/listed', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async createListedAccount(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -113,7 +114,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/settings', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/:userUuid/settings', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async listSettings(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -126,7 +127,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpPut('/:userUuid/settings', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPut('/:userUuid/settings', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async putSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -140,7 +141,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/settings/:settingName', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/:userUuid/settings/:settingName', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -154,7 +155,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpDelete('/:userUuid/settings/:settingName', TYPES.RequiredCrossServiceTokenMiddleware)
@httpDelete('/:userUuid/settings/:settingName', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async deleteSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -169,7 +170,10 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/subscription-settings/:subscriptionSettingName', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet(
'/:userUuid/subscription-settings/:subscriptionSettingName',
TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware,
)
async getSubscriptionSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -183,7 +187,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/features', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/:userUuid/features', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getFeatures(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -196,7 +200,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/subscription', TYPES.RequiredCrossServiceTokenMiddleware)
@httpGet('/:userUuid/subscription', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getSubscription(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -209,7 +213,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/subscription', TYPES.SubscriptionTokenAuthMiddleware)
@httpGet('/subscription', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async getSubscriptionBySubscriptionToken(request: Request, response: Response): Promise<void> {
if (response.locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) {
await this.httpService.callAuthServer(
@@ -232,12 +236,20 @@ export class UsersController extends BaseHttpController {
)
}
@httpDelete('/:userUuid', TYPES.RequiredCrossServiceTokenMiddleware)
@httpDelete('/:userUuid', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async deleteUser(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/account', request.body)
if (!this.isConfiguredForHomeServer) {
await this.httpService.callPaymentsServer(request, response, 'api/account', request.body, true)
}
await this.httpService.callAuthServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('DELETE', 'users/:userUuid', request.params.userUuid),
)
}
@httpPost('/:userUuid/requests', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/:userUuid/requests', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async submitRequest(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -10,14 +10,14 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v1/sockets')
export class WebSocketsController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
) {
super()
}
@httpPost('/tokens', TYPES.RequiredCrossServiceTokenMiddleware)
@httpPost('/tokens', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async createWebSocketConnectionToken(request: Request, response: Response): Promise<void> {
await this.httpService.callWebSocketServer(
request,
@@ -27,7 +27,7 @@ export class WebSocketsController extends BaseHttpController {
)
}
@httpPost('/connections', TYPES.WebSocketAuthMiddleware)
@httpPost('/connections', TYPES.ApiGateway_WebSocketAuthMiddleware)
async createWebSocketConnection(request: Request, response: Response): Promise<void> {
if (!request.headers.connectionid) {
this.logger.error('Could not create a websocket connection. Missing connection id header.')

View File

@@ -9,8 +9,8 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v2')
export class ActionsControllerV2 extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}
@@ -25,7 +25,7 @@ export class ActionsControllerV2 extends BaseHttpController {
)
}
@httpPost('/login-params', TYPES.OptionalCrossServiceTokenMiddleware)
@httpPost('/login-params', TYPES.ApiGateway_OptionalCrossServiceTokenMiddleware)
async loginParams(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,

View File

@@ -6,7 +6,7 @@ import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
@controller('/v2')
export class PaymentsControllerV2 extends BaseHttpController {
constructor(@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface) {
constructor(@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface) {
super()
}
@@ -15,22 +15,22 @@ export class PaymentsControllerV2 extends BaseHttpController {
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/features', request.body)
}
@httpGet('/subscriptions/tailored', TYPES.SubscriptionTokenAuthMiddleware)
@httpGet('/subscriptions/tailored', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async getTailoredSubscriptionsWithFeatures(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/features', request.body)
}
@httpGet('/subscriptions/deltas', TYPES.SubscriptionTokenAuthMiddleware)
@httpGet('/subscriptions/deltas', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async getSubscriptionDeltasForChangingPlan(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/deltas', request.body)
}
@httpPost('/subscriptions/deltas/apply', TYPES.SubscriptionTokenAuthMiddleware)
@httpPost('/subscriptions/deltas/apply', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async applySubscriptionDelta(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/deltas/apply', request.body)
}
@httpPost('/subscriptions/change-payment-method', TYPES.SubscriptionTokenAuthMiddleware)
@httpPost('/subscriptions/change-payment-method', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async changePaymentMethod(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(
request,
@@ -40,7 +40,7 @@ export class PaymentsControllerV2 extends BaseHttpController {
)
}
@httpGet('/subscriptions/:subscriptionId', TYPES.SubscriptionTokenAuthMiddleware)
@httpGet('/subscriptions/:subscriptionId', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async getSubscription(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(
request,
@@ -50,7 +50,7 @@ export class PaymentsControllerV2 extends BaseHttpController {
)
}
@httpDelete('/subscriptions/:subscriptionId', TYPES.SubscriptionTokenAuthMiddleware)
@httpDelete('/subscriptions/:subscriptionId', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async cancelSubscription(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(
request,
@@ -60,7 +60,7 @@ export class PaymentsControllerV2 extends BaseHttpController {
)
}
@httpPatch('/subscriptions/:subscriptionId', TYPES.SubscriptionTokenAuthMiddleware)
@httpPatch('/subscriptions/:subscriptionId', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async updateSubscription(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(
request,

View File

@@ -6,11 +6,11 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v2/items/:itemUuid/revisions', TYPES.RequiredCrossServiceTokenMiddleware)
@controller('/v2/items/:itemUuid/revisions', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
export class RevisionsControllerV2 extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
}

View File

@@ -9,7 +9,7 @@ export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterf
private readonly PREFIX = 'cst'
private readonly USER_CST_PREFIX = 'user-cst'
constructor(@inject(TYPES.Redis) private redisClient: IORedis.Redis) {}
constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
async set(dto: {
authorizationHeaderValue: string

View File

@@ -11,17 +11,17 @@ import { ServiceProxyInterface } from './ServiceProxyInterface'
@injectable()
export class HttpServiceProxy implements ServiceProxyInterface {
constructor(
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
@inject(TYPES.SYNCING_SERVER_JS_URL) private syncingServerJsUrl: string,
@inject(TYPES.PAYMENTS_SERVER_URL) private paymentsServerUrl: string,
@inject(TYPES.FILES_SERVER_URL) private filesServerUrl: string,
@inject(TYPES.WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string,
@inject(TYPES.REVISIONS_SERVER_URL) private revisionsServerUrl: string,
@inject(TYPES.EMAIL_SERVER_URL) private emailServerUrl: string,
@inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.ApiGateway_HTTPClient) private httpClient: AxiosInstance,
@inject(TYPES.ApiGateway_AUTH_SERVER_URL) private authServerUrl: string,
@inject(TYPES.ApiGateway_SYNCING_SERVER_JS_URL) private syncingServerJsUrl: string,
@inject(TYPES.ApiGateway_PAYMENTS_SERVER_URL) private paymentsServerUrl: string,
@inject(TYPES.ApiGateway_FILES_SERVER_URL) private filesServerUrl: string,
@inject(TYPES.ApiGateway_WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string,
@inject(TYPES.ApiGateway_REVISIONS_SERVER_URL) private revisionsServerUrl: string,
@inject(TYPES.ApiGateway_EMAIL_SERVER_URL) private emailServerUrl: string,
@inject(TYPES.ApiGateway_HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
@inject(TYPES.ApiGateway_CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
) {}
async validateSession(
@@ -130,19 +130,26 @@ export class HttpServiceProxy implements ServiceProxyInterface {
response: Response,
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
returnRawResponse?: boolean,
): Promise<void | Response<unknown, Record<string, unknown>>> {
if (!this.paymentsServerUrl) {
this.logger.debug('Payments Server URL not defined. Skipped request to Payments API.')
return
}
await this.callServerWithLegacyFormat(
const rawResponse = await this.callServerWithLegacyFormat(
this.paymentsServerUrl,
request,
response,
endpointOrMethodIdentifier,
payload,
returnRawResponse,
)
if (returnRawResponse) {
return rawResponse
}
}
async callAuthServerWithLegacyFormat(
@@ -279,7 +286,8 @@ export class HttpServiceProxy implements ServiceProxyInterface {
response: Response,
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
returnRawResponse?: boolean,
): Promise<void | Response<unknown, Record<string, unknown>>> {
const serviceResponse = await this.getServerResponse(
serverUrl,
request,
@@ -295,9 +303,21 @@ export class HttpServiceProxy implements ServiceProxyInterface {
this.applyResponseHeaders(serviceResponse, response)
if (serviceResponse.request._redirectable._redirectCount > 0) {
response.status(302).redirect(serviceResponse.request.res.responseUrl)
response.status(302)
if (returnRawResponse) {
return response
}
response.redirect(serviceResponse.request.res.responseUrl)
} else {
response.status(serviceResponse.status).send(serviceResponse.data)
response.status(serviceResponse.status)
if (returnRawResponse) {
return response
}
response.send(serviceResponse.data)
}
}

View File

@@ -42,7 +42,8 @@ export interface ServiceProxyInterface {
response: Response,
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
): Promise<void>
returnRawResponse?: boolean,
): Promise<void | Response<unknown, Record<string, unknown>>>
callWebSocketServer(
request: Request,
response: Response,

View File

@@ -43,6 +43,7 @@ export class EndpointResolver implements EndpointResolverInterface {
['[PATCH]:users/:userId', 'auth.users.update'],
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
['[PUT]:auth/params', 'auth.users.getKeyParams'],
['[DELETE]:users/:userUuid', 'auth.users.delete'],
['[POST]:listed', 'auth.users.createListedAccount'],
['[POST]:auth', 'auth.users.register'],
['[GET]:users/:userUuid/settings', 'auth.users.getSettings'],
@@ -79,6 +80,7 @@ export class EndpointResolver implements EndpointResolverInterface {
['[POST]:shared-vaults/:sharedVaultUuid/invites/:inviteUuid/accept', 'sync.shared-vault-invites.accept'],
['[POST]:shared-vaults/:sharedVaultUuid/invites/:inviteUuid/decline', 'sync.shared-vault-invites.decline'],
['[DELETE]:shared-vaults/invites/inbound', 'sync.shared-vault-invites.delete-inbound'],
['[DELETE]:shared-vaults/invites/outbound', 'sync.shared-vault-invites.delete-outbound'],
['[GET]:shared-vaults/invites/outbound', 'sync.shared-vault-invites.get-outbound'],
['[GET]:shared-vaults/invites', 'sync.shared-vault-invites.get-user-invites'],
['[GET]:shared-vaults/:sharedVaultUuid/invites', 'sync.shared-vault-invites.get-vault-invites'],

View File

@@ -3,6 +3,74 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
* **auth:** add handling payments account deleted events STA-1769 ([#682](https://github.com/standardnotes/server/issues/682)) ([8e35dfa](https://github.com/standardnotes/server/commit/8e35dfa4b77256f4c0a3294b296a5526fd1020ad))
## [1.128.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.128.0...@standardnotes/auth-server@1.128.1) (2023-08-02)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.128.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.127.2...@standardnotes/auth-server@1.128.0) (2023-08-02)
### Features
* enable Write Ahead Log mode for SQLite ([#681](https://github.com/standardnotes/server/issues/681)) ([8cd7a13](https://github.com/standardnotes/server/commit/8cd7a138ab56f6a2b0d6c06ef6041ab9b85ae540))
## [1.127.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.127.1...@standardnotes/auth-server@1.127.2) (2023-08-01)
### Bug Fixes
* controller naming ([#678](https://github.com/standardnotes/server/issues/678)) ([56f0aef](https://github.com/standardnotes/server/commit/56f0aef21d3fcec7ac7e968cb1c1b071becbbe26))
## [1.127.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.127.0...@standardnotes/auth-server@1.127.1) (2023-07-31)
### Bug Fixes
* **auth:** auth middleware on delete account ([318af57](https://github.com/standardnotes/server/commit/318af5757d6c42f580157647b22112a9936765e7))
# [1.127.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.5...@standardnotes/auth-server@1.127.0) (2023-07-31)
### Features
* refactor deleting account ([#676](https://github.com/standardnotes/server/issues/676)) ([0d5dcdd](https://github.com/standardnotes/server/commit/0d5dcdd8ec2336e41e7604c4157f79a89163ed29))
## [1.126.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.4...@standardnotes/auth-server@1.126.5) (2023-07-27)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.126.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.3...@standardnotes/auth-server@1.126.4) (2023-07-26)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.126.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.2...@standardnotes/auth-server@1.126.3) (2023-07-26)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.126.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.1...@standardnotes/auth-server@1.126.2) (2023-07-21)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,23 +1,23 @@
import 'reflect-metadata'
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSessionsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressUsersController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressValetTokenController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressAdminController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionTokensController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionSettingsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSettingsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSessionController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressOfflineController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressListedController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressInternalController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressHealthCheckController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressFeaturesController'
import '../src/Infra/InversifyExpressUtils/AnnotatedAuthController'
import '../src/Infra/InversifyExpressUtils/AnnotatedAuthenticatorsController'
import '../src/Infra/InversifyExpressUtils/AnnotatedSessionsController'
import '../src/Infra/InversifyExpressUtils/AnnotatedSubscriptionInvitesController'
import '../src/Infra/InversifyExpressUtils/AnnotatedUserRequestsController'
import '../src/Infra/InversifyExpressUtils/AnnotatedWebSocketsController'
import '../src/Infra/InversifyExpressUtils/AnnotatedUsersController'
import '../src/Infra/InversifyExpressUtils/AnnotatedValetTokenController'
import '../src/Infra/InversifyExpressUtils/AnnotatedAdminController'
import '../src/Infra/InversifyExpressUtils/AnnotatedSubscriptionTokensController'
import '../src/Infra/InversifyExpressUtils/AnnotatedSubscriptionSettingsController'
import '../src/Infra/InversifyExpressUtils/AnnotatedSettingsController'
import '../src/Infra/InversifyExpressUtils/AnnotatedSessionController'
import '../src/Infra/InversifyExpressUtils/AnnotatedOfflineController'
import '../src/Infra/InversifyExpressUtils/AnnotatedListedController'
import '../src/Infra/InversifyExpressUtils/AnnotatedInternalController'
import '../src/Infra/InversifyExpressUtils/AnnotatedHealthCheckController'
import '../src/Infra/InversifyExpressUtils/AnnotatedFeaturesController'
import * as cors from 'cors'
import { urlencoded, json, Request, Response, NextFunction } from 'express'

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.126.2",
"version": "1.131.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -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'
@@ -234,24 +234,28 @@ import { OfflineUserAuthMiddleware } from '../Infra/InversifyExpressUtils/Middle
import { LockMiddleware } from '../Infra/InversifyExpressUtils/Middleware/LockMiddleware'
import { RequiredCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/RequiredCrossServiceTokenMiddleware'
import { OptionalCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OptionalCrossServiceTokenMiddleware'
import { HomeServerSettingsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSettingsController'
import { HomeServerAdminController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerAdminController'
import { HomeServerAuthController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerAuthController'
import { HomeServerAuthenticatorsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerAuthenticatorsController'
import { HomeServerFeaturesController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerFeaturesController'
import { HomeServerListedController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerListedController'
import { HomeServerOfflineController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerOfflineController'
import { HomeServerSessionController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSessionController'
import { HomeServerSubscriptionInvitesController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionInvitesController'
import { HomeServerSubscriptionSettingsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionSettingsController'
import { HomeServerSubscriptionTokensController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionTokensController'
import { HomeServerUserRequestsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerUserRequestsController'
import { HomeServerUsersController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerUsersController'
import { HomeServerValetTokenController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerValetTokenController'
import { HomeServerWebSocketsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerWebSocketsController'
import { HomeServerSessionsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSessionsController'
import { BaseSettingsController } from '../Infra/InversifyExpressUtils/Base/BaseSettingsController'
import { BaseAdminController } from '../Infra/InversifyExpressUtils/Base/BaseAdminController'
import { BaseAuthController } from '../Infra/InversifyExpressUtils/Base/BaseAuthController'
import { BaseAuthenticatorsController } from '../Infra/InversifyExpressUtils/Base/BaseAuthenticatorsController'
import { BaseFeaturesController } from '../Infra/InversifyExpressUtils/Base/BaseFeaturesController'
import { BaseListedController } from '../Infra/InversifyExpressUtils/Base/BaseListedController'
import { BaseOfflineController } from '../Infra/InversifyExpressUtils/Base/BaseOfflineController'
import { BaseSessionController } from '../Infra/InversifyExpressUtils/Base/BaseSessionController'
import { BaseSubscriptionInvitesController } from '../Infra/InversifyExpressUtils/Base/BaseSubscriptionInvitesController'
import { BaseSubscriptionSettingsController } from '../Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController'
import { BaseSubscriptionTokensController } from '../Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController'
import { BaseUserRequestsController } from '../Infra/InversifyExpressUtils/Base/BaseUserRequestsController'
import { BaseUsersController } from '../Infra/InversifyExpressUtils/Base/BaseUsersController'
import { BaseValetTokenController } from '../Infra/InversifyExpressUtils/Base/BaseValetTokenController'
import { BaseWebSocketsController } from '../Infra/InversifyExpressUtils/Base/BaseWebSocketsController'
import { BaseSessionsController } from '../Infra/InversifyExpressUtils/Base/BaseSessionsController'
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'
export class ContainerConfigLoader {
async load(configuration?: {
@@ -826,9 +830,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)
@@ -883,6 +885,15 @@ 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),
),
)
// Controller
container
@@ -952,8 +963,38 @@ 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<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)
@@ -978,6 +1019,14 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_SettingService),
),
)
container
.bind<PaymentsAccountDeletedEventHandler>(TYPES.Auth_PaymentsAccountDeletedEventHandler)
.toConstantValue(
new PaymentsAccountDeletedEventHandler(
container.get(TYPES.Auth_DeleteAccount),
container.get(TYPES.Auth_Logger),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
@@ -992,7 +1041,9 @@ 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)],
['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)],
[
@@ -1005,6 +1056,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)],
])
if (isConfiguredForHomeServer) {
@@ -1037,9 +1089,9 @@ export class ContainerConfigLoader {
}
container
.bind<HomeServerAuthController>(TYPES.Auth_HomeServerAuthController)
.bind<BaseAuthController>(TYPES.Auth_BaseAuthController)
.toConstantValue(
new HomeServerAuthController(
new BaseAuthController(
container.get(TYPES.Auth_VerifyMFA),
container.get(TYPES.Auth_SignIn),
container.get(TYPES.Auth_GetUserKeyParams),
@@ -1054,42 +1106,42 @@ export class ContainerConfigLoader {
// Inversify Controllers
if (isConfiguredForHomeServer) {
container
.bind<HomeServerAuthenticatorsController>(TYPES.Auth_HomeServerAuthenticatorsController)
.bind<BaseAuthenticatorsController>(TYPES.Auth_BaseAuthenticatorsController)
.toConstantValue(
new HomeServerAuthenticatorsController(
new BaseAuthenticatorsController(
container.get(TYPES.Auth_AuthenticatorsController),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerSubscriptionInvitesController>(TYPES.Auth_HomeServerSubscriptionInvitesController)
.bind<BaseSubscriptionInvitesController>(TYPES.Auth_BaseSubscriptionInvitesController)
.toConstantValue(
new HomeServerSubscriptionInvitesController(
new BaseSubscriptionInvitesController(
container.get(TYPES.Auth_SubscriptionInvitesController),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerUserRequestsController>(TYPES.Auth_HomeServerUserRequestsController)
.bind<BaseUserRequestsController>(TYPES.Auth_BaseUserRequestsController)
.toConstantValue(
new HomeServerUserRequestsController(
new BaseUserRequestsController(
container.get(TYPES.Auth_UserRequestsController),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerWebSocketsController>(TYPES.Auth_HomeServerWebSocketsController)
.bind<BaseWebSocketsController>(TYPES.Auth_BaseWebSocketsController)
.toConstantValue(
new HomeServerWebSocketsController(
new BaseWebSocketsController(
container.get(TYPES.Auth_CreateCrossServiceToken),
container.get(TYPES.Auth_WebSocketConnectionTokenDecoder),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerSessionsController>(TYPES.Auth_HomeServerSessionsController)
.bind<BaseSessionsController>(TYPES.Auth_BaseSessionsController)
.toConstantValue(
new HomeServerSessionsController(
new BaseSessionsController(
container.get(TYPES.Auth_GetActiveSessionsForUser),
container.get(TYPES.Auth_AuthenticateRequest),
container.get(TYPES.Auth_SessionProjector),
@@ -1098,17 +1150,17 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerValetTokenController>(TYPES.Auth_HomeServerValetTokenController)
.bind<BaseValetTokenController>(TYPES.Auth_BaseValetTokenController)
.toConstantValue(
new HomeServerValetTokenController(
new BaseValetTokenController(
container.get(TYPES.Auth_CreateValetToken),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerUsersController>(TYPES.Auth_HomeServerUsersController)
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
.toConstantValue(
new HomeServerUsersController(
new BaseUsersController(
container.get(TYPES.Auth_UpdateUser),
container.get(TYPES.Auth_GetUserKeyParams),
container.get(TYPES.Auth_DeleteAccount),
@@ -1120,9 +1172,9 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerAdminController>(TYPES.Auth_HomeServerAdminController)
.bind<BaseAdminController>(TYPES.Auth_BaseAdminController)
.toConstantValue(
new HomeServerAdminController(
new BaseAdminController(
container.get(TYPES.Auth_DeleteSetting),
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_CreateSubscriptionToken),
@@ -1131,9 +1183,9 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerSubscriptionTokensController>(TYPES.Auth_HomeServerSubscriptionTokensController)
.bind<BaseSubscriptionTokensController>(TYPES.Auth_BaseSubscriptionTokensController)
.toConstantValue(
new HomeServerSubscriptionTokensController(
new BaseSubscriptionTokensController(
container.get(TYPES.Auth_CreateSubscriptionToken),
container.get(TYPES.Auth_AuthenticateSubscriptionToken),
container.get(TYPES.Auth_SettingService),
@@ -1145,17 +1197,17 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerSubscriptionSettingsController>(TYPES.Auth_HomeServerSubscriptionSettingsController)
.bind<BaseSubscriptionSettingsController>(TYPES.Auth_BaseSubscriptionSettingsController)
.toConstantValue(
new HomeServerSubscriptionSettingsController(
new BaseSubscriptionSettingsController(
container.get(TYPES.Auth_GetSetting),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerSettingsController>(TYPES.Auth_HomeServerSettingsController)
.bind<BaseSettingsController>(TYPES.Auth_BaseSettingsController)
.toConstantValue(
new HomeServerSettingsController(
new BaseSettingsController(
container.get(TYPES.Auth_GetSettings),
container.get(TYPES.Auth_GetSetting),
container.get(TYPES.Auth_UpdateSetting),
@@ -1164,19 +1216,19 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerSessionController>(TYPES.Auth_HomeServerSessionController)
.bind<BaseSessionController>(TYPES.Auth_BaseSessionController)
.toConstantValue(
new HomeServerSessionController(
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),
),
)
container
.bind<HomeServerOfflineController>(TYPES.Auth_HomeServerOfflineController)
.bind<BaseOfflineController>(TYPES.Auth_BaseOfflineController)
.toConstantValue(
new HomeServerOfflineController(
new BaseOfflineController(
container.get(TYPES.Auth_GetUserFeatures),
container.get(TYPES.Auth_GetUserOfflineSubscription),
container.get(TYPES.Auth_CreateOfflineSubscriptionToken),
@@ -1188,17 +1240,17 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerListedController>(TYPES.Auth_HomeServerListedController)
.bind<BaseListedController>(TYPES.Auth_BaseListedController)
.toConstantValue(
new HomeServerListedController(
new BaseListedController(
container.get(TYPES.Auth_CreateListedAccount),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerFeaturesController>(TYPES.Auth_HomeServerFeaturesController)
.bind<BaseFeaturesController>(TYPES.Auth_BaseFeaturesController)
.toConstantValue(
new HomeServerFeaturesController(
new BaseFeaturesController(
container.get(TYPES.Auth_GetUserFeatures),
container.get(TYPES.Auth_ControllerContainer),
),

View File

@@ -114,6 +114,8 @@ export class AppDataSource {
...commonDataSourceOptions,
type: 'sqlite',
database: this.env.get('DB_SQLITE_DATABASE_PATH'),
enableWAL: true,
busyErrorRetry: 2000,
}
this._dataSource = new DataSource(sqliteDataSourceOptions)

View File

@@ -113,7 +113,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 +152,7 @@ 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'),
// Handlers
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
@@ -165,7 +166,9 @@ 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_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(
@@ -176,6 +179,7 @@ const TYPES = {
),
Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'),
Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
// Services
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
Auth_SessionService: Symbol.for('Auth_SessionService'),
@@ -217,22 +221,22 @@ const TYPES = {
Auth_ProtocolVersionSelector: Symbol.for('Auth_ProtocolVersionSelector'),
Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'),
Auth_UserSubscriptionService: Symbol.for('Auth_UserSubscriptionService'),
Auth_HomeServerAuthController: Symbol.for('Auth_HomeServerAuthController'),
Auth_HomeServerAuthenticatorsController: Symbol.for('Auth_HomeServerAuthenticatorsController'),
Auth_HomeServerSubscriptionInvitesController: Symbol.for('Auth_HomeServerSubscriptionInvitesController'),
Auth_HomeServerUserRequestsController: Symbol.for('Auth_HomeServerUserRequestsController'),
Auth_HomeServerWebSocketsController: Symbol.for('Auth_HomeServerWebSocketsController'),
Auth_HomeServerSessionsController: Symbol.for('Auth_HomeServerSessionsController'),
Auth_HomeServerValetTokenController: Symbol.for('Auth_HomeServerValetTokenController'),
Auth_HomeServerUsersController: Symbol.for('Auth_HomeServerUsersController'),
Auth_HomeServerAdminController: Symbol.for('Auth_HomeServerAdminController'),
Auth_HomeServerSubscriptionTokensController: Symbol.for('Auth_HomeServerSubscriptionTokensController'),
Auth_HomeServerSubscriptionSettingsController: Symbol.for('Auth_HomeServerSubscriptionSettingsController'),
Auth_HomeServerSettingsController: Symbol.for('Auth_HomeServerSettingsController'),
Auth_HomeServerSessionController: Symbol.for('Auth_HomeServerSessionController'),
Auth_HomeServerOfflineController: Symbol.for('Auth_HomeServerOfflineController'),
Auth_HomeServerListedController: Symbol.for('Auth_HomeServerListedController'),
Auth_HomeServerFeaturesController: Symbol.for('Auth_HomeServerFeaturesController'),
Auth_BaseAuthController: Symbol.for('Auth_BaseAuthController'),
Auth_BaseAuthenticatorsController: Symbol.for('Auth_BaseAuthenticatorsController'),
Auth_BaseSubscriptionInvitesController: Symbol.for('Auth_BaseSubscriptionInvitesController'),
Auth_BaseUserRequestsController: Symbol.for('Auth_BaseUserRequestsController'),
Auth_BaseWebSocketsController: Symbol.for('Auth_BaseWebSocketsController'),
Auth_BaseSessionsController: Symbol.for('Auth_BaseSessionsController'),
Auth_BaseValetTokenController: Symbol.for('Auth_BaseValetTokenController'),
Auth_BaseUsersController: Symbol.for('Auth_BaseUsersController'),
Auth_BaseAdminController: Symbol.for('Auth_BaseAdminController'),
Auth_BaseSubscriptionTokensController: Symbol.for('Auth_BaseSubscriptionTokensController'),
Auth_BaseSubscriptionSettingsController: Symbol.for('Auth_BaseSubscriptionSettingsController'),
Auth_BaseSettingsController: Symbol.for('Auth_BaseSettingsController'),
Auth_BaseSessionController: Symbol.for('Auth_BaseSessionController'),
Auth_BaseOfflineController: Symbol.for('Auth_BaseOfflineController'),
Auth_BaseListedController: Symbol.for('Auth_BaseListedController'),
Auth_BaseFeaturesController: Symbol.for('Auth_BaseFeaturesController'),
}
export default TYPES

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,9 @@ describe('AuthenticationMethodResolver', () => {
user = {} as jest.Mocked<User>
session = {} as jest.Mocked<Session>
session = {
userUuid: '00000000-0000-0000-0000-000000000000',
} as jest.Mocked<Session>
revokedSession = {} as jest.Mocked<RevokedSession>
@@ -38,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)
@@ -50,19 +52,25 @@ describe('AuthenticationMethodResolver', () => {
})
it('should resolve jwt authentication method', async () => {
sessionTokenDecoder.decodeToken = jest.fn().mockReturnValue({ user_uuid: '123' })
sessionTokenDecoder.decodeToken = jest.fn().mockReturnValue({ user_uuid: '00000000-0000-0000-0000-000000000000' })
expect(await createResolver().resolve('test')).toEqual({
claims: {
user_uuid: '123',
user_uuid: '00000000-0000-0000-0000-000000000000',
},
type: 'jwt',
user,
})
})
it('should not resolve jwt authentication method with invalid user uuid', async () => {
sessionTokenDecoder.decodeToken = jest.fn().mockReturnValue({ user_uuid: 'invalid' })
expect(await createResolver().resolve('test')).toBeUndefined
})
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,
@@ -71,6 +79,14 @@ describe('AuthenticationMethodResolver', () => {
})
})
it('should not resolve session authentication method with invalid user uuid on session', async () => {
sessionService.getSessionFromToken = jest
.fn()
.mockReturnValue({ session: { userUuid: 'invalid' }, isEphemeral: false })
expect(await createResolver().resolve('test')).toBeUndefined
})
it('should resolve archvied session authentication method', async () => {
sessionService.getRevokedSessionFromToken = jest.fn().mockReturnValue(revokedSession)

View File

@@ -6,6 +6,7 @@ import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { AuthenticationMethod } from './AuthenticationMethod'
import { AuthenticationMethodResolverInterface } from './AuthenticationMethodResolverInterface'
import { Logger } from 'winston'
import { Uuid } from '@standardnotes/domain-core'
@injectable()
export class AuthenticationMethodResolver implements AuthenticationMethodResolverInterface {
@@ -29,20 +30,32 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
if (decodedToken) {
this.logger.debug('Token decoded successfully. User found.')
const userUuidOrError = Uuid.create(decodedToken.user_uuid as string)
if (userUuidOrError.isFailed()) {
return undefined
}
const userUuid = userUuidOrError.getValue()
return {
type: 'jwt',
user: await this.userRepository.findOneByUuid(<string>decodedToken.user_uuid),
user: await this.userRepository.findOneByUuid(userUuid),
claims: decodedToken,
}
}
const session = await this.sessionService.getSessionFromToken(token)
const { session } = await this.sessionService.getSessionFromToken(token)
if (session) {
this.logger.debug('Token decoded successfully. Session found.')
const userUuidOrError = Uuid.create(session.userUuid)
if (userUuidOrError.isFailed()) {
return undefined
}
const userUuid = userUuidOrError.getValue()
return {
type: 'session_token',
user: await this.userRepository.findOneByUuid(session.userUuid),
user: await this.userRepository.findOneByUuid(userUuid),
session: session,
}
}

View File

@@ -50,7 +50,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
ephemeralSession = {
uuid: '2-3-4',
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
} as jest.Mocked<EphemeralSession>
ephemeralSessionRepository = {} as jest.Mocked<EphemeralSessionRepositoryInterface>
@@ -68,7 +68,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: '2-3-4',
}
@@ -84,6 +84,14 @@ describe('AccountDeletionRequestedEventHandler', () => {
expect(userRepository.remove).toHaveBeenCalledWith(user)
})
it('should not remove a user with invalid uuid', async () => {
event.payload.userUuid = 'invalid'
await createHandler().handle(event)
expect(userRepository.remove).not.toHaveBeenCalled()
})
it('should not remove a user if one does not exist', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
@@ -100,6 +108,6 @@ describe('AccountDeletionRequestedEventHandler', () => {
expect(sessionRepository.remove).toHaveBeenCalledWith(session)
expect(revokedSessionRepository.remove).toHaveBeenCalledWith(revokedSession)
expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2-3-4', '1-2-3')
expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2-3-4', '00000000-0000-0000-0000-000000000000')
})
})

View File

@@ -6,6 +6,7 @@ import { EphemeralSessionRepositoryInterface } from '../Session/EphemeralSession
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { Uuid } from '@standardnotes/domain-core'
@injectable()
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
@@ -19,19 +20,27 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
) {}
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
const user = await this.userRepository.findOneByUuid(event.payload.userUuid)
if (user === null) {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(`Could not find user with uuid: ${event.payload.userUuid}`)
return
}
const userUuid = userUuidOrError.getValue()
await this.removeSessions(event.payload.userUuid)
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
return
}
await this.removeSessions(userUuid.value)
await this.userRepository.remove(user)
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
this.logger.info(`Finished account cleanup for user: ${userUuid.value}`)
}
private async removeSessions(userUuid: string): Promise<void> {

View File

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

View File

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

View File

@@ -1,73 +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 { 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 user = await this.userRepository.findOneByUuid(event.payload.userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${event.payload.userUuid}`)
return
}
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, 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()}`)
}
}
}

View File

@@ -0,0 +1,48 @@
import { Logger } from 'winston'
import { Result } from '@standardnotes/domain-core'
import { PaymentsAccountDeletedEvent } from '@standardnotes/domain-events'
import { DeleteAccount } from '../UseCase/DeleteAccount/DeleteAccount'
import { PaymentsAccountDeletedEventHandler } from './PaymentsAccountDeletedEventHandler'
describe('PaymentsAccountDeletedEventHandler', () => {
let deleteAccountUseCase: DeleteAccount
let logger: Logger
let event: PaymentsAccountDeletedEvent
const createHandler = () => new PaymentsAccountDeletedEventHandler(deleteAccountUseCase, logger)
beforeEach(() => {
deleteAccountUseCase = {} as jest.Mocked<DeleteAccount>
deleteAccountUseCase.execute = jest.fn().mockResolvedValue(Result.ok('success'))
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
event = {
payload: {
username: 'username',
},
} as jest.Mocked<PaymentsAccountDeletedEvent>
})
it('should delete account', async () => {
const handler = createHandler()
await handler.handle(event)
expect(deleteAccountUseCase.execute).toHaveBeenCalledWith({
username: 'username',
})
})
it('should log error if delete account fails', async () => {
const handler = createHandler()
deleteAccountUseCase.execute = jest.fn().mockResolvedValue(Result.fail('error'))
await handler.handle(event)
expect(logger.error).toHaveBeenCalledWith('Failed to delete account for user username: error')
})
})

View File

@@ -0,0 +1,18 @@
import { DomainEventHandlerInterface, PaymentsAccountDeletedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { DeleteAccount } from '../UseCase/DeleteAccount/DeleteAccount'
export class PaymentsAccountDeletedEventHandler implements DomainEventHandlerInterface {
constructor(private deleteAccountUseCase: DeleteAccount, private logger: Logger) {}
async handle(event: PaymentsAccountDeletedEvent): Promise<void> {
const result = await this.deleteAccountUseCase.execute({
username: event.payload.username,
})
if (result.isFailed()) {
this.logger.error(`Failed to delete account for user ${event.payload.username}: ${result.getError()}`)
}
}
}

View File

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

View File

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

View File

@@ -214,13 +214,25 @@ describe('RoleService', () => {
})
it('should indicate if a user has given permission', async () => {
const userHasPermission = await createService().userHasPermission('1-2-3', PermissionName.DailyEmailBackup)
const userHasPermission = await createService().userHasPermission(
'00000000-0000-0000-0000-000000000000',
PermissionName.DailyEmailBackup,
)
expect(userHasPermission).toBeTruthy()
})
it('should not indiciate if a user has permission if the user uuid is invalid', async () => {
const userHasPermission = await createService().userHasPermission('invalid', PermissionName.DailyEmailBackup)
expect(userHasPermission).toBeFalsy()
})
it('should indicate if a user does not have a given permission', async () => {
const userHasPermission = await createService().userHasPermission('1-2-3', PermissionName.MarkdownProEditor)
const userHasPermission = await createService().userHasPermission(
'00000000-0000-0000-0000-000000000000',
PermissionName.MarkdownProEditor,
)
expect(userHasPermission).toBeFalsy()
})
@@ -228,7 +240,10 @@ describe('RoleService', () => {
it('should indicate user does not have a permission if user could not be found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const userHasPermission = await createService().userHasPermission('1-2-3', PermissionName.MarkdownProEditor)
const userHasPermission = await createService().userHasPermission(
'00000000-0000-0000-0000-000000000000',
PermissionName.MarkdownProEditor,
)
expect(userHasPermission).toBeFalsy()
})

View File

@@ -13,6 +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'
@injectable()
export class RoleService implements RoleServiceInterface {
@@ -26,10 +27,16 @@ export class RoleService implements RoleServiceInterface {
@inject(TYPES.Auth_Logger) private logger: Logger,
) {}
async userHasPermission(userUuid: string, permissionName: PermissionName): Promise<boolean> {
async userHasPermission(userUuidString: string, permissionName: PermissionName): Promise<boolean> {
const userUuidOrError = Uuid.create(userUuidString)
if (userUuidOrError.isFailed()) {
return false
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid ${userUuid} for permissions check`)
this.logger.warn(`Could not find user with uuid ${userUuid.value} for permissions check`)
return false
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,9 @@ describe('SettingDecrypter', () => {
serverEncryptionVersion: EncryptionVersion.Default,
} as jest.Mocked<Setting>
expect(await createDecrypter().decryptSettingValue(setting, '1-2-3')).toEqual('decrypted')
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual(
'decrypted',
)
})
it('should return null if the setting value is null', async () => {
@@ -41,7 +43,7 @@ describe('SettingDecrypter', () => {
serverEncryptionVersion: EncryptionVersion.Default,
} as jest.Mocked<Setting>
expect(await createDecrypter().decryptSettingValue(setting, '1-2-3')).toBeNull()
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toBeNull()
})
it('should return unencrypted value if the setting value is unencrypted', async () => {
@@ -50,7 +52,7 @@ describe('SettingDecrypter', () => {
serverEncryptionVersion: EncryptionVersion.Unencrypted,
} as jest.Mocked<Setting>
expect(await createDecrypter().decryptSettingValue(setting, '1-2-3')).toEqual('test')
expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual('test')
})
it('should throw if the user could not be found', async () => {
@@ -62,7 +64,23 @@ describe('SettingDecrypter', () => {
let caughtError = null
try {
await createDecrypter().decryptSettingValue(setting, '1-2-3')
await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
it('should throw if the user uuid is invalid', async () => {
const setting = {
value: 'encrypted',
serverEncryptionVersion: EncryptionVersion.Default,
} as jest.Mocked<Setting>
let caughtError = null
try {
await createDecrypter().decryptSettingValue(setting, 'invalid')
} catch (error) {
caughtError = error
}

View File

@@ -6,6 +6,7 @@ import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { Setting } from './Setting'
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
import { SubscriptionSetting } from './SubscriptionSetting'
import { Uuid } from '@standardnotes/domain-core'
@injectable()
export class SettingDecrypter implements SettingDecrypterInterface {
@@ -14,12 +15,18 @@ export class SettingDecrypter implements SettingDecrypterInterface {
@inject(TYPES.Auth_Crypter) private crypter: CrypterInterface,
) {}
async decryptSettingValue(setting: Setting | SubscriptionSetting, userUuid: string): Promise<string | null> {
async decryptSettingValue(setting: Setting | SubscriptionSetting, userUuidString: string): Promise<string | null> {
if (setting.value !== null && setting.serverEncryptionVersion === EncryptionVersion.Default) {
const userUuidOrError = Uuid.create(userUuidString)
if (userUuidOrError.isFailed()) {
throw new Error(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
throw new Error(`Could not find user with uuid: ${userUuid}`)
throw new Error(`Could not find user with uuid: ${userUuid.value}`)
}
return this.crypter.decryptForUser(setting.value, user)

View File

@@ -17,7 +17,7 @@ describe('AuthenticateSubscriptionToken', () => {
beforeEach(() => {
subscriptionTokenRepository = {} as jest.Mocked<SubscriptionTokenRepositoryInterface>
subscriptionTokenRepository.getUserUuidByToken = jest.fn().mockReturnValue('1-2-3')
subscriptionTokenRepository.getUserUuidByToken = jest.fn().mockReturnValue('00000000-0000-0000-0000-000000000000')
user = {
roles: Promise.resolve([{ name: RoleName.NAMES.CoreUser }]),
@@ -30,8 +30,6 @@ describe('AuthenticateSubscriptionToken', () => {
it('should authenticate an subscription token', async () => {
const response = await createUseCase().execute({ token: 'test' })
expect(userRepository.findOneByUuid).toHaveBeenCalledWith('1-2-3')
expect(response.success).toBeTruthy()
expect(response.user).toEqual(user)

View File

@@ -6,6 +6,7 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { AuthenticateSubscriptionTokenDTO } from './AuthenticateSubscriptionTokenDTO'
import { AuthenticateSubscriptionTokenResponse } from './AuthenticateSubscriptionTokenResponse'
import { Uuid } from '@standardnotes/domain-core'
@injectable()
export class AuthenticateSubscriptionToken implements UseCaseInterface {
@@ -16,12 +17,14 @@ export class AuthenticateSubscriptionToken implements UseCaseInterface {
) {}
async execute(dto: AuthenticateSubscriptionTokenDTO): Promise<AuthenticateSubscriptionTokenResponse> {
const userUuid = await this.subscriptionTokenRepository.getUserUuidByToken(dto.token)
if (userUuid === undefined) {
const userUuidString = await this.subscriptionTokenRepository.getUserUuidByToken(dto.token)
const userUuidOrError = Uuid.create(userUuidString as string)
if (userUuidOrError.isFailed()) {
return {
success: false,
}
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
import { AuthResponse20161215 } from '../../Auth/AuthResponse20161215'
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
export type ChangeCredentialsResponse = {
success: boolean
authResponse?: AuthResponse20161215 | AuthResponse20200115
errorMessage?: string
}

View File

@@ -28,13 +28,15 @@ describe('CreateCrossServiceToken', () => {
session = {} as jest.Mocked<Session>
user = {
uuid: '1-2-3',
uuid: '00000000-0000-0000-0000-000000000000',
email: 'test@test.te',
} as jest.Mocked<User>
user.roles = Promise.resolve([role])
userProjector = {} as jest.Mocked<ProjectorInterface<User>>
userProjector.projectSimple = jest.fn().mockReturnValue({ uuid: '1-2-3', email: 'test@test.te' })
userProjector.projectSimple = jest
.fn()
.mockReturnValue({ uuid: '00000000-0000-0000-0000-000000000000', email: 'test@test.te' })
roleProjector = {} as jest.Mocked<ProjectorInterface<Role>>
roleProjector.projectSimple = jest.fn().mockReturnValue({ name: 'role1', uuid: '1-3-4' })
@@ -69,7 +71,7 @@ describe('CreateCrossServiceToken', () => {
},
user: {
email: 'test@test.te',
uuid: '1-2-3',
uuid: '00000000-0000-0000-0000-000000000000',
},
},
60,
@@ -91,7 +93,7 @@ describe('CreateCrossServiceToken', () => {
],
user: {
email: 'test@test.te',
uuid: '1-2-3',
uuid: '00000000-0000-0000-0000-000000000000',
},
},
60,
@@ -100,7 +102,7 @@ describe('CreateCrossServiceToken', () => {
it('should create a cross service token for user by user uuid', async () => {
await createUseCase().execute({
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
@@ -113,7 +115,7 @@ describe('CreateCrossServiceToken', () => {
],
user: {
email: 'test@test.te',
uuid: '1-2-3',
uuid: '00000000-0000-0000-0000-000000000000',
},
},
60,
@@ -126,7 +128,20 @@ describe('CreateCrossServiceToken', () => {
let caughtError = null
try {
await createUseCase().execute({
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
})
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
it('should throw an error if user uuid is invalid', async () => {
let caughtError = null
try {
await createUseCase().execute({
userUuid: 'invalid',
})
} catch (error) {
caughtError = error

View File

@@ -11,6 +11,7 @@ import { UseCaseInterface } from '../UseCaseInterface'
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
import { Uuid } from '@standardnotes/domain-core'
@injectable()
export class CreateCrossServiceToken implements UseCaseInterface {
@@ -26,7 +27,13 @@ export class CreateCrossServiceToken implements UseCaseInterface {
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
let user: User | undefined | null = dto.user
if (user === undefined && dto.userUuid !== undefined) {
user = await this.userRepository.findOneByUuid(dto.userUuid)
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
user = await this.userRepository.findOneByUuid(userUuid)
}
if (!user) {

View File

@@ -34,6 +34,7 @@ describe('DeleteAccount', () => {
} as jest.Mocked<UserSubscription>
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
@@ -53,65 +54,124 @@ describe('DeleteAccount', () => {
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(1)
})
it('should trigger account deletion - no subscription', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
describe('when user uuid is provided', () => {
it('should trigger account deletion - no subscription', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
expect(await createUseCase().execute({ email: 'test@test.te' })).toEqual({
message: 'Successfully deleted user',
responseCode: 200,
success: true,
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect(result.isFailed()).toBeFalsy()
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: undefined,
})
})
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: undefined,
it('should trigger account deletion - subscription present', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect(result.isFailed()).toBeFalsy()
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: '1-2-3',
})
})
it('should not trigger account deletion if user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect(result.isFailed()).toBeFalsy()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
})
it('should not trigger account deletion if user uuid is invalid', async () => {
const result = await createUseCase().execute({ userUuid: 'invalid' })
expect(result.isFailed()).toBeTruthy()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
})
})
it('should trigger account deletion - subscription present', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
describe('when username is provided', () => {
it('should trigger account deletion - no subscription', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
expect(await createUseCase().execute({ email: 'test@test.te' })).toEqual({
message: 'Successfully deleted user',
responseCode: 200,
success: true,
const result = await createUseCase().execute({ username: 'test@test.te' })
expect(result.isFailed()).toBeFalsy()
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: undefined,
})
})
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: '1-2-3',
it('should trigger account deletion - subscription present', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
const result = await createUseCase().execute({ username: 'test@test.te' })
expect(result.isFailed()).toBeFalsy()
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: '1-2-3',
})
})
it('should not trigger account deletion if user is not found', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
const result = await createUseCase().execute({ username: 'test@test.te' })
expect(result.isFailed()).toBeFalsy()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
})
it('should not trigger account deletion if username is invalid', async () => {
const result = await createUseCase().execute({ username: '' })
expect(result.isFailed()).toBeTruthy()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
})
})
it('should not trigger account deletion if user is not found', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
describe('when neither user uuid nor username is provided', () => {
it('should not trigger account deletion', async () => {
const result = await createUseCase().execute({})
expect(await createUseCase().execute({ email: 'test@test.te' })).toEqual({
message: 'User not found',
responseCode: 404,
success: false,
expect(result.isFailed()).toBeTruthy()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
})
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
})
it('should not trigger account deletion if username is invalid', async () => {
expect(await createUseCase().execute({ email: '' })).toEqual({
message: 'Username cannot be empty',
responseCode: 400,
success: false,
})
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createAccountDeletionRequestedEvent).not.toHaveBeenCalled()
})
})

View File

@@ -1,4 +1,4 @@
import { Username } from '@standardnotes/domain-core'
import { Result, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
@@ -7,13 +7,12 @@ import TYPES from '../../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { DeleteAccountDTO } from './DeleteAccountDTO'
import { DeleteAccountResponse } from './DeleteAccountResponse'
import { User } from '../../User/User'
@injectable()
export class DeleteAccount implements UseCaseInterface {
export class DeleteAccount implements UseCaseInterface<string> {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
@@ -22,25 +21,30 @@ export class DeleteAccount implements UseCaseInterface {
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
) {}
async execute(dto: DeleteAccountDTO): Promise<DeleteAccountResponse> {
const usernameOrError = Username.create(dto.email)
if (usernameOrError.isFailed()) {
return {
success: false,
responseCode: 400,
message: usernameOrError.getError(),
async execute(dto: DeleteAccountDTO): Promise<Result<string>> {
let user: User | null = null
if (dto.userUuid !== undefined) {
const uuidOrError = Uuid.create(dto.userUuid)
if (uuidOrError.isFailed()) {
return Result.fail(uuidOrError.getError())
}
}
const username = usernameOrError.getValue()
const uuid = uuidOrError.getValue()
const user = await this.userRepository.findOneByUsernameOrEmail(username)
user = await this.userRepository.findOneByUuid(uuid)
} else if (dto.username !== undefined) {
const usernameOrError = Username.create(dto.username)
if (usernameOrError.isFailed()) {
return Result.fail(usernameOrError.getError())
}
const username = usernameOrError.getValue()
user = await this.userRepository.findOneByUsernameOrEmail(username)
} else {
return Result.fail('Either userUuid or username must be provided.')
}
if (user === null) {
return {
success: false,
responseCode: 404,
message: 'User not found',
}
return Result.ok('User already deleted.')
}
let regularSubscriptionUuid = undefined
@@ -57,10 +61,6 @@ export class DeleteAccount implements UseCaseInterface {
}),
)
return {
success: true,
message: 'Successfully deleted user',
responseCode: 200,
}
return Result.ok('Successfully deleted account.')
}
}

View File

@@ -1,3 +1,4 @@
export type DeleteAccountDTO = {
email: string
userUuid?: string
username?: string
}

View File

@@ -1,5 +0,0 @@
export type DeleteAccountResponse = {
success: boolean
responseCode: number
message: string
}

View File

@@ -20,7 +20,7 @@ export class DeleteAuthenticator implements UseCaseInterface<string> {
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid.value)
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
return Result.fail('Could not delete authenticator: user not found.')
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
export type DeleteOtherSessionsForUserDTO = {
userUuid: string
currentSessionUuid: string
markAsRevoked: boolean
}

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
export type DeletePreviousSessionsForUserDTO = {
userUuid: string
currentSessionUuid: string
}

View File

@@ -1,3 +0,0 @@
export type DeletePreviousSessionsForUserResponse = {
success: boolean
}

View File

@@ -37,7 +37,7 @@ export class GenerateAuthenticatorRegistrationOptions
}
const username = usernameOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid.value)
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
return Result.fail('Could not generate authenticator registration options: user not found.')
}

View File

@@ -19,7 +19,7 @@ export class GenerateRecoveryCodes implements UseCaseInterface<string> {
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid.value)
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
return Result.fail('Could not generate recovery codes: user not found')
}

View File

@@ -45,6 +45,10 @@ describe('GetSettings', () => {
)
beforeEach(() => {
user = {
uuid: '00000000-0000-0000-0000-000000000000',
} as jest.Mocked<User>
setting = new Setting()
setting.name = 'test'
setting.updatedAt = 345
@@ -90,8 +94,6 @@ describe('GetSettings', () => {
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
user = {} as jest.Mocked<User>
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
@@ -103,18 +105,27 @@ describe('GetSettings', () => {
it('should fail if a user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({
success: false,
error: {
message: 'User 1-2-3 not found.',
message: 'User 00000000-0000-0000-0000-000000000000 not found.',
},
})
})
it('should fail if a user uuid is invalid', async () => {
expect(await createUseCase().execute({ userUuid: 'invalid' })).toEqual({
success: false,
error: {
message: 'Given value is not a valid uuid: invalid',
},
})
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({
success: true,
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
settings: [{ foo: 'bar' }],
})
@@ -131,9 +142,9 @@ describe('GetSettings', () => {
} as jest.Mocked<Setting>
settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting])
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({
success: true,
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
settings: [{ foo: 'bar' }],
})
@@ -147,10 +158,14 @@ describe('GetSettings', () => {
it('should return all user settings of certain name', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test', allowSensitiveRetrieval: true }),
await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
settingName: 'test',
allowSensitiveRetrieval: true,
}),
).toEqual({
success: true,
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
settings: [{ foo: 'bar' }],
})
@@ -159,10 +174,14 @@ describe('GetSettings', () => {
it('should return all user settings updated after', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true, updatedAfter: 123 }),
await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
allowSensitiveRetrieval: true,
updatedAfter: 123,
}),
).toEqual({
success: true,
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
settings: [{ foo: 'bar' }],
})
@@ -170,9 +189,14 @@ describe('GetSettings', () => {
})
it('should return all sensitive user settings if explicit', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true })).toEqual({
expect(
await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
allowSensitiveRetrieval: true,
}),
).toEqual({
success: true,
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
settings: [{ foo: 'bar' }, { foo: 'bar' }],
})
@@ -190,9 +214,9 @@ describe('GetSettings', () => {
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({
success: true,
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
settings: [{ foo: 'bar' }, { foo: 'sub-bar' }],
})
@@ -210,9 +234,9 @@ describe('GetSettings', () => {
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })).toEqual({
success: true,
userUuid: '1-2-3',
userUuid: '00000000-0000-0000-0000-000000000000',
settings: [{ foo: 'bar' }, { foo: 'sub-bar' }],
})

View File

@@ -15,6 +15,7 @@ import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
import { SimpleSetting } from '../../Setting/SimpleSetting'
import { SimpleSubscriptionSetting } from '../../Setting/SimpleSubscriptionSetting'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { Uuid } from '@standardnotes/domain-core'
@injectable()
export class GetSettings implements UseCaseInterface {
@@ -30,7 +31,16 @@ export class GetSettings implements UseCaseInterface {
) {}
async execute(dto: GetSettingsDto): Promise<GetSettingsResponse> {
const { userUuid } = dto
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return {
success: false,
error: {
message: userUuidOrError.getError(),
},
}
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
@@ -38,13 +48,13 @@ export class GetSettings implements UseCaseInterface {
return {
success: false,
error: {
message: `User ${userUuid} not found.`,
message: `User ${userUuid.value} not found.`,
},
}
}
let settings: Array<Setting | SubscriptionSetting>
settings = await this.settingRepository.findAllByUserUuid(userUuid)
settings = await this.settingRepository.findAllByUserUuid(user.uuid)
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid)
@@ -83,7 +93,7 @@ export class GetSettings implements UseCaseInterface {
return {
success: true,
userUuid,
userUuid: user.uuid,
settings: simpleSettings,
}
}

View File

@@ -31,23 +31,36 @@ describe('GetUserFeatures', () => {
it('should fail if a user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
expect(await createUseCase().execute({ userUuid: 'user-1-1-1', offline: false })).toEqual({
success: false,
error: {
message: 'User user-1-1-1 not found.',
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000', offline: false })).toEqual(
{
success: false,
error: {
message: 'User 00000000-0000-0000-0000-000000000000 not found.',
},
},
})
)
})
it('should return user features', async () => {
expect(await createUseCase().execute({ userUuid: 'user-1-1-1', offline: false })).toEqual({
success: true,
userUuid: 'user-1-1-1',
features: [
{
name: 'foobar',
},
],
expect(await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000', offline: false })).toEqual(
{
success: true,
userUuid: '00000000-0000-0000-0000-000000000000',
features: [
{
name: 'foobar',
},
],
},
)
})
it('should fail if a user uuid is invalid', async () => {
expect(await createUseCase().execute({ userUuid: 'invalid', offline: false })).toEqual({
success: false,
error: {
message: 'Given value is not a valid uuid: invalid',
},
})
})

View File

@@ -5,6 +5,7 @@ import { GetUserFeaturesDto } from './GetUserFeaturesDto'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GetUserFeaturesResponse } from './GetUserFeaturesResponse'
import { FeatureServiceInterface } from '../../Feature/FeatureServiceInterface'
import { Uuid } from '@standardnotes/domain-core'
@injectable()
export class GetUserFeatures implements UseCaseInterface {
@@ -24,13 +25,24 @@ export class GetUserFeatures implements UseCaseInterface {
}
}
const user = await this.userRepository.findOneByUuid(dto.userUuid)
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return {
success: false,
error: {
message: userUuidOrError.getError(),
},
}
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
return {
success: false,
error: {
message: `User ${dto.userUuid} not found.`,
message: `User ${userUuid.value} not found.`,
},
}
}
@@ -39,7 +51,7 @@ export class GetUserFeatures implements UseCaseInterface {
return {
success: true,
userUuid: dto.userUuid,
userUuid: userUuid.value,
features: userFeatures,
}
}

View File

@@ -61,7 +61,9 @@ describe('GetUserKeyParams', () => {
})
it('should get key params for an authenticated user - searching by uuid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', authenticated: true })).toEqual({
expect(
await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000', authenticated: true }),
).toEqual({
keyParams: {
foo: 'bar',
},
@@ -71,7 +73,9 @@ describe('GetUserKeyParams', () => {
})
it('should get key params for an unauthenticated user - searching by uuid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', authenticated: false })).toEqual({
expect(
await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000', authenticated: false }),
).toEqual({
keyParams: {
foo: 'bar',
},
@@ -81,7 +85,13 @@ describe('GetUserKeyParams', () => {
})
it("should get key params for an unauthenticated user and store it's code challenge", async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', authenticated: false, codeChallenge: 'test' })).toEqual({
expect(
await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
authenticated: false,
codeChallenge: 'test',
}),
).toEqual({
keyParams: {
foo: 'bar',
},
@@ -107,7 +117,18 @@ describe('GetUserKeyParams', () => {
let error = null
try {
await createUseCase().execute({ userUuid: '1-2-3', authenticated: false })
await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000', authenticated: false })
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
it('should throw error for an invalid user uuid', async () => {
let error = null
try {
await createUseCase().execute({ userUuid: 'invalid', authenticated: false })
} catch (e) {
error = e
}

View File

@@ -10,7 +10,7 @@ import { User } from '../../User/User'
import { PKCERepositoryInterface } from '../../User/PKCERepositoryInterface'
import { GetUserKeyParamsDTOV2Challenged } from './GetUserKeyParamsDTOV2Challenged'
import { KeyParamsData } from '@standardnotes/responses'
import { Username } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
@injectable()
export class GetUserKeyParams implements UseCaseInterface {
@@ -39,7 +39,13 @@ export class GetUserKeyParams implements UseCaseInterface {
}
}
} else if (dto.userUuid) {
user = await this.userRepository.findOneByUuid(dto.userUuid)
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
throw Error(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
user = await this.userRepository.findOneByUuid(userUuid)
}
if (!user) {

Some files were not shown because too many files have changed in this diff Show More