Compare commits

..

58 Commits

Author SHA1 Message Date
standardci 23b8cdc4a1 chore(release): publish new version
- @standardnotes/home-server@1.15.6
 - @standardnotes/revisions-server@1.27.0
 - @standardnotes/syncing-server@1.86.0
2023-08-29 10:54:17 +00:00
Karol Sójko 2646b756a9 feat(revisions): add MongoDB support (#715)
* feat(revisions): add MongoDB support

* fix: add missing mongodb from revisions

* fix: mongodb bson imports
2023-08-29 12:19:55 +02:00
Karol Sójko 28e058c6e8 fix: remove redundant vaults enabled flag 2023-08-29 08:28:40 +02:00
standardci 8dea171115 chore(release): publish new version
- @standardnotes/api-gateway@1.72.1
 - @standardnotes/auth-server@1.135.2
 - @standardnotes/files-server@1.22.2
 - @standardnotes/home-server@1.15.5
 - @standardnotes/revisions-server@1.26.12
 - @standardnotes/syncing-server@1.85.1
2023-08-28 14:09:12 +00:00
Karol Sójko aef9254713 fix: allow self hosted to use new model of items (#714)
* fix: allow self hosted to use new model of items

* fix: env sample

* fix: binding
2023-08-28 15:34:07 +02:00
Karol Sójko 31b7396006 fix: enable vault tests for all suites (#713)
* fix: enable vault tests for all suites

* fix: rename test suite
2023-08-28 14:29:01 +02:00
standardci be0a2649da chore(release): publish new version
- @standardnotes/home-server@1.15.4
 - @standardnotes/syncing-server@1.85.0
2023-08-28 12:17:41 +00:00
Karol Sójko bf8f91f83d feat(syncing-server): distinguish between legacy and current items model usage (#712)
* feat(syncing-server): turn mysql items model into legacy

* fix: rename MySQL model to SQL model to include SQLite option

* feat(syncing-server): distinguish between legacy and current items model usage
2023-08-28 13:48:27 +02:00
Karol Sójko effdfebc19 feat(syncing-server): turn mysql items model into legacy (#711)
* feat(syncing-server): turn mysql items model into legacy

* fix: rename MySQL model to SQL model to include SQLite option

* fix: rename mysqlitem to sqlitem
2023-08-28 12:28:48 +02:00
standardci f4816e6c9a chore(release): publish new version
- @standardnotes/auth-server@1.135.1
 - @standardnotes/home-server@1.15.3
 - @standardnotes/syncing-server@1.84.2
2023-08-25 13:41:14 +00:00
Karol Sójko 152a5cbd27 fix(syncing-server): items sorting in MongoDB (#710) 2023-08-25 15:01:17 +02:00
Karol Sójko 1488763115 fix(syncing-server): logs severity on creating duplicates 2023-08-25 12:24:43 +02:00
Karol Sójko bbb35d16fc fix(auth): account enumeration with pseudo u2f and mfa (#709) 2023-08-25 12:05:16 +02:00
standardci ef07045ee9 chore(release): publish new version
- @standardnotes/home-server@1.15.2
 - @standardnotes/syncing-server@1.84.1
2023-08-25 09:16:28 +00:00
Karol Sójko 3ba673b424 fix(syncing-server): handling mixed values of deleted flag in MongoDB (#708) 2023-08-25 10:45:14 +02:00
standardci 9c4032ebea chore(release): publish new version
- @standardnotes/analytics@2.25.17
 - @standardnotes/api-gateway@1.72.0
 - @standardnotes/auth-server@1.135.0
 - @standardnotes/domain-events-infra@1.12.14
 - @standardnotes/domain-events@2.117.0
 - @standardnotes/event-store@1.11.23
 - @standardnotes/files-server@1.22.1
 - @standardnotes/home-server@1.15.1
 - @standardnotes/revisions-server@1.26.11
 - @standardnotes/scheduler-server@1.20.27
 - @standardnotes/security@1.12.0
 - @standardnotes/syncing-server@1.84.0
 - @standardnotes/time@1.15.0
 - @standardnotes/websockets-server@1.10.21
2023-08-24 13:57:16 +00:00
Karol Sójko 05bb12c978 feat: add trigerring items transition and checking status of it (#707) 2023-08-24 14:39:33 +02:00
Karol Sójko df957f07e3 fix: missing topic subscription on localstack 2023-08-23 09:11:30 +02:00
standardci b510284e01 chore(release): publish new version
- @standardnotes/analytics@2.25.16
 - @standardnotes/api-gateway@1.71.1
 - @standardnotes/auth-server@1.134.0
 - @standardnotes/domain-events-infra@1.12.13
 - @standardnotes/domain-events@2.116.0
 - @standardnotes/event-store@1.11.22
 - @standardnotes/files-server@1.22.0
 - @standardnotes/home-server@1.15.0
 - @standardnotes/revisions-server@1.26.10
 - @standardnotes/scheduler-server@1.20.26
 - @standardnotes/security@1.11.0
 - @standardnotes/syncing-server@1.83.0
 - @standardnotes/websockets-server@1.10.20
2023-08-23 06:47:38 +00:00
Karol Sójko 205a1ed637 feat: add handling file moving and updating storage quota (#705)
* feat: add handling file moving and updating storage quota

* fix: getting file metada when moving files

* fix: missing event handler binding
2023-08-23 08:09:34 +02:00
standardci 2073c735a5 chore(release): publish new version
- @standardnotes/analytics@2.25.15
 - @standardnotes/api-gateway@1.71.0
 - @standardnotes/auth-server@1.133.0
 - @standardnotes/domain-events-infra@1.12.12
 - @standardnotes/domain-events@2.115.1
 - @standardnotes/event-store@1.11.21
 - @standardnotes/files-server@1.21.0
 - @standardnotes/home-server@1.14.2
 - @standardnotes/revisions-server@1.26.9
 - @standardnotes/scheduler-server@1.20.25
 - @standardnotes/security@1.10.0
 - @standardnotes/syncing-server@1.82.0
 - @standardnotes/websockets-server@1.10.19
2023-08-22 09:23:30 +00:00
Karol Sójko 34085ac6fb feat: consider shared vault owner quota when uploading files to shared vault (#704)
* fix(auth): updating storage quota on shared subscriptions

* fix(syncing-server): turn shared vault and key associations into value objects

* feat: consider shared vault owner quota when uploading files to shared vault

* fix: add passing x-shared-vault-owner-context value

* fix: refactor creating cross service token to not throw errors

* fix: caching cross service token

* fix: missing header in http service proxy
2023-08-22 10:49:58 +02:00
standardci 3d6559921b chore(release): publish new version
- @standardnotes/home-server@1.14.1
 - @standardnotes/scheduler-server@1.20.24
 - @standardnotes/syncing-server@1.81.0
2023-08-21 09:00:52 +00:00
Karol Sójko 15a7f0e71a fix(syncing-server): DocumentDB retry writes support (#703)
* fix(syncing-server): DocumentDB retry writes support

* fix: auth source for mongo
2023-08-21 10:25:56 +02:00
Karol Sójko 3e56243d6f fix(scheduler): remove exit interview form link (#702) 2023-08-21 08:42:21 +02:00
Karol Sójko 032fcb938d feat(syncing-server): add use case for migrating items from one database to another (#701) 2023-08-18 17:25:24 +02:00
standardci e98393452b chore(release): publish new version
- @standardnotes/analytics@2.25.14
 - @standardnotes/api-gateway@1.70.5
 - @standardnotes/auth-server@1.132.0
 - @standardnotes/domain-core@1.26.0
 - @standardnotes/event-store@1.11.20
 - @standardnotes/files-server@1.20.4
 - @standardnotes/home-server@1.14.0
 - @standardnotes/revisions-server@1.26.8
 - @standardnotes/scheduler-server@1.20.23
 - @standardnotes/settings@1.21.25
 - @standardnotes/syncing-server@1.80.0
 - @standardnotes/websockets-server@1.10.18
2023-08-18 15:15:20 +00:00
Karol Sójko 302b624504 feat: add mechanism for determining if a user should use the primary or secondary items database (#700)
* feat(domain-core): introduce new role for users transitioning to new mechanisms

* feat: add mechanism for determining if a user should use the primary or secondary items database

* fix: add transition mode enabled switch in docker entrypoint

* fix(syncing-server): mapping roles from middleware

* fix: mongodb item repository binding

* fix: item backups service binding

* fix: passing transition mode enabled variable to docker setup
2023-08-18 16:45:10 +02:00
Karol Sójko e00d9d2ca0 fix: e2e parameter for running vault tests 2023-08-18 11:11:54 +02:00
Karol Sójko 9ab4601c8d feat: add transition mode switch to e2e test suite 2023-08-18 11:00:36 +02:00
Karol Sójko 19e43bdb1a fix: run vault tests based on secondary db usage (#699) 2023-08-17 13:21:50 +02:00
standardci 49832e7944 chore(release): publish new version
- @standardnotes/home-server@1.13.51
 - @standardnotes/syncing-server@1.79.1
2023-08-17 10:15:43 +00:00
Karol Sójko 916e98936a fix(home-server): add default env values for secondary database 2023-08-17 11:56:56 +02:00
Karol Sójko 31d1eef7f7 fix(syncing-server): refactor shared vault and key system associations (#698)
* feat(syncing-server): refactor persistence of shared vault and key system associations

* fix(syncing-server): refactor shared vault and key system associations
2023-08-17 11:56:16 +02:00
standardci 2648d9a813 chore(release): publish new version
- @standardnotes/home-server@1.13.50
 - @standardnotes/syncing-server@1.79.0
2023-08-16 11:16:38 +00:00
Karol Sójko b24b576209 feat: add mongodb initial support (#696)
* feat: add mongodb initial support

* fix: typeorm annotations for mongodb entity

* wip mongo repo

* feat: add mongodb queries

* fix(syncing-server): env sample

* fix(syncing-server): Mongo connection auth source

* fix(syncing-server): db switch env var name

* fix(syncing-server): persisting and querying by _id as UUID in MongoDB

* fix(syncing-server): items upserts on MongoDB

* fix: remove foreign key migration
2023-08-16 13:00:16 +02:00
Karol Sójko faee38bffd fix: hosts for home-server e2e ci setup 2023-08-15 13:17:20 +02:00
Karol Sójko 65f3503fe8 fix: docker compose ci setup 2023-08-15 13:11:14 +02:00
Karol Sójko 054023b791 fix: host variables 2023-08-15 12:59:13 +02:00
Karol Sójko 383c3a68fa fix: default value for SECONDARY_DB_ENABLED 2023-08-15 12:56:55 +02:00
Karol Sójko 7d22b1c15c feat: run mongo db secondary database in e2e 2023-08-15 12:50:38 +02:00
standardci c71e7cd926 chore(release): publish new version
- @standardnotes/auth-server@1.131.5
 - @standardnotes/home-server@1.13.49
2023-08-15 10:34:11 +00:00
Karol Sójko 83ad069c5d fix(auth): passing the invalidate cache header (#697) 2023-08-15 12:16:01 +02:00
standardci 081108d9ba chore(release): publish new version
- @standardnotes/home-server@1.13.48
 - @standardnotes/syncing-server@1.78.11
2023-08-11 11:52:27 +00:00
Karol Sójko 8f3df56a2b chore: fix revisions frequency 2023-08-11 13:22:11 +02:00
Karol Sójko d02124f4e5 Revert "tmp: disable shared vaults"
This reverts commit c49dc35ab5.
2023-08-11 12:28:57 +02:00
Karol Sójko 09e351fedb Revert "tmp: ci"
This reverts commit 06cedd11d8.
2023-08-11 12:27:12 +02:00
Karol Sójko ad4b85b095 Revert "tmp: disable decorating with associations on revisions"
This reverts commit ac3646836c.
2023-08-11 12:26:44 +02:00
Karol Sójko 0bf7d8beae Revert "tmp: disable decorating items completely"
This reverts commit bc1c7a8ae1.
2023-08-11 12:25:35 +02:00
standardci 1ae7cca394 chore(release): publish new version
- @standardnotes/home-server@1.13.47
 - @standardnotes/syncing-server@1.78.10
2023-08-11 09:00:00 +00:00
Karol Sójko bc1c7a8ae1 tmp: disable decorating items completely 2023-08-11 10:54:12 +02:00
standardci c22c5e4584 chore(release): publish new version
- @standardnotes/home-server@1.13.46
 - @standardnotes/syncing-server@1.78.9
2023-08-11 08:46:28 +00:00
Karol Sójko ac3646836c tmp: disable decorating with associations on revisions 2023-08-11 10:40:03 +02:00
standardci 7a31ab75d6 chore(release): publish new version
- @standardnotes/home-server@1.13.45
 - @standardnotes/syncing-server@1.78.8
2023-08-11 08:23:28 +00:00
Karol Sójko c49dc35ab5 tmp: disable shared vaults 2023-08-11 10:15:55 +02:00
Karol Sójko 06cedd11d8 tmp: ci 2023-08-11 10:15:55 +02:00
standardci f496376fb3 chore(release): publish new version
- @standardnotes/scheduler-server@1.20.22
2023-08-11 08:14:41 +00:00
Karol Sójko 091e2a57e8 fix(scheduler): adjust email backups encouraging email schedule (#695) 2023-08-11 09:35:51 +02:00
308 changed files with 6319 additions and 1723 deletions
+6
View File
@@ -22,6 +22,12 @@ MYSQL_USER=std_notes_user
MYSQL_PASSWORD=changeme123
MYSQL_ROOT_PASSWORD=changeme123
MONGO_HOST=secondary_db
MONGO_PORT=27017
MONGO_USERNAME=standardnotes
MONGO_PASSWORD=standardnotes
MONGO_DATABASE=standardnotes
AUTH_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
+33 -16
View File
@@ -19,7 +19,12 @@ on:
jobs:
e2e:
name: (Docker) E2E Test Suite
name: (Self Hosting) E2E Test Suite
strategy:
fail-fast: false
matrix:
secondary_db_enabled: [true, false]
transition_mode_enabled: [true, false]
runs-on: ubuntu-latest
services:
@@ -45,6 +50,8 @@ jobs:
env:
DB_TYPE: mysql
CACHE_TYPE: redis
SECONDARY_DB_ENABLED: ${{ matrix.secondary_db_enabled }}
TRANSITION_MODE_ENABLED: ${{ matrix.transition_mode_enabled }}
- name: Wait for server to start
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
@@ -67,13 +74,8 @@ jobs:
matrix:
db_type: [mysql, sqlite]
cache_type: [redis, memory]
include:
- cache_type: redis
db_type: mysql
redis_port: 6380
- cache_type: redis
db_type: sqlite
redis_port: 6381
secondary_db_enabled: [true, false]
transition_mode_enabled: [true, false]
runs-on: ubuntu-latest
@@ -85,16 +87,24 @@ jobs:
cache:
image: redis
ports:
- ${{ matrix.redis_port }}:6379
- 6379:6379
db:
image: mysql
ports:
- 3307:3306
- 3306:3306
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: standardnotes_${{ matrix.cache_type }}
MYSQL_DATABASE: standardnotes
MYSQL_USER: standardnotes
MYSQL_PASSWORD: standardnotes
secondary_db:
image: mongo:5.0
ports:
- 27017:27017
env:
MONGO_INITDB_ROOT_USERNAME: standardnotes
MONGO_INITDB_ROOT_PASSWORD: standardnotes
MONGO_INITDB_DATABASE: standardnotes
steps:
- uses: actions/checkout@v3
@@ -123,16 +133,23 @@ jobs:
sed -i "s/VALET_TOKEN_SECRET=/VALET_TOKEN_SECRET=$(openssl rand -hex 32)/g" packages/home-server/.env
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=5" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=2" >> packages/home-server/.env
echo "DB_HOST=localhost" >> packages/home-server/.env
echo "DB_PORT=3307" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes_${{ matrix.cache_type }}" >> packages/home-server/.env
echo "DB_SQLITE_DATABASE_PATH=sqlite_${{ matrix.cache_type }}.db" >> packages/home-server/.env
echo "DB_PORT=3306" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env
echo "DB_SQLITE_DATABASE_PATH=homeserver.db" >> packages/home-server/.env
echo "DB_USERNAME=standardnotes" >> packages/home-server/.env
echo "DB_PASSWORD=standardnotes" >> packages/home-server/.env
echo "DB_TYPE=${{ matrix.db_type }}" >> packages/home-server/.env
echo "REDIS_URL=redis://localhost:${{ matrix.redis_port }}" >> packages/home-server/.env
echo "REDIS_URL=redis://localhost:6379" >> packages/home-server/.env
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
echo "SECONDARY_DB_ENABLED=${{ matrix.secondary_db_enabled }}" >> packages/home-server/.env
echo "TRANSITION_MODE_ENABLED=${{ matrix.transition_mode_enabled }}" >> packages/home-server/.env
echo "MONGO_HOST=localhost" >> packages/home-server/.env
echo "MONGO_PORT=27017" >> packages/home-server/.env
echo "MONGO_DATABASE=standardnotes" >> packages/home-server/.env
echo "MONGO_USERNAME=standardnotes" >> packages/home-server/.env
echo "MONGO_PASSWORD=standardnotes" >> packages/home-server/.env
echo "FILES_SERVER_URL=http://localhost:3123" >> packages/home-server/.env
echo "E2E_TESTING=true" >> packages/home-server/.env
Generated
+145 -1
View File
@@ -3602,6 +3602,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@mongodb-js/saslprep", [\
["npm:1.1.0", {\
"packageLocation": "./.yarn/cache/@mongodb-js-saslprep-npm-1.1.0-3906c025b8-2cf6d124d4.zip/node_modules/@mongodb-js/saslprep/",\
"packageDependencies": [\
["@mongodb-js/saslprep", "npm:1.1.0"],\
["sparse-bitfield", "npm:3.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["@mrleebo/prisma-ast", [\
["npm:0.5.2", {\
"packageLocation": "./.yarn/cache/@mrleebo-prisma-ast-npm-0.5.2-538c9d793e-69a7f3c188.zip/node_modules/@mrleebo/prisma-ast/",\
@@ -4997,6 +5007,7 @@ const RAW_RUNTIME_STATE =
["inversify", "npm:6.0.1"],\
["inversify-express-utils", "npm:6.4.3"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
["mongodb", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0"],\
["mysql2", "npm:3.3.3"],\
["newrelic", "npm:10.1.2"],\
["npm-check-updates", "npm:16.10.12"],\
@@ -5191,6 +5202,7 @@ const RAW_RUNTIME_STATE =
["inversify-express-utils", "npm:6.4.3"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
["jsonwebtoken", "npm:9.0.0"],\
["mongodb", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0"],\
["mysql2", "npm:3.3.3"],\
["newrelic", "npm:10.1.2"],\
["nodemon", "npm:2.0.22"],\
@@ -5869,6 +5881,26 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@types/webidl-conversions", [\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/@types-webidl-conversions-npm-7.0.0-0903313151-86c337dc1e.zip/node_modules/@types/webidl-conversions/",\
"packageDependencies": [\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/whatwg-url", [\
["npm:8.2.2", {\
"packageLocation": "./.yarn/cache/@types-whatwg-url-npm-8.2.2-54c5c24e6c-25f20f5649.zip/node_modules/@types/whatwg-url/",\
"packageDependencies": [\
["@types/whatwg-url", "npm:8.2.2"],\
["@types/node", "npm:20.2.5"],\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/yargs", [\
["npm:17.0.24", {\
"packageLocation": "./.yarn/cache/@types-yargs-npm-17.0.24-b034cf1d8b-f7811cc0b9.zip/node_modules/@types/yargs/",\
@@ -7074,6 +7106,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["bson", [\
["npm:6.0.0", {\
"packageLocation": "./.yarn/cache/bson-npm-6.0.0-7b3cba060e-7290998ee8.zip/node_modules/bson/",\
"packageDependencies": [\
["bson", "npm:6.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["buffer", [\
["npm:5.7.1", {\
"packageLocation": "./.yarn/cache/buffer-npm-5.7.1-513ef8259e-8e611bed4d.zip/node_modules/buffer/",\
@@ -11932,6 +11973,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["memory-pager", [\
["npm:1.5.0", {\
"packageLocation": "./.yarn/cache/memory-pager-npm-1.5.0-46e20e6c81-6b00ff499b.zip/node_modules/memory-pager/",\
"packageDependencies": [\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["meow", [\
["npm:8.1.2", {\
"packageLocation": "./.yarn/cache/meow-npm-8.1.2-bcfe48d4f3-e36c879078.zip/node_modules/meow/",\
@@ -12290,6 +12340,66 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["mongodb", [\
["npm:6.0.0", {\
"packageLocation": "./.yarn/cache/mongodb-npm-6.0.0-7c1e74de91-daec6dc9dc.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "npm:6.0.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0", {\
"packageLocation": "./.yarn/__virtual__/mongodb-virtual-789f2eaaac/0/cache/mongodb-npm-6.0.0-7c1e74de91-daec6dc9dc.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0"],\
["@aws-sdk/credential-providers", null],\
["@mongodb-js/saslprep", "npm:1.1.0"],\
["@mongodb-js/zstd", null],\
["@types/aws-sdk__credential-providers", null],\
["@types/gcp-metadata", null],\
["@types/kerberos", null],\
["@types/mongodb-client-encryption", null],\
["@types/mongodb-js__zstd", null],\
["@types/snappy", null],\
["@types/socks", null],\
["bson", "npm:6.0.0"],\
["gcp-metadata", null],\
["kerberos", null],\
["mongodb-client-encryption", null],\
["mongodb-connection-string-url", "npm:2.6.0"],\
["snappy", null],\
["socks", null]\
],\
"packagePeers": [\
"@aws-sdk/credential-providers",\
"@mongodb-js/zstd",\
"@types/aws-sdk__credential-providers",\
"@types/gcp-metadata",\
"@types/kerberos",\
"@types/mongodb-client-encryption",\
"@types/mongodb-js__zstd",\
"@types/snappy",\
"@types/socks",\
"gcp-metadata",\
"kerberos",\
"mongodb-client-encryption",\
"snappy",\
"socks"\
],\
"linkType": "HARD"\
}]\
]],\
["mongodb-connection-string-url", [\
["npm:2.6.0", {\
"packageLocation": "./.yarn/cache/mongodb-connection-string-url-npm-2.6.0-af011ba17f-8a9186dd1b.zip/node_modules/mongodb-connection-string-url/",\
"packageDependencies": [\
["mongodb-connection-string-url", "npm:2.6.0"],\
["@types/whatwg-url", "npm:8.2.2"],\
["whatwg-url", "npm:11.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["ms", [\
["npm:2.0.0", {\
"packageLocation": "./.yarn/cache/ms-npm-2.0.0-9e1101a471-de027828fc.zip/node_modules/ms/",\
@@ -14604,6 +14714,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["sparse-bitfield", [\
["npm:3.0.3", {\
"packageLocation": "./.yarn/cache/sparse-bitfield-npm-3.0.3-cb80d0c89f-625ecdf6f4.zip/node_modules/sparse-bitfield/",\
"packageDependencies": [\
["sparse-bitfield", "npm:3.0.3"],\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["spawn-please", [\
["npm:2.0.1", {\
"packageLocation": "./.yarn/cache/spawn-please-npm-2.0.1-265b6b5432-fe19a7ceb5.zip/node_modules/spawn-please/",\
@@ -15246,6 +15366,14 @@ const RAW_RUNTIME_STATE =
["tr46", "npm:0.0.3"]\
],\
"linkType": "HARD"\
}],\
["npm:3.0.0", {\
"packageLocation": "./.yarn/cache/tr46-npm-3.0.0-e1ae1ea7c9-3a481676bf.zip/node_modules/tr46/",\
"packageDependencies": [\
["tr46", "npm:3.0.0"],\
["punycode", "npm:2.3.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["treeverse", [\
@@ -15701,7 +15829,7 @@ const RAW_RUNTIME_STATE =
["hdb-pool", null],\
["ioredis", null],\
["mkdirp", "npm:2.1.6"],\
["mongodb", null],\
["mongodb", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:6.0.0"],\
["mssql", null],\
["mysql2", "npm:3.3.3"],\
["oracledb", null],\
@@ -16191,6 +16319,13 @@ const RAW_RUNTIME_STATE =
["webidl-conversions", "npm:3.0.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/webidl-conversions-npm-7.0.0-e8c8e30c68-bdbe11c68c.zip/node_modules/webidl-conversions/",\
"packageDependencies": [\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["webpack", [\
@@ -16249,6 +16384,15 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["whatwg-url", [\
["npm:11.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-11.0.0-073529d93a-ee3a532bfb.zip/node_modules/whatwg-url/",\
"packageDependencies": [\
["whatwg-url", "npm:11.0.0"],\
["tr46", "npm:3.0.0"],\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}],\
["npm:5.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-bd0cc6b75b.zip/node_modules/whatwg-url/",\
"packageDependencies": [\
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17
View File
@@ -23,6 +23,8 @@ services:
environment:
DB_TYPE: "${DB_TYPE}"
CACHE_TYPE: "${CACHE_TYPE}"
SECONDARY_DB_ENABLED: "${SECONDARY_DB_ENABLED}"
TRANSITION_MODE_ENABLED: "${TRANSITION_MODE_ENABLED}"
container_name: server-ci
ports:
- 3123:3000
@@ -61,6 +63,21 @@ services:
networks:
- standardnotes_self_hosted
secondary_db:
image: mongo:5.0
container_name: secondary_db-ci
expose:
- 27017
restart: unless-stopped
volumes:
- ./data/mongo:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: standardnotes
MONGO_INITDB_ROOT_PASSWORD: standardnotes
MONGO_INITDB_DATABASE: standardnotes
networks:
- standardnotes_self_hosted
cache:
image: redis:6.0-alpine
container_name: cache-ci
+8
View File
@@ -2,6 +2,8 @@
# Setup environment variables
export MODE="self-hosted"
#########
# PORTS #
#########
@@ -63,6 +65,12 @@ fi
if [ -z "$CACHE_TYPE" ]; then
export CACHE_TYPE="redis"
fi
if [ -z "$SECONDARY_DB_ENABLED" ]; then
export SECONDARY_DB_ENABLED=false
fi
if [ -z "$TRANSITION_MODE_ENABLED" ]; then
export TRANSITION_MODE_ENABLED=false
fi
export DB_MIGRATIONS_PATH="dist/migrations/*.js"
#########
+8 -2
View File
@@ -147,10 +147,16 @@ LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $SYNCING_SERVER_
echo "linking done:"
echo "$LINKING_RESULT"
echo "linking topic $SYNCING_SERVER_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
echo "linking topic $FILES_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic $FILES_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
echo "linking done:"
echo "$LINKING_RESULT"
echo "linking topic $SYNCING_SERVER_TOPIC_ARN to queue $AUTH_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $AUTH_QUEUE_ARN)
echo "linking done:"
echo "$LINKING_RESULT"
echo "linking topic $AUTH_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic $AUTH_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
echo "linking done:"
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.25.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.16...@standardnotes/analytics@2.25.17) (2023-08-24)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.15...@standardnotes/analytics@2.25.16) (2023-08-23)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.14...@standardnotes/analytics@2.25.15) (2023-08-22)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.13...@standardnotes/analytics@2.25.14) (2023-08-18)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.12...@standardnotes/analytics@2.25.13) (2023-08-11)
**Note:** Version bump only for package @standardnotes/analytics
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.25.13",
"version": "2.25.17",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+1 -1
View File
@@ -1,4 +1,4 @@
MODE=microservice # microservice | home-server
MODE=microservice # microservice | home-server | self-hosted
LOG_LEVEL=debug
NODE_ENV=development
VERSION=development
+26
View File
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.72.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.72.0...@standardnotes/api-gateway@1.72.1) (2023-08-28)
### Bug Fixes
* allow self hosted to use new model of items ([#714](https://github.com/standardnotes/api-gateway/issues/714)) ([aef9254](https://github.com/standardnotes/api-gateway/commit/aef9254713560c00a90a3e84e3cd94417e8f30d2))
# [1.72.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.1...@standardnotes/api-gateway@1.72.0) (2023-08-24)
### Features
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/api-gateway/issues/707)) ([05bb12c](https://github.com/standardnotes/api-gateway/commit/05bb12c97899824f06e6d01d105dec75fc328440))
## [1.71.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.0...@standardnotes/api-gateway@1.71.1) (2023-08-23)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/api-gateway/issues/704)) ([34085ac](https://github.com/standardnotes/api-gateway/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
## [1.70.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.4...@standardnotes/api-gateway@1.70.5) (2023-08-18)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.3...@standardnotes/api-gateway@1.70.4) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.70.4",
"version": "1.72.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -27,16 +27,23 @@ export abstract class AuthMiddleware extends BaseMiddleware {
}
const authHeaderValue = request.headers.authorization as string
const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
const cacheKey = `${authHeaderValue}${
sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
}`
try {
let crossServiceTokenFetchedFromCache = true
let crossServiceToken = null
if (this.crossServiceTokenCacheTTL) {
crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue)
crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
}
if (crossServiceToken === null) {
const authResponse = await this.serviceProxy.validateSession(authHeaderValue)
if (this.crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken)) {
const authResponse = await this.serviceProxy.validateSession({
authorization: authHeaderValue,
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
})
if (!this.handleSessionValidationResponse(authResponse, response, next)) {
return
@@ -48,12 +55,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
response.locals.authToken = crossServiceToken
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
const decodedToken = <CrossServiceTokenData>(
verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
)
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({
authorizationHeaderValue: authHeaderValue,
encodedCrossServiceToken: crossServiceToken,
key: cacheKey,
encodedCrossServiceToken: response.locals.authToken,
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
userUuid: decodedToken.user.uuid,
})
@@ -62,6 +71,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
} catch (error) {
const errorMessage = (error as AxiosError).isAxiosError
? JSON.stringify((error as AxiosError).response?.data)
@@ -118,4 +128,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration)
}
private crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken: string | null) {
if (crossServiceToken === null) {
return true
}
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
return decodedToken.ongoing_transition === true
}
}
@@ -34,6 +34,16 @@ export class ItemsController extends BaseHttpController {
)
}
@httpPost('/transition')
async transition(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callSyncingServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'items/transition'),
request.body,
)
}
@httpGet('/:uuid')
async getItem(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callSyncingServer(
@@ -80,6 +80,15 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/transition-status', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getTransitionStatus(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/transition-status'),
)
}
@httpGet('/:userId/params', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getKeyParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
@@ -12,29 +12,29 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
constructor(private timer: TimerInterface) {}
async set(dto: {
authorizationHeaderValue: string
key: string
encodedCrossServiceToken: string
expiresAtInSeconds: number
userUuid: string
}): Promise<void> {
let userAuthHeaders = []
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
if (userAuthHeadersJSON) {
userAuthHeaders = JSON.parse(userAuthHeadersJSON)
let userKeys = []
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
if (userKeysJSON) {
userKeys = JSON.parse(userKeysJSON)
}
userAuthHeaders.push(dto.authorizationHeaderValue)
userKeys.push(dto.key)
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userAuthHeaders))
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userKeys))
this.crossServiceTokenTTLCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
}
async get(authorizationHeaderValue: string): Promise<string | null> {
async get(key: string): Promise<string | null> {
this.invalidateExpiredTokens()
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${authorizationHeaderValue}`)
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${key}`)
if (!cachedToken) {
return null
}
@@ -43,15 +43,15 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
}
async invalidate(userUuid: string): Promise<void> {
let userAuthorizationHeaderValues = []
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
if (userAuthHeadersJSON) {
userAuthorizationHeaderValues = JSON.parse(userAuthHeadersJSON)
let userKeyValues = []
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
if (userKeysJSON) {
userKeyValues = JSON.parse(userKeysJSON)
}
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
this.crossServiceTokenCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
for (const key of userKeyValues) {
this.crossServiceTokenCache.delete(`${this.PREFIX}:${key}`)
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${key}`)
}
this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
this.crossServiceTokenTTLCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
@@ -12,32 +12,32 @@ export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterf
constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
async set(dto: {
authorizationHeaderValue: string
key: string
encodedCrossServiceToken: string
expiresAtInSeconds: number
userUuid: string
}): Promise<void> {
const pipeline = this.redisClient.pipeline()
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.authorizationHeaderValue)
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.key)
pipeline.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
pipeline.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
pipeline.expireat(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
await pipeline.exec()
}
async get(authorizationHeaderValue: string): Promise<string | null> {
return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`)
async get(key: string): Promise<string | null> {
return this.redisClient.get(`${this.PREFIX}:${key}`)
}
async invalidate(userUuid: string): Promise<void> {
const userAuthorizationHeaderValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
const userKeyValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
const pipeline = this.redisClient.pipeline()
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`)
for (const key of userKeyValues) {
pipeline.del(`${this.PREFIX}:${key}`)
}
pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)
@@ -1,10 +1,10 @@
export interface CrossServiceTokenCacheInterface {
set(dto: {
authorizationHeaderValue: string
key: string
encodedCrossServiceToken: string
expiresAtInSeconds: number
userUuid: string
}): Promise<void>
get(authorizationHeaderValue: string): Promise<string | null>
get(key: string): Promise<string | null>
invalidate(userUuid: string): Promise<void>
}
@@ -24,14 +24,16 @@ export class HttpServiceProxy implements ServiceProxyInterface {
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
) {}
async validateSession(
authorizationHeaderValue: string,
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
async validateSession(headers: {
authorization: string
sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authResponse = await this.httpClient.request({
method: 'POST',
headers: {
Authorization: authorizationHeaderValue,
Authorization: headers.authorization,
Accept: 'application/json',
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
validateStatus: (status: number) => {
return status >= 200 && status < 500
@@ -50,7 +50,7 @@ export interface ServiceProxyInterface {
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
): Promise<void>
validateSession(authorizationHeaderValue: string): Promise<{
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
status: number
data: unknown
headers: {
@@ -6,9 +6,10 @@ import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
export class DirectCallServiceProxy implements ServiceProxyInterface {
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
async validateSession(
authorizationHeaderValue: string,
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
async validateSession(headers: {
authorization: string
sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
if (!authService) {
throw new Error('Auth service not found')
@@ -17,7 +18,8 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
const serviceResponse = (await authService.handleRequest(
{
headers: {
authorization: authorizationHeaderValue,
authorization: headers.authorization,
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
} as never,
{} as never,
@@ -42,7 +42,8 @@ export class EndpointResolver implements EndpointResolverInterface {
// Users Controller
['[PATCH]:users/:userId', 'auth.users.update'],
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
['[PUT]:auth/params', 'auth.users.getKeyParams'],
['[GET]:users/params', 'auth.users.getKeyParams'],
['[GET]:users/transition-status', 'auth.users.transition-status'],
['[DELETE]:users/:userUuid', 'auth.users.delete'],
['[POST]:listed', 'auth.users.createListedAccount'],
['[POST]:auth', 'auth.users.register'],
@@ -58,6 +59,7 @@ export class EndpointResolver implements EndpointResolverInterface {
// Syncing Server
['[POST]:items/sync', 'sync.items.sync'],
['[POST]:items/check-integrity', 'sync.items.check_integrity'],
['[POST]:items/transition', 'sync.items.transition'],
['[GET]:items/:uuid', 'sync.items.get_item'],
// Revisions Controller V2
['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'],
+1 -1
View File
@@ -1,4 +1,4 @@
MODE=microservice # microservice | home-server
MODE=microservice # microservice | home-server | self-hosted
LOG_LEVEL=debug
NODE_ENV=development
VERSION=development
+42
View File
@@ -3,6 +3,48 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.135.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.1...@standardnotes/auth-server@1.135.2) (2023-08-28)
### Bug Fixes
* allow self hosted to use new model of items ([#714](https://github.com/standardnotes/server/issues/714)) ([aef9254](https://github.com/standardnotes/server/commit/aef9254713560c00a90a3e84e3cd94417e8f30d2))
## [1.135.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.0...@standardnotes/auth-server@1.135.1) (2023-08-25)
### Bug Fixes
* **auth:** account enumeration with pseudo u2f and mfa ([#709](https://github.com/standardnotes/server/issues/709)) ([bbb35d1](https://github.com/standardnotes/server/commit/bbb35d16fc4f6a57fe774a648fbda13ec64a8865))
# [1.135.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.134.0...@standardnotes/auth-server@1.135.0) (2023-08-24)
### Features
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/server/issues/707)) ([05bb12c](https://github.com/standardnotes/server/commit/05bb12c97899824f06e6d01d105dec75fc328440))
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.133.0...@standardnotes/auth-server@1.134.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.132.0...@standardnotes/auth-server@1.133.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/server/issues/704)) ([34085ac](https://github.com/standardnotes/server/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
# [1.132.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.5...@standardnotes/auth-server@1.132.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.131.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.4...@standardnotes/auth-server@1.131.5) (2023-08-15)
### Bug Fixes
* **auth:** passing the invalidate cache header ([#697](https://github.com/standardnotes/server/issues/697)) ([83ad069](https://github.com/standardnotes/server/commit/83ad069c5dd9afa3a6db881f0d8a55a58d0642aa))
## [1.131.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.3...@standardnotes/auth-server@1.131.4) (2023-08-11)
**Note:** Version bump only for package @standardnotes/auth-server
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddTransitionRole1692348191367 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
)
}
public async down(): Promise<void> {
return
}
}
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddTransitionRole1692348280258 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
)
}
public async down(): Promise<void> {
return
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.131.4",
"version": "1.135.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+59 -8
View File
@@ -256,6 +256,13 @@ import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAc
import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
import { TransitionStatusRepositoryInterface } from '../Domain/Transition/TransitionStatusRepositoryInterface'
import { RedisTransitionStatusRepository } from '../Infra/Redis/RedisTransitionStatusRepository'
import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTransitionStatusRepository'
import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
import { GetTransitionStatus } from '../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
export class ContainerConfigLoader {
async load(configuration?: {
@@ -560,6 +567,9 @@ export class ContainerConfigLoader {
container
.bind(TYPES.Auth_READONLY_USERS)
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
container
.bind(TYPES.Auth_TRANSITION_MODE_ENABLED)
.toConstantValue(env.get('TRANSITION_MODE_ENABLED', true) === 'true')
if (isConfiguredForInMemoryCache) {
container
@@ -606,6 +616,9 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Timer),
),
)
container
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
.toConstantValue(new InMemoryTransitionStatusRepository())
} else {
container.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository).to(RedisPKCERepository)
container.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository).to(LockRepository)
@@ -618,6 +631,9 @@ export class ContainerConfigLoader {
container
.bind<SubscriptionTokenRepositoryInterface>(TYPES.Auth_SubscriptionTokenRepository)
.to(RedisSubscriptionTokenRepository)
container
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
.toConstantValue(new RedisTransitionStatusRepository(container.get<Redis>(TYPES.Auth_Redis)))
}
// Services
@@ -894,6 +910,22 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_SubscriptionSettingService),
),
)
container
.bind<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus)
.toConstantValue(
new UpdateTransitionStatus(
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
),
)
container
.bind<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus)
.toConstantValue(
new GetTransitionStatus(
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
),
)
// Controller
container
@@ -979,6 +1011,14 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Logger),
),
)
container
.bind<SharedVaultFileMovedEventHandler>(TYPES.Auth_SharedVaultFileMovedEventHandler)
.toConstantValue(
new SharedVaultFileMovedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
.toConstantValue(
@@ -1027,6 +1067,14 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Logger),
),
)
container
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Auth_TransitionStatusUpdatedEventHandler)
.toConstantValue(
new TransitionStatusUpdatedEventHandler(
container.get<UpdateTransitionStatus>(TYPES.Auth_UpdateTransitionStatus),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
@@ -1042,6 +1090,7 @@ export class ContainerConfigLoader {
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)],
['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],
@@ -1057,6 +1106,7 @@ export class ContainerConfigLoader {
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.Auth_PredicateVerificationRequestedEventHandler)],
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)],
['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)],
['TRANSITION_STATUS_UPDATED', container.get(TYPES.Auth_TransitionStatusUpdatedEventHandler)],
])
if (isConfiguredForHomeServer) {
@@ -1161,14 +1211,15 @@ export class ContainerConfigLoader {
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
.toConstantValue(
new BaseUsersController(
container.get(TYPES.Auth_UpdateUser),
container.get(TYPES.Auth_GetUserKeyParams),
container.get(TYPES.Auth_DeleteAccount),
container.get(TYPES.Auth_GetUserSubscription),
container.get(TYPES.Auth_ClearLoginAttempts),
container.get(TYPES.Auth_IncreaseLoginAttempts),
container.get(TYPES.Auth_ChangeCredentials),
container.get(TYPES.Auth_ControllerContainer),
container.get<UpdateUser>(TYPES.Auth_UpdateUser),
container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription),
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
container.get<ChangeCredentials>(TYPES.Auth_ChangeCredentials),
container.get<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus),
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
),
)
container
+1
View File
@@ -27,6 +27,7 @@ export class Service implements AuthServiceInterface {
async activatePremiumFeatures(dto: {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}): Promise<Result<string>> {
if (!this.container) {
+6
View File
@@ -35,6 +35,7 @@ const TYPES = {
Auth_AuthenticatorRepository: Symbol.for('Auth_AuthenticatorRepository'),
Auth_AuthenticatorChallengeRepository: Symbol.for('Auth_AuthenticatorChallengeRepository'),
Auth_CacheEntryRepository: Symbol.for('Auth_CacheEntryRepository'),
Auth_TransitionStatusRepository: Symbol.for('Auth_TransitionStatusRepository'),
// ORM
Auth_ORMOfflineSettingRepository: Symbol.for('Auth_ORMOfflineSettingRepository'),
Auth_ORMOfflineUserSubscriptionRepository: Symbol.for('Auth_ORMOfflineUserSubscriptionRepository'),
@@ -101,6 +102,7 @@ const TYPES = {
Auth_U2F_EXPECTED_ORIGIN: Symbol.for('Auth_U2F_EXPECTED_ORIGIN'),
Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'),
Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'),
Auth_TRANSITION_MODE_ENABLED: Symbol.for('Auth_TRANSITION_MODE_ENABLED'),
// use cases
Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'),
Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'),
@@ -153,6 +155,8 @@ const TYPES = {
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'),
// Handlers
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
@@ -167,6 +171,7 @@ const TYPES = {
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),
@@ -180,6 +185,7 @@ const TYPES = {
Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'),
Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
Auth_TransitionStatusUpdatedEventHandler: Symbol.for('Auth_TransitionStatusUpdatedEventHandler'),
// Services
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
Auth_SessionService: Symbol.for('Auth_SessionService'),
@@ -0,0 +1,28 @@
import { DomainEventHandlerInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
export class SharedVaultFileMovedEventHandler implements DomainEventHandlerInterface {
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: SharedVaultFileMovedEvent): Promise<void> {
const subtractResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.from.ownerUuid,
bytesUsed: -event.payload.fileByteSize,
})
if (subtractResult.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${subtractResult.getError()}`)
}
const addResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.to.ownerUuid,
bytesUsed: event.payload.fileByteSize,
})
if (addResult.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${addResult.getError()}`)
}
}
}
@@ -60,7 +60,7 @@ describe('SubscriptionExpiredEventHandler', () => {
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.removeUserRole = jest.fn()
roleService.removeUserRoleBasedOnSubscription = jest.fn()
timestamp = dayjs.utc().valueOf()
@@ -86,7 +86,7 @@ describe('SubscriptionExpiredEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
})
it('should update subscription ends at', async () => {
@@ -108,7 +108,7 @@ describe('SubscriptionExpiredEventHandler', () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
})
@@ -117,7 +117,7 @@ describe('SubscriptionExpiredEventHandler', () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
})
})
@@ -48,7 +48,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
await this.roleService.removeUserRole(await userSubscription.user, subscriptionName)
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
}
}
@@ -72,7 +72,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
roleService.setOfflineUserRole = jest.fn()
subscriptionExpiresAt = timestamp + 365 * 1000
@@ -106,7 +106,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
})
it('should update user default settings', async () => {
@@ -162,7 +162,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -171,7 +171,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
})
@@ -70,7 +70,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
}
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
await this.roleService.addUserRole(user, subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}
private async createSubscription(
@@ -62,7 +62,7 @@ describe('SubscriptionReassignedEventHandler', () => {
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
subscriptionExpiresAt = timestamp + 365 * 1000
@@ -100,7 +100,7 @@ describe('SubscriptionReassignedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
})
it('should create subscription', async () => {
@@ -146,7 +146,7 @@ describe('SubscriptionReassignedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -155,7 +155,7 @@ describe('SubscriptionReassignedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
})
@@ -67,7 +67,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
}
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
await this.roleService.addUserRole(user, subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}
private async createSubscription(
@@ -61,7 +61,7 @@ describe('SubscriptionRefundedEventHandler', () => {
offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.removeUserRole = jest.fn()
roleService.removeUserRoleBasedOnSubscription = jest.fn()
timestamp = dayjs.utc().valueOf()
@@ -87,7 +87,7 @@ describe('SubscriptionRefundedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.PlusPlan)
})
it('should update subscription ends at', async () => {
@@ -109,7 +109,7 @@ describe('SubscriptionRefundedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
})
@@ -118,7 +118,7 @@ describe('SubscriptionRefundedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
})
})
@@ -48,7 +48,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
await this.roleService.removeUserRole(await userSubscription.user, subscriptionName)
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
}
}
@@ -67,7 +67,7 @@ describe('SubscriptionRenewedEventHandler', () => {
offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
roleService.setOfflineUserRole = jest.fn()
timestamp = dayjs.utc().valueOf()
@@ -107,7 +107,7 @@ describe('SubscriptionRenewedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
})
it('should update the offline user role', async () => {
@@ -123,7 +123,7 @@ describe('SubscriptionRenewedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -132,7 +132,7 @@ describe('SubscriptionRenewedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -143,7 +143,7 @@ describe('SubscriptionRenewedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
})
@@ -73,7 +73,7 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
for (const userSubscription of userSubscriptions) {
const user = await userSubscription.user
await this.roleService.addUserRole(user, subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}
}
@@ -88,7 +88,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
})
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
roleService.setOfflineUserRole = jest.fn()
subscriptionExpiresAt = timestamp + 365 * 1000
@@ -121,7 +121,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
it('should update the user role', async () => {
await createHandler().handle(event)
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
})
it('should update user default settings', async () => {
@@ -243,7 +243,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
@@ -252,7 +252,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
})
})
@@ -93,7 +93,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
event.payload.timestamp,
)
await this.roleService.addUserRole(user, event.payload.subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
@@ -0,0 +1,18 @@
import { DomainEventHandlerInterface, TransitionStatusUpdatedEvent } from '@standardnotes/domain-events'
import { UpdateTransitionStatus } from '../UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
import { Logger } from 'winston'
export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface {
constructor(private updateTransitionStatusUseCase: UpdateTransitionStatus, private logger: Logger) {}
async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
const result = await this.updateTransitionStatusUseCase.execute({
status: event.payload.status,
userUuid: event.payload.userUuid,
})
if (result.isFailed()) {
this.logger.error(`Failed to update transition status for user ${event.payload.userUuid}`)
}
}
}
@@ -5,7 +5,7 @@ import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
import { SubscriptionName } from '@standardnotes/common'
import { RoleName } from '@standardnotes/domain-core'
import { RoleName, Uuid } from '@standardnotes/domain-core'
import { Role } from '../Role/Role'
import { ClientServiceInterface } from '../Client/ClientServiceInterface'
@@ -81,9 +81,44 @@ describe('RoleService', () => {
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
logger.warn = jest.fn()
logger.error = jest.fn()
})
describe('adding roles', () => {
beforeEach(() => {
user = {
uuid: '123',
email: 'test@test.com',
roles: Promise.resolve([basicRole]),
} as jest.Mocked<User>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
userRepository.save = jest.fn().mockReturnValue(user)
})
it('should add a role to a user', async () => {
await createService().addRoleToUser(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
RoleName.create(RoleName.NAMES.ProUser).getValue(),
)
user.roles = Promise.resolve([basicRole, proRole])
expect(userRepository.save).toHaveBeenCalledWith(user)
})
it('should not add a role to a user if the user could not be found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
await createService().addRoleToUser(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
RoleName.create(RoleName.NAMES.ProUser).getValue(),
)
expect(userRepository.save).not.toHaveBeenCalled()
})
})
describe('adding roles based on subscription', () => {
beforeEach(() => {
user = {
uuid: '123',
@@ -96,7 +131,7 @@ describe('RoleService', () => {
})
it('should add role to user', async () => {
await createService().addUserRole(user, SubscriptionName.ProPlan)
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
user.roles = Promise.resolve([basicRole, proRole])
@@ -112,7 +147,7 @@ describe('RoleService', () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
await createService().addUserRole(user, SubscriptionName.ProPlan)
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
expect(userRepository.save).toHaveBeenCalledWith(user)
@@ -120,7 +155,7 @@ describe('RoleService', () => {
})
it('should send websockets event', async () => {
await createService().addUserRole(user, SubscriptionName.ProPlan)
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user)
})
@@ -128,14 +163,14 @@ describe('RoleService', () => {
it('should not add role if no role name exists for subscription name', async () => {
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined)
await createService().addUserRole(user, 'test' as SubscriptionName)
await createService().addUserRoleBasedOnSubscription(user, 'test' as SubscriptionName)
expect(userRepository.save).not.toHaveBeenCalled()
})
it('should not add role if no role exists for role name', async () => {
roleRepository.findOneByName = jest.fn().mockReturnValue(null)
await createService().addUserRole(user, SubscriptionName.ProPlan)
await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(userRepository.save).not.toHaveBeenCalled()
})
@@ -169,7 +204,7 @@ describe('RoleService', () => {
})
})
describe('removing roles', () => {
describe('removing roles based on subscription', () => {
beforeEach(() => {
user = {
uuid: '123',
@@ -182,13 +217,13 @@ describe('RoleService', () => {
})
it('should remove role from user', async () => {
await createService().removeUserRole(user, SubscriptionName.ProPlan)
await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(userRepository.save).toHaveBeenCalledWith(user)
})
it('should send websockets event', async () => {
await createService().removeUserRole(user, SubscriptionName.ProPlan)
await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user)
})
@@ -196,7 +231,7 @@ describe('RoleService', () => {
it('should not remove role if role name does not exist for subscription name', async () => {
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined)
await createService().removeUserRole(user, 'test' as SubscriptionName)
await createService().removeUserRoleBasedOnSubscription(user, 'test' as SubscriptionName)
expect(userRepository.save).not.toHaveBeenCalled()
})
+38 -22
View File
@@ -13,7 +13,7 @@ import { RoleToSubscriptionMapInterface } from './RoleToSubscriptionMapInterface
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Role } from './Role'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { Uuid } from '@standardnotes/domain-core'
import { RoleName, Uuid } from '@standardnotes/domain-core'
@injectable()
export class RoleService implements RoleServiceInterface {
@@ -54,7 +54,18 @@ export class RoleService implements RoleServiceInterface {
return false
}
async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
async addRoleToUser(userUuid: Uuid, roleName: RoleName): Promise<void> {
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.error(`Could not find user with uuid ${userUuid.value} to add role ${roleName.value}`)
return
}
await this.addToExistingRoles(user, roleName.value)
}
async addUserRoleBasedOnSubscription(user: User, subscriptionName: SubscriptionName): Promise<void> {
const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
if (roleName === undefined) {
@@ -62,25 +73,7 @@ export class RoleService implements RoleServiceInterface {
return
}
const role = await this.roleRepository.findOneByName(roleName)
if (role === null) {
this.logger.warn(`Could not find role for role name: ${roleName}`)
return
}
const rolesMap = new Map<string, Role>()
const currentRoles = await user.roles
for (const currentRole of currentRoles) {
rolesMap.set(currentRole.name, currentRole)
}
if (!rolesMap.has(role.name)) {
rolesMap.set(role.name, role)
}
user.roles = Promise.resolve([...rolesMap.values()])
await this.userRepository.save(user)
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
await this.addToExistingRoles(user, roleName)
}
async setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void> {
@@ -107,7 +100,7 @@ export class RoleService implements RoleServiceInterface {
await this.offlineUserSubscriptionRepository.save(offlineUserSubscription)
}
async removeUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
async removeUserRoleBasedOnSubscription(user: User, subscriptionName: SubscriptionName): Promise<void> {
const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
if (roleName === undefined) {
@@ -120,4 +113,27 @@ export class RoleService implements RoleServiceInterface {
await this.userRepository.save(user)
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
}
private async addToExistingRoles(user: User, roleNameString: string): Promise<void> {
const role = await this.roleRepository.findOneByName(roleNameString)
if (role === null) {
this.logger.warn(`Could not find role for role name: ${roleNameString}`)
return
}
const rolesMap = new Map<string, Role>()
const currentRoles = await user.roles
for (const currentRole of currentRoles) {
rolesMap.set(currentRole.name, currentRole)
}
if (!rolesMap.has(role.name)) {
rolesMap.set(role.name, role)
}
user.roles = Promise.resolve([...rolesMap.values()])
await this.userRepository.save(user)
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
}
}
@@ -1,10 +1,12 @@
import { PermissionName } from '@standardnotes/features'
import { RoleName, Uuid } from '@standardnotes/domain-core'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { User } from '../User/User'
export interface RoleServiceInterface {
addUserRole(user: User, subscriptionName: string): Promise<void>
addRoleToUser(userUuid: Uuid, roleName: RoleName): Promise<void>
addUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void>
removeUserRole(user: User, subscriptionName: string): Promise<void>
removeUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
userHasPermission(userUuid: string, permissionName: PermissionName): Promise<boolean>
}
@@ -0,0 +1,5 @@
export interface TransitionStatusRepositoryInterface {
updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void>
removeStatus(userUuid: string): Promise<void>
getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null>
}
@@ -69,7 +69,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
userSubscriptionRepository.save = jest.fn().mockReturnValue(inviteeSubscription)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
@@ -103,7 +103,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
updatedAt: 1,
user: Promise.resolve(invitee),
})
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
inviteeSubscription,
)
@@ -143,7 +143,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
updatedAt: 3,
user: Promise.resolve(invitee),
})
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
inviteeSubscription,
)
@@ -162,7 +162,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
@@ -180,7 +180,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
@@ -202,7 +202,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
@@ -219,7 +219,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
@@ -244,7 +244,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
})
})
@@ -100,7 +100,7 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
}
private async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
await this.roleService.addUserRole(user, subscriptionName)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}
private async createSharedSubscription(
@@ -34,7 +34,7 @@ describe('ActivatePremiumFeatures', () => {
userSubscriptionRepository.save = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
@@ -73,7 +73,7 @@ describe('ActivatePremiumFeatures', () => {
expect(result.isFailed()).toBe(false)
expect(userSubscriptionRepository.save).toHaveBeenCalled()
expect(roleService.addUserRole).toHaveBeenCalled()
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalled()
})
it('should save a subscription with custom plan name and endsAt', async () => {
@@ -53,11 +53,11 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
await this.userSubscriptionRepository.save(subscription)
await this.roleService.addUserRole(user, subscriptionPlanName.value)
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionPlanName.value)
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
subscription,
new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]),
new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
)
return Result.ok('Premium features activated.')
@@ -1,5 +1,6 @@
export interface ActivatePremiumFeaturesDTO {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}
@@ -84,7 +84,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
userSubscriptionRepository.save = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.removeUserRole = jest.fn()
roleService.removeUserRoleBasedOnSubscription = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
@@ -122,7 +122,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
endsAt: 1,
user: Promise.resolve(invitee),
})
expect(roleService.removeUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createSharedSubscriptionInvitationCanceledEvent).toHaveBeenCalledWith({
inviteeIdentifier: '123',
@@ -156,7 +156,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviteeIdentifierType: 'email',
})
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.removeUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
})
it('should not cancel a shared subscription invitation if it is not found', async () => {
@@ -204,7 +204,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviteeIdentifierType: 'email',
})
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
})
it('should not cancel a shared subscription invitation if inviter subscription is not found', async () => {
@@ -90,7 +90,10 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
if (invitee !== null) {
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
await this.roleService.removeUserRoleBasedOnSubscription(
invitee,
inviterUserSubscription.planName as SubscriptionName,
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
@@ -8,6 +8,9 @@ import { Role } from '../../Role/Role'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
import { GetSetting } from '../GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
describe('CreateCrossServiceToken', () => {
let userProjector: ProjectorInterface<User>
@@ -15,6 +18,8 @@ describe('CreateCrossServiceToken', () => {
let roleProjector: ProjectorInterface<Role>
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
let userRepository: UserRepositoryInterface
let getSettingUseCase: GetSetting
let transitionStatusRepository: TransitionStatusRepositoryInterface
const jwtTTL = 60
let session: Session
@@ -22,7 +27,16 @@ describe('CreateCrossServiceToken', () => {
let role: Role
const createUseCase = () =>
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
new CreateCrossServiceToken(
userProjector,
sessionProjector,
roleProjector,
tokenEncoder,
userRepository,
jwtTTL,
getSettingUseCase,
transitionStatusRepository,
)
beforeEach(() => {
session = {} as jest.Mocked<Session>
@@ -50,6 +64,12 @@ describe('CreateCrossServiceToken', () => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
getSettingUseCase = {} as jest.Mocked<GetSetting>
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } }))
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('TO-DO')
})
it('should create a cross service token for user', async () => {
@@ -73,6 +93,36 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
},
60,
)
})
it('should create a cross service token for user that has an ongoing transaction', async () => {
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
await createUseCase().execute({
user,
session,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: true,
},
60,
)
@@ -95,6 +145,7 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
},
60,
)
@@ -117,6 +168,7 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
},
60,
)
@@ -125,28 +177,75 @@ describe('CreateCrossServiceToken', () => {
it('should throw an error if user does not exist', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
let caughtError = null
try {
await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
} catch (error) {
caughtError = error
}
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(caughtError).not.toBeNull()
expect(result.isFailed()).toBeTruthy()
})
it('should throw an error if user uuid is invalid', async () => {
let caughtError = null
try {
await createUseCase().execute({
userUuid: 'invalid',
})
} catch (error) {
caughtError = error
}
const result = await createUseCase().execute({
userUuid: 'invalid',
})
expect(caughtError).not.toBeNull()
expect(result.isFailed()).toBeTruthy()
})
describe('shared vault context', () => {
it('should add shared vault context if shared vault owner uuid is provided', async () => {
await createUseCase().execute({
user,
session,
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
shared_vault_owner_context: {
upload_bytes_limit: 100,
},
user: {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
},
60,
)
})
it('should throw an error if shared vault owner context is sensitive', async () => {
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sensitive: true }))
const result = await createUseCase().execute({
user,
session,
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
})
it('should throw an error if it fails to retrieve shared vault owner setting', async () => {
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const result = await createUseCase().execute({
user,
session,
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
})
})
})
@@ -1,5 +1,6 @@
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
@@ -7,14 +8,14 @@ import { Role } from '../../Role/Role'
import { Session } from '../../Session/Session'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
import { Uuid } from '@standardnotes/domain-core'
import { GetSetting } from '../GetSetting/GetSetting'
import { SettingName } from '@standardnotes/settings'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
@injectable()
export class CreateCrossServiceToken implements UseCaseInterface {
export class CreateCrossServiceToken implements UseCaseInterface<string> {
constructor(
@inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>,
@inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface<Session>,
@@ -22,14 +23,18 @@ export class CreateCrossServiceToken implements UseCaseInterface {
@inject(TYPES.Auth_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
@inject(TYPES.Auth_GetSetting)
private getSettingUseCase: GetSetting,
@inject(TYPES.Auth_TransitionStatusRepository)
private transitionStatusRepository: TransitionStatusRepositoryInterface,
) {}
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
async execute(dto: CreateCrossServiceTokenDTO): Promise<Result<string>> {
let user: User | undefined | null = dto.user
if (user === undefined && dto.userUuid !== undefined) {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error(userUuidOrError.getError())
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
@@ -37,23 +42,44 @@ export class CreateCrossServiceToken implements UseCaseInterface {
}
if (!user) {
throw new Error(`Could not find user with uuid ${dto.userUuid}`)
return Result.fail(`Could not find user with uuid ${dto.userUuid}`)
}
const transitionStatus = await this.transitionStatusRepository.getStatus(user.uuid)
const roles = await user.roles
const authTokenData: CrossServiceTokenData = {
user: this.projectUser(user),
roles: this.projectRoles(roles),
shared_vault_owner_context: undefined,
ongoing_transition: transitionStatus === 'STARTED',
}
if (dto.sharedVaultOwnerContext !== undefined) {
const uploadBytesLimitSettingOrError = await this.getSettingUseCase.execute({
settingName: SettingName.NAMES.FileUploadBytesLimit,
userUuid: dto.sharedVaultOwnerContext,
})
if (uploadBytesLimitSettingOrError.isFailed()) {
return Result.fail(uploadBytesLimitSettingOrError.getError())
}
const uploadBytesLimitSetting = uploadBytesLimitSettingOrError.getValue()
if (uploadBytesLimitSetting.sensitive) {
return Result.fail('Shared vault owner upload bytes limit setting is sensitive!')
}
const uploadBytesLimit = parseInt(uploadBytesLimitSetting.setting.value as string)
authTokenData.shared_vault_owner_context = {
upload_bytes_limit: uploadBytesLimit,
}
}
if (dto.session !== undefined) {
authTokenData.session = this.projectSession(dto.session)
}
return {
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
}
return Result.ok(this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL))
}
private projectUser(user: User): { uuid: string; email: string } {
@@ -6,6 +6,7 @@ export type CreateCrossServiceTokenDTO = Either<
{
user: User
session?: Session
sharedVaultOwnerContext?: string
},
{
userUuid: string
@@ -1,3 +0,0 @@
export type CreateCrossServiceTokenResponse = {
token: string
}
@@ -73,35 +73,30 @@ describe('GetSetting', () => {
describe('no subscription', () => {
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.DropboxBackupFrequency,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
})
it('should not find a setting if the setting name is invalid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({
success: false,
error: {
message: 'Invalid setting name: invalid',
},
})
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })
expect(result.isFailed()).toBeTruthy()
})
it('should not get a setting for user if it does not exist', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: false,
error: {
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
},
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.DropboxBackupFrequency,
})
expect(result.isFailed()).toBeTruthy()
})
it('should not retrieve a sensitive setting for user', async () => {
@@ -112,21 +107,19 @@ describe('GetSetting', () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
success: true,
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
sensitive: true,
})
})
it('should not retrieve a subscription setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'No subscription found.',
},
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeTruthy()
})
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
@@ -137,14 +130,13 @@ describe('GetSetting', () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(
await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MfaSecret,
allowSensitiveRetrieval: true,
}),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MfaSecret,
allowSensitiveRetrieval: true,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
@@ -159,10 +151,12 @@ describe('GetSetting', () => {
})
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
@@ -171,14 +165,11 @@ describe('GetSetting', () => {
it('should not get a suscription setting for user if it does not exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
},
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeTruthy()
})
it('should not retrieve a sensitive subscription setting for user', async () => {
@@ -188,10 +179,12 @@ describe('GetSetting', () => {
.fn()
.mockReturnValue(subscriptionSetting)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
sensitive: true,
})
})
@@ -205,10 +198,12 @@ describe('GetSetting', () => {
})
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
@@ -221,10 +216,12 @@ describe('GetSetting', () => {
})
it('should find a regular subscription only setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.FileUploadBytesLimit,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
@@ -1,7 +1,7 @@
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { UseCaseInterface } from '../UseCaseInterface'
import TYPES from '../../../Bootstrap/Types'
import { SettingProjector } from '../../../Projection/SettingProjector'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
@@ -14,7 +14,7 @@ import { GetSettingResponse } from './GetSettingResponse'
import { UserSubscription } from '../../Subscription/UserSubscription'
@injectable()
export class GetSetting implements UseCaseInterface {
export class GetSetting implements UseCaseInterface<GetSettingResponse> {
constructor(
@inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector,
@inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
@@ -24,15 +24,10 @@ export class GetSetting implements UseCaseInterface {
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
) {}
async execute(dto: GetSettingDto): Promise<GetSettingResponse> {
async execute(dto: GetSettingDto): Promise<Result<GetSettingResponse>> {
const settingNameOrError = SettingName.create(dto.settingName)
if (settingNameOrError.isFailed()) {
return {
success: false,
error: {
message: settingNameOrError.getError(),
},
}
return Result.fail(settingNameOrError.getError())
}
const settingName = settingNameOrError.getValue()
@@ -47,12 +42,7 @@ export class GetSetting implements UseCaseInterface {
}
if (!subscription) {
return {
success: false,
error: {
message: 'No subscription found.',
},
}
return Result.fail('No subscription found.')
}
const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
@@ -62,28 +52,21 @@ export class GetSetting implements UseCaseInterface {
})
if (subscriptionSetting === null) {
return {
success: false,
error: {
message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`)
}
if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) {
return {
success: true,
return Result.ok({
sensitive: true,
}
})
}
const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting)
return {
success: true,
return Result.ok({
userUuid: dto.userUuid,
setting: simpleSubscriptionSetting,
}
})
}
const setting = await this.settingService.findSettingWithDecryptedValue({
@@ -92,27 +75,20 @@ export class GetSetting implements UseCaseInterface {
})
if (setting === null) {
return {
success: false,
error: {
message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
return Result.fail(`Setting ${settingName.value} for user ${dto.userUuid} not found!`)
}
if (setting.sensitive && !dto.allowSensitiveRetrieval) {
return {
success: true,
return Result.ok({
sensitive: true,
}
})
}
const simpleSetting = await this.settingProjector.projectSimple(setting)
return {
success: true,
return Result.ok({
userUuid: dto.userUuid,
setting: simpleSetting,
}
})
}
}
@@ -1,18 +1,13 @@
import { Either } from '@standardnotes/common'
import { SimpleSetting } from '../../Setting/SimpleSetting'
export type GetSettingResponse =
| {
success: true
userUuid: string
setting: SimpleSetting
}
| {
success: true
sensitive: true
}
| {
success: false
error: {
message: string
}
}
export type GetSettingResponse = Either<
{
userUuid: string
setting: SimpleSetting
},
{
sensitive: true
}
>
@@ -0,0 +1,108 @@
import { RoleName } from '@standardnotes/domain-core'
import { Role } from '../../Role/Role'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GetTransitionStatus } from './GetTransitionStatus'
describe('GetTransitionStatus', () => {
let transitionStatusRepository: TransitionStatusRepositoryInterface
let userRepository: UserRepositoryInterface
let user: User
let role: Role
const createUseCase = () => new GetTransitionStatus(transitionStatusRepository, userRepository)
beforeEach(() => {
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.getStatus = jest.fn().mockReturnValue(null)
role = {} as jest.Mocked<Role>
role.name = RoleName.NAMES.CoreUser
user = {
uuid: '00000000-0000-0000-0000-000000000000',
email: 'test@test.te',
} as jest.Mocked<User>
user.roles = Promise.resolve([role])
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
})
it('returns transition status FINISHED', async () => {
role.name = RoleName.NAMES.TransitionUser
user.roles = Promise.resolve([role])
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('FINISHED')
})
it('returns transition status STARTED', async () => {
const useCase = createUseCase()
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('STARTED')
})
it('returns transition status TO-DO', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('TO-DO')
})
it('returns transition status FAILED', async () => {
const useCase = createUseCase()
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('FAILED')
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('FAILED')
})
it('return error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
})
it('return error if user not found', async () => {
const useCase = createUseCase()
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('User not found.')
})
})
@@ -0,0 +1,39 @@
import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { GetTransitionStatusDTO } from './GetTransitionStatusDTO'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
export class GetTransitionStatus implements UseCaseInterface<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'> {
constructor(
private transitionStatusRepository: TransitionStatusRepositoryInterface,
private userRepository: UserRepositoryInterface,
) {}
async execute(dto: GetTransitionStatusDTO): Promise<Result<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
return Result.fail('User not found.')
}
const roles = await user.roles
for (const role of roles) {
if (role.name === RoleName.NAMES.TransitionUser) {
return Result.ok('FINISHED')
}
}
const transitionStatus = await this.transitionStatusRepository.getStatus(userUuid.value)
if (transitionStatus === null) {
return Result.ok('TO-DO')
}
return Result.ok(transitionStatus)
}
}
@@ -0,0 +1,3 @@
export interface GetTransitionStatusDTO {
userUuid: string
}
@@ -11,6 +11,7 @@ import { Register } from './Register'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { Session } from '../Session/Session'
import { RoleName } from '@standardnotes/domain-core'
describe('Register', () => {
let userRepository: UserRepositoryInterface
@@ -20,9 +21,19 @@ describe('Register', () => {
let user: User
let crypter: CrypterInterface
let timer: TimerInterface
let transitionModeEnabled = false
const createUseCase = () =>
new Register(userRepository, roleRepository, authResponseFactory, crypter, false, settingService, timer)
new Register(
userRepository,
roleRepository,
authResponseFactory,
crypter,
false,
settingService,
timer,
transitionModeEnabled,
)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
@@ -75,6 +86,7 @@ describe('Register', () => {
updatedWithUserAgent: 'Mozilla',
uuid: expect.any(String),
version: '004',
roles: Promise.resolve([]),
createdAt: new Date(1),
updatedAt: new Date(1),
})
@@ -118,6 +130,48 @@ describe('Register', () => {
})
})
it('should register a new user with default role and transition role', async () => {
transitionModeEnabled = true
const role = new Role()
role.name = RoleName.NAMES.CoreUser
const transitionRole = new Role()
transitionRole.name = RoleName.NAMES.TransitionUser
roleRepository.findOneByName = jest.fn().mockReturnValueOnce(role).mockReturnValueOnce(transitionRole)
expect(
await createUseCase().execute({
email: 'test@test.te',
password: 'asdzxc',
updatedWithUserAgent: 'Mozilla',
apiVersion: '20200115',
ephemeralSession: false,
version: '004',
pwCost: 11,
pwSalt: 'qweqwe',
pwNonce: undefined,
}),
).toEqual({ success: true, authResponse: { foo: 'bar' } })
expect(userRepository.save).toHaveBeenCalledWith({
email: 'test@test.te',
encryptedPassword: expect.any(String),
encryptedServerKey: 'test',
serverEncryptionVersion: 1,
pwCost: 11,
pwNonce: undefined,
pwSalt: 'qweqwe',
updatedWithUserAgent: 'Mozilla',
uuid: expect.any(String),
version: '004',
createdAt: new Date(1),
updatedAt: new Date(1),
roles: Promise.resolve([role, transitionRole]),
})
})
it('should fail to register if username is invalid', async () => {
expect(
await createUseCase().execute({
@@ -195,6 +249,7 @@ describe('Register', () => {
true,
settingService,
timer,
transitionModeEnabled,
).execute({
email: 'test@test.te',
password: 'asdzxc',
+12 -3
View File
@@ -1,8 +1,9 @@
import * as bcrypt from 'bcryptjs'
import { RoleName, Username } from '@standardnotes/domain-core'
import { v4 as uuidv4 } from 'uuid'
import { inject, injectable } from 'inversify'
import { TimerInterface } from '@standardnotes/time'
import TYPES from '../../Bootstrap/Types'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
@@ -11,7 +12,6 @@ import { RegisterResponse } from './RegisterResponse'
import { UseCaseInterface } from './UseCaseInterface'
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { TimerInterface } from '@standardnotes/time'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { AuthResponse20200115 } from '../Auth/AuthResponse20200115'
@@ -27,6 +27,7 @@ export class Register implements UseCaseInterface {
@inject(TYPES.Auth_DISABLE_USER_REGISTRATION) private disableUserRegistration: boolean,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
@inject(TYPES.Auth_TRANSITION_MODE_ENABLED) private transitionModeEnabled: boolean,
) {}
async execute(dto: RegisterDTO): Promise<RegisterResponse> {
@@ -72,10 +73,18 @@ export class Register implements UseCaseInterface {
user.encryptedServerKey = await this.crypter.generateEncryptedUserServerKey()
user.serverEncryptionVersion = User.DEFAULT_ENCRYPTION_VERSION
const roles = []
const defaultRole = await this.roleRepository.findOneByName(RoleName.NAMES.CoreUser)
if (defaultRole) {
user.roles = Promise.resolve([defaultRole])
roles.push(defaultRole)
}
if (this.transitionModeEnabled) {
const transitionRole = await this.roleRepository.findOneByName(RoleName.NAMES.TransitionUser)
if (transitionRole) {
roles.push(transitionRole)
}
}
user.roles = Promise.resolve(roles)
Object.assign(user, registrationFields)
@@ -93,7 +93,7 @@ describe('UpdateSetting', () => {
settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(true)
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn()
roleService.addUserRoleBasedOnSubscription = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
@@ -7,7 +7,6 @@ import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO'
import { User } from '../../User/User'
export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
constructor(
@@ -34,23 +33,20 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
}
await this.updateUploadBytesUsedSetting(regularSubscription, user, dto.bytesUsed)
await this.updateUploadBytesUsedSetting(regularSubscription, dto.bytesUsed)
if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, user, dto.bytesUsed)
await this.updateUploadBytesUsedSetting(sharedSubscription, dto.bytesUsed)
}
return Result.ok()
}
private async updateUploadBytesUsedSetting(
subscription: UserSubscription,
user: User,
bytesUsed: number,
): Promise<void> {
private async updateUploadBytesUsedSetting(subscription: UserSubscription, bytesUsed: number): Promise<void> {
let bytesAlreadyUsed = '0'
const subscriptionUser = await subscription.user
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid,
userUuid: subscriptionUser.uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
})
@@ -60,7 +56,7 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
user,
user: subscriptionUser,
props: {
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(),
@@ -0,0 +1,64 @@
import { RoleName, Uuid } from '@standardnotes/domain-core'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatus } from './UpdateTransitionStatus'
describe('UpdateTransitionStatus', () => {
let transitionStatusRepository: TransitionStatusRepositoryInterface
let roleService: RoleServiceInterface
const createUseCase = () => new UpdateTransitionStatus(transitionStatusRepository, roleService)
beforeEach(() => {
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.removeStatus = jest.fn()
transitionStatusRepository.updateStatus = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addRoleToUser = jest.fn()
})
it('should remove transition status and add TransitionUser role', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'FINISHED',
})
expect(result.isFailed()).toBeFalsy()
expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith('00000000-0000-0000-0000-000000000000')
expect(roleService.addRoleToUser).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
RoleName.create(RoleName.NAMES.TransitionUser).getValue(),
)
})
it('should update transition status', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'STARTED',
})
expect(result.isFailed()).toBeFalsy()
expect(transitionStatusRepository.updateStatus).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000',
'STARTED',
)
})
it('should return error when user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
status: 'STARTED',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
})
})
@@ -0,0 +1,31 @@
import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatusDTO } from './UpdateTransitionStatusDTO'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
export class UpdateTransitionStatus implements UseCaseInterface<void> {
constructor(
private transitionStatusRepository: TransitionStatusRepositoryInterface,
private roleService: RoleServiceInterface,
) {}
async execute(dto: UpdateTransitionStatusDTO): Promise<Result<void>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
if (dto.status === 'FINISHED') {
await this.transitionStatusRepository.removeStatus(dto.userUuid)
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
return Result.ok()
}
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.status)
return Result.ok()
}
}
@@ -0,0 +1,4 @@
export interface UpdateTransitionStatusDTO {
userUuid: string
status: 'STARTED' | 'FINISHED' | 'FAILED'
}
@@ -257,7 +257,7 @@ describe('VerifyMFA', () => {
})
it('should not pass if user is not found and pseudo u2f is required', async () => {
booleanSelector.select = jest.fn().mockReturnValueOnce(false).mockReturnValueOnce(true)
booleanSelector.select = jest.fn().mockReturnValueOnce(true).mockReturnValueOnce(true)
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
expect(
+22 -22
View File
@@ -48,33 +48,33 @@ export class VerifyMFA implements UseCaseInterface {
const user = await this.userRepository.findOneByUsernameOrEmail(username)
if (user == null) {
const mfaSelectorHash = crypto
const secondFactorSelectorHash = crypto
.createHash('sha256')
.update(`mfa-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.digest('hex')
const u2fSelectorHash = crypto
.createHash('sha256')
.update(`u2f-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.update(`second-factor-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.digest('hex')
const isPseudoMFARequired = this.booleanSelector.select(mfaSelectorHash, [true, false])
const isPseudoSecondFactorRequired = this.booleanSelector.select(secondFactorSelectorHash, [true, false])
if (isPseudoSecondFactorRequired) {
const u2fSelectorHash = crypto
.createHash('sha256')
.update(`u2f-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.digest('hex')
const isPseudoU2FRequired = this.booleanSelector.select(u2fSelectorHash, [true, false])
const isPseudoU2FRequired = this.booleanSelector.select(u2fSelectorHash, [true, false])
if (isPseudoMFARequired) {
return {
success: false,
errorTag: ErrorTag.MfaRequired,
errorMessage: 'Please enter your two-factor authentication code.',
errorPayload: { mfa_key: `mfa_${uuidv4()}` },
}
}
if (isPseudoU2FRequired) {
return {
success: false,
errorTag: ErrorTag.U2FRequired,
errorMessage: 'Please authenticate with your U2F device.',
if (isPseudoU2FRequired) {
return {
success: false,
errorTag: ErrorTag.U2FRequired,
errorMessage: 'Please authenticate with your U2F device.',
}
} else {
return {
success: false,
errorTag: ErrorTag.MfaRequired,
errorMessage: 'Please enter your two-factor authentication code.',
errorPayload: { mfa_key: `mfa_${uuidv4()}` },
}
}
}
@@ -0,0 +1,19 @@
import { TransitionStatusRepositoryInterface } from '../../Domain/Transition/TransitionStatusRepositoryInterface'
export class InMemoryTransitionStatusRepository implements TransitionStatusRepositoryInterface {
private statuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
async updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void> {
this.statuses.set(userUuid, status)
}
async removeStatus(userUuid: string): Promise<void> {
this.statuses.delete(userUuid)
}
async getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null> {
const status = this.statuses.get(userUuid) || null
return status
}
}
@@ -7,6 +7,7 @@ import { results } from 'inversify-express-utils'
import { User } from '../../Domain/User/User'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedInternalController', () => {
let getUserFeatures: GetUserFeatures
@@ -73,7 +74,7 @@ describe('AnnotatedInternalController', () => {
request.params.userUuid = '1-2-3'
request.params.settingName = 'foobar'
getSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
const httpResponse = <results.JsonResult>await createController().getSetting(request)
const result = await httpResponse.executeAsync()
@@ -91,7 +92,7 @@ describe('AnnotatedInternalController', () => {
request.params.userUuid = '1-2-3'
request.params.settingName = 'foobar'
getSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.JsonResult>await createController().getSetting(request)
const result = await httpResponse.executeAsync()
@@ -36,16 +36,26 @@ export class AnnotatedInternalController extends BaseHttpController {
@httpGet('/users/:userUuid/settings/:settingName')
async getSetting(request: Request): Promise<results.JsonResult> {
const result = await this.doGetSetting.execute({
const resultOrError = await this.doGetSetting.execute({
userUuid: request.params.userUuid,
settingName: request.params.settingName,
allowSensitiveRetrieval: true,
})
if (result.success) {
return this.json(result)
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json(result, 400)
return this.json({
success: true,
...resultOrError.getValue(),
})
}
}
@@ -11,6 +11,7 @@ import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossService
import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Session } from '../../Domain/Session/Session'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSessionsController', () => {
let getActiveSessionsForUser: GetActiveSessionsForUser
@@ -45,7 +46,7 @@ describe('AnnotatedSessionsController', () => {
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken>
createCrossServiceToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
createCrossServiceToken.execute = jest.fn().mockReturnValue(Result.ok('foobar'))
request = {
params: {},
@@ -10,6 +10,7 @@ import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
import { User } from '../../Domain/User/User'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSettingsController', () => {
let deleteSetting: DeleteSetting
@@ -85,7 +86,7 @@ describe('AnnotatedSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync()
@@ -119,7 +120,7 @@ describe('AnnotatedSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync()
@@ -6,6 +6,7 @@ import { results } from 'inversify-express-utils'
import { AnnotatedSubscriptionSettingsController } from './AnnotatedSubscriptionSettingsController'
import { User } from '../../Domain/User/User'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSubscriptionSettingsController', () => {
let getSetting: GetSetting
@@ -41,7 +42,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync()
@@ -58,7 +59,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync()
@@ -14,6 +14,7 @@ import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempt
import { InviteToSharedSubscription } from '../../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
import { UpdateUser } from '../../Domain/UseCase/UpdateUser'
import { User } from '../../Domain/User/User'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
describe('AnnotatedUsersController', () => {
let updateUser: UpdateUser
@@ -24,6 +25,7 @@ describe('AnnotatedUsersController', () => {
let increaseLoginAttempts: IncreaseLoginAttempts
let changeCredentials: ChangeCredentials
let inviteToSharedSubscription: InviteToSharedSubscription
let getTransitionStatus: GetTransitionStatus
let request: express.Request
let response: express.Response
@@ -38,6 +40,7 @@ describe('AnnotatedUsersController', () => {
clearLoginAttempts,
increaseLoginAttempts,
changeCredentials,
getTransitionStatus,
)
beforeEach(() => {
@@ -69,6 +72,9 @@ describe('AnnotatedUsersController', () => {
inviteToSharedSubscription = {} as jest.Mocked<InviteToSharedSubscription>
inviteToSharedSubscription.execute = jest.fn()
getTransitionStatus = {} as jest.Mocked<GetTransitionStatus>
getTransitionStatus.execute = jest.fn()
request = {
headers: {},
body: {},
@@ -18,6 +18,7 @@ import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
import { BaseUsersController } from './Base/BaseUsersController'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
@controller('/users')
export class AnnotatedUsersController extends BaseUsersController {
@@ -29,6 +30,7 @@ export class AnnotatedUsersController extends BaseUsersController {
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
@inject(TYPES.Auth_GetTransitionStatus) override getTransitionStatusUseCase: GetTransitionStatus,
) {
super(
updateUser,
@@ -38,6 +40,7 @@ export class AnnotatedUsersController extends BaseUsersController {
clearLoginAttempts,
increaseLoginAttempts,
changeCredentialsUseCase,
getTransitionStatusUseCase,
)
}
@@ -51,6 +54,11 @@ export class AnnotatedUsersController extends BaseUsersController {
return super.keyParams(request)
}
@httpGet('/transition-status', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async transitionStatus(request: Request, response: Response): Promise<results.JsonResult> {
return super.transitionStatus(request, response)
}
@httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
return super.deleteAccount(request, response)
@@ -285,6 +285,10 @@ export class BaseAuthController extends BaseHttpController {
authorizationHeader: <string>request.headers.authorization,
})
if (result.headers?.has('x-invalidate-cache')) {
response.setHeader('x-invalidate-cache', result.headers.get('x-invalidate-cache') as string)
}
return this.json(result.data, result.status)
}
@@ -45,12 +45,25 @@ export class BaseSessionsController extends BaseHttpController {
const user = authenticateRequestResponse.user as User
const result = await this.createCrossServiceToken.execute({
const sharedVaultOwnerContext = request.headers['x-shared-vault-owner-context'] as string | undefined
const resultOrError = await this.createCrossServiceToken.execute({
user,
session: authenticateRequestResponse.session,
sharedVaultOwnerContext,
})
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json({ authToken: result.token })
return this.json({ authToken: resultOrError.getValue() })
}
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
@@ -58,13 +58,22 @@ export class BaseSettingsController extends BaseHttpController {
}
const { userUuid, settingName } = request.params
const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
if (result.success) {
return this.json(result)
const resultOrError = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json(result, 400)
return this.json({
success: true,
...resultOrError.getValue(),
})
}
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
@@ -14,15 +14,25 @@ export class BaseSubscriptionSettingsController extends BaseHttpController {
}
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.doGetSetting.execute({
const resultOrError = await this.doGetSetting.execute({
userUuid: response.locals.user.uuid,
settingName: request.params.subscriptionSettingName.toUpperCase(),
})
if (result.success) {
return this.json(result)
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json(result, 400)
return this.json({
success: true,
...resultOrError.getValue(),
})
}
}
@@ -10,6 +10,7 @@ import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription
import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
import { ErrorTag } from '@standardnotes/responses'
import { GetTransitionStatus } from '../../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
export class BaseUsersController extends BaseHttpController {
constructor(
@@ -20,6 +21,7 @@ export class BaseUsersController extends BaseHttpController {
protected clearLoginAttempts: ClearLoginAttempts,
protected increaseLoginAttempts: IncreaseLoginAttempts,
protected changeCredentialsUseCase: ChangeCredentials,
protected getTransitionStatusUseCase: GetTransitionStatus,
private controllerContainer?: ControllerContainerInterface,
) {
super()
@@ -30,6 +32,7 @@ export class BaseUsersController extends BaseHttpController {
this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this))
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
this.controllerContainer.register('auth.users.delete', this.deleteAccount.bind(this))
this.controllerContainer.register('auth.users.transition-status', this.transitionStatus.bind(this))
}
}
@@ -103,6 +106,29 @@ export class BaseUsersController extends BaseHttpController {
return this.json(result.keyParams)
}
async transitionStatus(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.getTransitionStatusUseCase.execute({
userUuid: response.locals.user.uuid,
})
if (result.isFailed()) {
return this.json(
{
error: {
message: result.getError(),
},
},
400,
)
}
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
return this.json({
status: result.getValue(),
})
}
async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(
@@ -46,10 +46,20 @@ export class BaseWebSocketsController extends BaseHttpController {
)
}
const result = await this.createCrossServiceToken.execute({
const resultOrError = await this.createCrossServiceToken.execute({
userUuid: token.userUuid,
})
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json({ authToken: result.token })
return this.json({ authToken: resultOrError.getValue() })
}
}
@@ -0,0 +1,23 @@
import * as IORedis from 'ioredis'
import { TransitionStatusRepositoryInterface } from '../../Domain/Transition/TransitionStatusRepositoryInterface'
export class RedisTransitionStatusRepository implements TransitionStatusRepositoryInterface {
private readonly PREFIX = 'transition'
constructor(private redisClient: IORedis.Redis) {}
async updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void> {
await this.redisClient.set(`${this.PREFIX}:${userUuid}`, status)
}
async removeStatus(userUuid: string): Promise<void> {
await this.redisClient.del(`${this.PREFIX}:${userUuid}`)
}
async getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null> {
const status = (await this.redisClient.get(`${this.PREFIX}:${userUuid}`)) as 'STARTED' | 'FAILED' | null
return status
}
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.26.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.2...@standardnotes/domain-core@1.26.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.25.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.1...@standardnotes/domain-core@1.25.2) (2023-08-09)
### Reverts

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